Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,13 @@ public static FunctionCall wrap(XQueryContext context, Function call) throws XPa
newSignature.setArgumentTypes(newParamArray);

final UserDefinedFunction func = new UserDefinedFunction(context, newSignature);
// This wrapper exists to lift a built-in Function into a FunctionCall
// so that it can be used as a function item. Built-ins may be
// context-dependent (fn:node-name#0, fn:position#0, fn:lang#1, ...),
// so let the wrapped body see the caller's focus. See F&O 3.1
// section 16.1.1 for the closure-of-context rule that motivates this
// for fn:function-lookup and named function references.
func.setPropagateContextToBody(true);
Comment thread
line-o marked this conversation as resolved.
Outdated
for (final QName varName: variables) {
func.addVariable(varName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ public class UserDefinedFunction extends Function implements Cloneable {
private boolean hasBeenReset = false;
private List<ClosureVariable> closureVariables = null;

// When true, propagate the caller's focus (context sequence + item) to the
// body. Set by FunctionFactory.wrap so that named-function-reference and
// fn:function-lookup wrappers around context-dependent built-ins
// (fn:node-name#0, fn:string#0, fn:position#0, fn:lang#1, ...) can see a
// focus. Plain user-defined functions are not supposed to depend on the
// caller's focus, so this flag stays false for them. See F&O 3.1 §16.1.1.
private boolean propagateContextToBody = false;

public void setPropagateContextToBody(final boolean propagateContextToBody) {
this.propagateContextToBody = propagateContextToBody;
}

public UserDefinedFunction(XQueryContext context, FunctionSignature signature) {
super(context, signature);
}
Expand Down Expand Up @@ -155,7 +167,11 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc
", got " + currentArguments[j].getItemCount());
}
}
result = body.eval(null, null);
if (propagateContextToBody) {
result = body.eval(contextSequence, contextItem);
} else {
result = body.eval(null, null);
}
return result;
} finally {
// restore the local variable stack
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,21 @@ public Sequence eval(Sequence[] args, Sequence contextSequence)
}
throw e;
}
return call == null ? Sequence.EMPTY_SEQUENCE : new FunctionReference(this, call);
if (call == null) {
return Sequence.EMPTY_SEQUENCE;
}
final FunctionReference ref = new FunctionReference(this, call);
// F&O 3.1 section 16.1.1: the static and dynamic context of the
// call to fn:function-lookup forms part of the closure of the
// returned function. Capture the focus so that context-dependent
// built-ins (fn:position#0, fn:node-name#0, fn:lang#1, ...) run
// against the focus that was in scope at the lookup site.
final org.exist.xquery.value.Item capturedItem =
(contextSequence != null && !contextSequence.isEmpty())
? contextSequence.itemAt(0)
: null;
ref.setCapturedContext(contextSequence, capturedItem);
return ref;
} else if (isCalledAs("function-name")) {
final FunctionReference ref = (FunctionReference) args[0].itemAt(0);
final QName qname = ref.getSignature().getName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ public class FunctionReference extends AtomicValue implements AutoCloseable {

protected final FunctionCall functionCall;

// Focus captured at the time of fn:function-lookup. When non-null, this
// focus is substituted for the function body's evaluation, while the
// function's argument expressions are still evaluated in the outer focus.
// See F&O 3.1 section 16.1.1 (fn:function-lookup), which states that the
// implementation of a returned context-dependent built-in is associated
// with the dynamic context of the call to fn:function-lookup.
private Sequence capturedContextSequence;
private Item capturedContextItem;

public FunctionReference(final FunctionCall functionCall) {
this(null, functionCall);
}
Expand All @@ -52,6 +61,15 @@ public FunctionReference(final Expression expression, final FunctionCall functio
this.functionCall = functionCall;
}

public void setCapturedContext(final Sequence contextSequence, final Item contextItem) {
this.capturedContextSequence = contextSequence;
this.capturedContextItem = contextItem;
}

private boolean hasCapturedContext() {
return capturedContextSequence != null || capturedContextItem != null;
}

public FunctionCall getCall() {
return functionCall;
}
Expand Down Expand Up @@ -110,7 +128,7 @@ public void analyze(AnalyzeContextInfo contextInfo) throws XPathException {
* @throws XPathException in case of dynamic error
*/
public Sequence eval(Sequence contextSequence) throws XPathException {
return functionCall.eval(contextSequence, null);
return eval(contextSequence, null);
}

/**
Expand All @@ -122,6 +140,18 @@ public Sequence eval(Sequence contextSequence) throws XPathException {
* @throws XPathException in case of dynamic error
*/
public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException {
if (hasCapturedContext()) {
// Per F&O 3.1 section 16.1.1, when fn:function-lookup returns a
// context-dependent built-in, the function body must see the focus
// captured at lookup time. Argument expressions, however, still
// evaluate against the outer focus of the dynamic call site.
final int n = functionCall.getArgumentCount();
final Sequence[] seq = new Sequence[n];
for (int i = 0; i < n; i++) {
seq[i] = functionCall.getArgument(i).eval(contextSequence, contextItem);
}
return functionCall.evalFunction(capturedContextSequence, capturedContextItem, seq);
}
return functionCall.eval(contextSequence, contextItem);
}

Expand All @@ -135,6 +165,9 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr
* @throws XPathException in case of dynamic error
*/
public Sequence evalFunction(Sequence contextSequence, Item contextItem, Sequence[] seq) throws XPathException {
if (hasCapturedContext()) {
return functionCall.evalFunction(capturedContextSequence, capturedContextItem, seq);
}
return functionCall.evalFunction(contextSequence, contextItem, seq);
}

Expand Down
57 changes: 57 additions & 0 deletions exist-core/src/test/xquery/xquery3/higher-order.xml
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do you feel about translating the whole spec to XQsuite? I still find XSpec very cumbersome to read.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong feelings here. I'd be happy to switch, either just the ones added in this PR, or a broader set in a follow-on PR. Just let me know what you guys would like.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest switching the whole spec. Otherwise it ll be difficult to see what test there are at the same time.

Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,63 @@ return
$f()]]></code>
<expected>1 2 3 4 5 6 7 8 9 10</expected>
</test>
<test output="text">
<task>fn:function-lookup captures focus: fn:node-name#0</task>
<code><![CDATA[xquery version "3.0";
let $doc := document { <root/> }
return
$doc/root/function-lookup(xs:QName('fn:node-name'), 0)()]]></code>
<expected>root</expected>
</test>
<test output="text">
<task>fn:function-lookup captures focus: xqts-002 shape</task>
<code><![CDATA[xquery version "3.0";
let $doc := document { <root/> }
return
($doc/root)/function-lookup(xs:QName('fn:node-name'), 0)()]]></code>
<expected>root</expected>
</test>
<test output="text">
<task>fn:function-lookup captures focus: fn:string#0</task>
<code><![CDATA[xquery version "3.0";
let $doc := document { <root><child>hello</child></root> }
return
$doc/root/child/function-lookup(xs:QName('fn:string'), 0)()]]></code>
<expected>hello</expected>
</test>
<test output="text">
<task>fn:function-lookup captures focus: fn:lang#1</task>
<code><![CDATA[xquery version "3.0";
let $doc := document { <root xml:lang="en"/> }
return
$doc/root/function-lookup(xs:QName('fn:lang'), 1)('en')]]></code>
<expected>true</expected>
</test>
<test output="text">
<task>fn:function-lookup captures focus: fn:has-children#0</task>
<code><![CDATA[xquery version "3.0";
let $doc := document { <root><child/></root> }
return
$doc/function-lookup(xs:QName('fn:has-children'), 0)()]]></code>
<expected>true</expected>
</test>
<test output="text">
<task>fn:function-lookup: 1-arg fn:id captures focus, returns empty when no match</task>
<code><![CDATA[xquery version "3.0";
let $doc := document { <root/> }
return
count($doc/function-lookup(xs:QName('fn:id'), 1)('nope'))]]></code>
<expected>0</expected>
</test>
<test output="text">
<task>fn:function-lookup: named function reference still has no captured focus</task>
<code><![CDATA[xquery version "3.0";
declare namespace ex="http://exist-db.org/xquery/ex";
declare function ex:add($a, $b) { $a + $b };
let $f := function-lookup(xs:QName('ex:add'), 2)
return $f(2, 3)]]></code>
<expected>5</expected>
</test>
<test output="text">
<task>Closure test: redefined variable</task>
<code><![CDATA[xquery version "3.0";
Expand Down
Loading