diff --git a/exist-core/src/main/java/org/exist/xquery/Function.java b/exist-core/src/main/java/org/exist/xquery/Function.java index f812391988c..a7f6bae07e1 100644 --- a/exist-core/src/main/java/org/exist/xquery/Function.java +++ b/exist-core/src/main/java/org/exist/xquery/Function.java @@ -517,6 +517,37 @@ public boolean isCalledAs(final String localName) { return localName.equals(mySignature.getName().getLocalPart()); } + /** + * Indicates whether this function's behaviour depends on the dynamic + * context (focus, current dateTime, default collation, static base URI, + * implicit timezone, ...). + *

+ * Used by {@link FunctionFactory#wrap(XQueryContext, Function)} to decide + * whether the lifted wrapper around a built-in must forward the caller's + * focus into the wrapped body when the built-in is used as a function + * item -- e.g. when retrieved via {@code fn:function-lookup} or as a + * named function reference. F&O 3.1 section 16.1.1 requires the + * static and dynamic context of the lookup site to form part of the + * closure of the returned function. + *

+ * Default is {@code false}. Overridden to {@code true} in each + * context-dependent built-in (the 0-arg forms of {@code fn:position}, + * {@code fn:last}, {@code fn:node-name}, {@code fn:string}, + * {@code fn:data}, {@code fn:base-uri}, {@code fn:string-length}, + * {@code fn:normalize-space}, {@code fn:has-children}, + * {@code fn:document-uri}, {@code fn:nilled}; the focus-using forms of + * {@code fn:lang}, {@code fn:id}, {@code fn:idref}, + * {@code fn:element-with-id}; and {@code fn:current-dateTime}, + * {@code fn:current-date}, {@code fn:current-time}, + * {@code fn:implicit-timezone}, {@code fn:default-collation}, + * {@code fn:default-language}, {@code fn:static-base-uri}). + * + * @return {@code true} if this function depends on the dynamic context + */ + public boolean isContextDependent() { + return false; + } + @Override public int getDependencies() { return Dependency.CONTEXT_ITEM | Dependency.CONTEXT_SET; diff --git a/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java b/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java index 87b721751af..b8ff10c7444 100644 --- a/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java +++ b/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java @@ -508,6 +508,20 @@ 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. Per F&O 3.1 section + // 16.1.1, the static and dynamic context of the call to + // fn:function-lookup -- and of named function references -- forms + // part of the closure of the returned function. When the wrapped + // built-in is itself context-dependent (fn:position#0, + // fn:node-name#0, fn:lang#1, fn:default-collation, + // fn:static-base-uri, ...), the wrapper must forward that captured + // focus into the body. Each Function subclass declares its own + // context-dependency via Function.isContextDependent(); the default + // is false, so non-context-dependent built-ins (fn:concat, + // fn:string-length#1, ...) and user-defined functions do not pay + // the propagation cost. + func.setPropagateContextToBody(call.isContextDependent()); for (final QName varName: variables) { func.addVariable(varName); } diff --git a/exist-core/src/main/java/org/exist/xquery/InternalFunctionCall.java b/exist-core/src/main/java/org/exist/xquery/InternalFunctionCall.java index 247c787d79d..d68d40f8a90 100644 --- a/exist-core/src/main/java/org/exist/xquery/InternalFunctionCall.java +++ b/exist-core/src/main/java/org/exist/xquery/InternalFunctionCall.java @@ -83,6 +83,11 @@ public int getArgumentCount() { return function.getArgumentCount(); } + @Override + public boolean isContextDependent() { + return function.isContextDependent(); + } + @Override public QName getName() { return function.getName(); diff --git a/exist-core/src/main/java/org/exist/xquery/UserDefinedFunction.java b/exist-core/src/main/java/org/exist/xquery/UserDefinedFunction.java index 839cbb6bc44..bb7962cf73b 100644 --- a/exist-core/src/main/java/org/exist/xquery/UserDefinedFunction.java +++ b/exist-core/src/main/java/org/exist/xquery/UserDefinedFunction.java @@ -48,6 +48,18 @@ public class UserDefinedFunction extends Function implements Cloneable { private boolean hasBeenReset = false; private List 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); } @@ -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 diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnDefaultLanguage.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnDefaultLanguage.java index dd2e6ad1c55..eb603675602 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnDefaultLanguage.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnDefaultLanguage.java @@ -41,6 +41,11 @@ public FnDefaultLanguage(final XQueryContext context) { super(context, FS_DEFAULT_LANGUAGE); } + @Override + public boolean isContextDependent() { + return true; + } + @Override public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnHasChildren.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnHasChildren.java index 8e3cd3e2eec..cb28163baca 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FnHasChildren.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FnHasChildren.java @@ -51,6 +51,11 @@ public FnHasChildren(final XQueryContext context, final FunctionSignature signat super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { final NodeValue node; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunBaseURI.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunBaseURI.java index 4bc9c42a071..a44a002982a 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunBaseURI.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunBaseURI.java @@ -77,6 +77,15 @@ public FunBaseURI(XQueryContext context, FunctionSignature signature) { super(context, signature); } + @Override + public boolean isContextDependent() { + // 0-arg base-uri() reads the focus; static-base-uri() reads the + // call-site's static base URI from the XQueryContext. Both forms + // must close over the lookup-site context when wrapped via + // fn:function-lookup or a named function reference. + return isCalledAs(FS_STATIC_BASE_URI) || getArgumentCount() == 0; + } + /* (non-Javadoc) * @see org.exist.xquery.BasicFunction#eval(org.exist.xquery.value.Sequence[], * org.exist.xquery.value.Sequence) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCurrentDateTime.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCurrentDateTime.java index 84e3bf0e7e3..a88a65bfefa 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCurrentDateTime.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCurrentDateTime.java @@ -82,6 +82,11 @@ public FunCurrentDateTime(XQueryContext context, FunctionSignature signature) { super(context, signature); } + @Override + public boolean isContextDependent() { + return true; + } + public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException { if (context.getProfiler().isEnabled()) { context.getProfiler().start(this); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunData.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunData.java index 8594c7ea63b..15ddd04e30b 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunData.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunData.java @@ -68,6 +68,11 @@ public FunData(XQueryContext context, FunctionSignature signature) { super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + /* (non-Javadoc) * @see org.exist.xquery.Expression#eval(org.exist.xquery.value.Sequence, * org.exist.xquery.value.Item) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDefaultCollation.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDefaultCollation.java index bcd5446dbbe..cea49c47916 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDefaultCollation.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDefaultCollation.java @@ -55,6 +55,11 @@ public FunDefaultCollation(XQueryContext context) { super(context, signature); } + @Override + public boolean isContextDependent() { + return true; + } + public Sequence eval(Sequence[] args, Sequence contextSequence) throws XPathException { if (context.getProfiler().isEnabled()) { context.getProfiler().start(this); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDocumentURI.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDocumentURI.java index ec81779ade3..e39dbf40f04 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDocumentURI.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDocumentURI.java @@ -60,6 +60,11 @@ public FunDocumentURI(final XQueryContext context, final FunctionSignature signa super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException { if (context.getProfiler().isEnabled()) { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunElementWithId.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunElementWithId.java index dd289251062..e1caa1dbf8e 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunElementWithId.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunElementWithId.java @@ -59,6 +59,11 @@ public FunElementWithId(final XQueryContext context, final FunctionSignature sig super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 1; + } + @Override public Sequence eval(final Sequence[] args, Sequence contextSequence) throws XPathException { if (getArgumentCount() < 1) { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunGenerateId.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunGenerateId.java index 1f46d3593f2..99689c0c062 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunGenerateId.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunGenerateId.java @@ -50,6 +50,11 @@ public FunGenerateId(final XQueryContext context, final FunctionSignature signat super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { final NodeValue node; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunId.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunId.java index ba330cfb4c4..8f1dc78dc4f 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunId.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunId.java @@ -71,6 +71,11 @@ public FunId(XQueryContext context, FunctionSignature signature) { super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 1; + } + /** * @see org.exist.xquery.Expression#eval(Sequence, Item) */ diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIdRef.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIdRef.java index 19637562f7e..7e00291c161 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIdRef.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIdRef.java @@ -92,6 +92,11 @@ public FunIdRef(XQueryContext context, FunctionSignature signature) { super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 1; + } + /** * @see org.exist.xquery.Expression#eval(Sequence, Item) */ diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunImplicitTimezone.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunImplicitTimezone.java index fcb4e8b1b09..64278942f4b 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunImplicitTimezone.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunImplicitTimezone.java @@ -54,6 +54,11 @@ public FunImplicitTimezone(XQueryContext context) { super(context, signature); } + @Override + public boolean isContextDependent() { + return true; + } + public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException { if (context.getProfiler().isEnabled()) { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLang.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLang.java index 9df5559dbe6..4fe22008812 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLang.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLang.java @@ -97,6 +97,11 @@ public FunLang(XQueryContext context, FunctionSignature signature) { super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 1; + } + public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { super.analyze(contextInfo); try { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLast.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLast.java index f5b6ad971e3..feb07c6d0dc 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLast.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLast.java @@ -63,6 +63,11 @@ public int getDependencies() { Dependency.CONTEXT_POSITION; } + @Override + public boolean isContextDependent() { + return true; + } + /* (non-Javadoc) * @see org.exist.xquery.functions.Function#eval(org.exist.xquery.StaticContext, org.exist.dom.persistent.DocumentSet, org.exist.xquery.value.Sequence, org.exist.xquery.value.Item) */ diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLocalName.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLocalName.java index df9a253d2bf..b81760b07dc 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLocalName.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLocalName.java @@ -90,6 +90,11 @@ public FunLocalName(final XQueryContext context, final FunctionSignature signatu super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { if (context.getProfiler().isEnabled()) { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunName.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunName.java index 9d32f9f432a..74e4f6a8fda 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunName.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunName.java @@ -96,6 +96,11 @@ public FunName(final XQueryContext context, final FunctionSignature signature) { super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { if (context.getProfiler().isEnabled()) { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNamespaceURI.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNamespaceURI.java index 09b579593c9..3251d11ec5c 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNamespaceURI.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNamespaceURI.java @@ -90,6 +90,11 @@ public FunNamespaceURI(final XQueryContext context, final FunctionSignature sign super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { if (context.getProfiler().isEnabled()) { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNilled.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNilled.java index 17aa71d6e66..ac57e5791fe 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNilled.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNilled.java @@ -58,6 +58,11 @@ public FunNilled(final XQueryContext context, final FunctionSignature signature) super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNodeName.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNodeName.java index d45bb74be1f..e3d364beee5 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNodeName.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNodeName.java @@ -73,6 +73,11 @@ public FunNodeName(final XQueryContext context, final FunctionSignature signatur super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { if (context.getProfiler().isEnabled()) { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNormalizeSpace.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNormalizeSpace.java index cd922e65a86..d22f3d674b9 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNormalizeSpace.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNormalizeSpace.java @@ -93,6 +93,11 @@ public FunNormalizeSpace(final XQueryContext context, final FunctionSignature si super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override public int returnsType() { return Type.STRING; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNumber.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNumber.java index ca421938d3e..112fb60f0a4 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNumber.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNumber.java @@ -97,6 +97,11 @@ public FunNumber(XQueryContext context, FunctionSignature signature) { super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException { if(context.getProfiler().isEnabled()) { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunOnFunctions.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunOnFunctions.java index b6844b0d0cd..27d0df014bd 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunOnFunctions.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunOnFunctions.java @@ -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(); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunPath.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunPath.java index a78600ea214..98a49b0d36a 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunPath.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunPath.java @@ -55,6 +55,11 @@ public FunPath(final XQueryContext context, final FunctionSignature signature) { super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException { final Sequence result; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunPosition.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunPosition.java index f19c5a7d210..936df38b1ee 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunPosition.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunPosition.java @@ -62,6 +62,11 @@ public int getDependencies() { Dependency.CONTEXT_POSITION; } + @Override + public boolean isContextDependent() { + return true; + } + public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException { if (context.getProfiler().isEnabled()) { context.getProfiler().start(this); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunRoot.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunRoot.java index fcc444c6cfb..13b2a051f9f 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunRoot.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunRoot.java @@ -84,6 +84,11 @@ public FunRoot(final XQueryContext context, final FunctionSignature signature) { super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { if (context.getProfiler().isEnabled()) { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStrLength.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStrLength.java index f56586b006f..db27888a03c 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStrLength.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStrLength.java @@ -67,6 +67,11 @@ public FunStrLength(final XQueryContext context, final FunctionSignature signatu super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override protected Tuple2 strictCheckArgumentType(Expression argument, @Nullable final SequenceType argType, final AnalyzeContextInfo argContextInfo, final int argPosition, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunString.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunString.java index af79dd4ac02..70c8a42cc30 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunString.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunString.java @@ -67,6 +67,11 @@ public FunString(final XQueryContext context, final FunctionSignature signature) super(context, signature); } + @Override + public boolean isContextDependent() { + return getArgumentCount() == 0; + } + @Override public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { if (context.getProfiler().isEnabled()) { diff --git a/exist-core/src/main/java/org/exist/xquery/value/FunctionReference.java b/exist-core/src/main/java/org/exist/xquery/value/FunctionReference.java index bbac6e112b5..07eaaf72d9f 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/FunctionReference.java +++ b/exist-core/src/main/java/org/exist/xquery/value/FunctionReference.java @@ -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); } @@ -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; } @@ -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); } /** @@ -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); } @@ -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); } diff --git a/exist-core/src/test/xquery/xquery3/higher-order.xml b/exist-core/src/test/xquery/xquery3/higher-order.xml deleted file mode 100644 index e22d6bf95f1..00000000000 --- a/exist-core/src/test/xquery/xquery3/higher-order.xml +++ /dev/null @@ -1,762 +0,0 @@ - - - - xquery3 higher-order functions - -

Higher-order functions in XQuery 3.0

- Wolfgang Meier - - - - - - - - - - Named function reference - - - 70 - - - Named function reference in "local" namespace - - - 70 - - - Named function reference on internal standard function - - - 3 - - - Backwards compatibility test 1 - - - 70 - - - Backwards compatibility test 2 - - - 70 - - - Annotation support test - - - 70 - - - Inline function - - - HELLO WORLD! - - - Inline function with annotation - - - HELLO WORLD! - - - Inline function as parameter in function call - - - HELLO WORLD! - - - Immediate execution of inline function - - - Hello - - - Combine named function reference, dynamic calls and inline functions - - - HELLO WORLD! - - - Function sequence type: any function - - - HELLO WORLD! - - - Function sequence type: one parameter - - - HELLO WORLD! - - - Function sequence type: two parameters 1 - - - 15 - - - Function sequence type: two parameters 2 - - - 210 - - - Function sequence type: two parameters 3 - - - 1 4 9 16 25 - - - Function sequence type: parenthesized type - - - OK - - - Calling function in imported module - - - 210 - - - Dynamic function call syntax test 1 - - 2 - - - Dynamic function call syntax test 2 - - 3 - - - Dynamic function call syntax test 3 - - 1 - - - Dynamic function call syntax test 4 - 1] -]]> - 2 3 - - - Dynamic function call syntax test 5 - - Hello you - - - Dynamic function call syntax test 6 - - HELLO - - - fn:function-lookup - - - 70 - - - fn:function-lookup on unknown function - - - - - - fn:function-lookup on function in imported module - - - 210 - - - fn:function-arity - - - 2 - - - fn:function-name - - - ex:add - - - fn:function-name on anonymous function - - - - - - fn:for-each function - fn:for-each(1 to 5, function($a) { $a * $a }) - 1 4 9 16 25 - - - fn:for-each on strings - fn:for-each(("john", "jane"), fn:string-to-codepoints#1) - 106 111 104 110 106 97 110 101 - - - fn:for-each with cast - fn:for-each(("23", "29"), xs:int#1) - 23 29 - - - fn:filter function (wrong argument order: deprecated) - fn:filter(function($a) {$a mod 2 = 0}, (1 to 10)) - XPTY0004 - - - fn:filter function - fn:filter(1 to 10, function($a) {$a mod 2 = 0}) - 2 4 6 8 10 - - - fn:fold-left function 1 - fn:fold-left(1 to 5, (), function($a, $b) {($b, $a)}) - 5 4 3 2 1 - - - fn:fold-left function 2 - fold-left((2,3,5,7), 1, function($a, $b) { $a * $b }) - 210 - - - fn:fold-left function 3 - fn:fold-left(1 to 5, "$zero", fn:concat("$f(", ?, ", ", ?, ")")) - $f($f($f($f($f($zero, 1), 2), 3), 4), 5) - - - fn:fold-left with partial application - fn:fold-left(1 to 5, "", fn:concat(?, ".", ?)) - .1.2.3.4.5 - - - fn:fold-left non-associative - fold-left(1 to 1000000, 10, function($a, $b){ ($a - ($a - 1)) div $b }) - 0.000001 - - - fn:fold-right function 1 - fn:fold-right(1 to 5, 0, function($a, $b) { $a + $b }) - 15 - - - fn:fold-right function 2 - fn:fold-right(1 to 5, "", function($a, $b) { concat($a, ".", $b) }) - 1.2.3.4.5. - - - fn:fold-right function 3 - fn:fold-right(1 to 5, "$zero", concat("$f(", ?, ", ", ?, ")")) - $f(1, $f(2, $f(3, $f(4, $f(5, $zero))))) - - - fn:fold-right non-associative - fold-right(1 to 1000000, 10, function($a, $b){ ($a - ($a - 1)) div $b }) - 10 - - - fn:for-each-pair function - fn:for-each-pair(1 to 5, 1 to 5, function($a, $b){10*$a + $b}) - 11 22 33 44 55 - - - fn:for-each-pair with strings - fn:for-each-pair(("a", "b", "c"), ("x", "y", "z"), concat#2) - ax by cz - - - - Partial function with fn:for-each - - 10 20 30 40 50 60 70 80 90 100 - - - Partial function with flwor - - 10 20 30 40 50 60 70 80 90 100 - - - Partial function application on dynamic call using named function reference - - 13 - - - Partial function application on dynamic call with function parameter - -) - -let $f := $a(?, function($x){ true() }) - -return $f($list)]]> - true - - - Partial function application: dynamic call with partial application - - 18 - - - Partial function application on inline function using map - - 5 10 - - - Partial function application on internal function - - a:b - - - Partial function application on internal overloaded function - - Hello world! Hello Wolf! - - - - Simple closure - {$p}

} -return -$f($b)]]>
-

Hello world!

-
- - Closure test: function passed to other function - {$b}

} -return - local:test($f)]]>
-

Hello world!

-
- - Closure test: inline function created in other function - { $b }

} -}; - -let $f := local:create() -return - local:test($f)]]>
-

Hello world!

-
- - Closure test: variable dependencies - {$b}

} -}; - -let $f := local:create("Hello") -return - local:test($f)]]>
-

Hello world!

-
- - Closure test: ensure variables are properly copied - - 1 2 3 4 5 6 7 8 9 10 - - - Closure test: redefined variable - - b - - diff --git a/exist-core/src/test/xquery/xquery3/higher-order.xqm b/exist-core/src/test/xquery/xquery3/higher-order.xqm new file mode 100644 index 00000000000..eb58bb50bb3 --- /dev/null +++ b/exist-core/src/test/xquery/xquery3/higher-order.xqm @@ -0,0 +1,912 @@ +(: + : eXist-db Open Source Native XML Database + : Copyright (C) 2001 The eXist-db Authors + : + : info@exist-db.org + : http://www.exist-db.org + : + : This library is free software; you can redistribute it and/or + : modify it under the terms of the GNU Lesser General Public + : License as published by the Free Software Foundation; either + : version 2.1 of the License, or (at your option) any later version. + : + : This library is distributed in the hope that it will be useful, + : but WITHOUT ANY WARRANTY; without even the implied warranty of + : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + : Lesser General Public License for more details. + : + : You should have received a copy of the GNU Lesser General Public + : License along with this library; if not, write to the Free Software + : Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + :) +xquery version "3.1"; + +(:~ + : Higher-order function tests, originally authored by Wolfgang Meier. + : Converted from the legacy XML format to an XQSuite module + : per duncdrum's review on PR #6352 so the entire suite uses a single + : annotation-based assertion format. Each scenario is wrapped in + : util:eval to preserve the per-test compilation isolation of the + : original format (each test had its own xquery version prologue and + : local:/ex: module-level function declarations). + :) +module namespace ho = "http://exist-db.org/xquery/test/higher-order"; + +declare namespace test = "http://exist-db.org/xquery/xqsuite"; + +declare + %test:setUp +function ho:setup() { + let $col := xmldb:create-collection("/db", "xq3-test") + return + xmldb:store($col, "test1.xql", + ``[module namespace ex2="http://exist-db.org/xquery/ex2"; + +(: copied from the XQuery spec :) +declare function ex2:fold-left( + $f as function(item()*, item()) as item()*, + $zero as item()*, + $seq as item()*) as item()* { + if (fn:empty($seq)) then $zero + else ex2:fold-left($f, $f($zero, $seq[1]), subsequence($seq, 2)) +};]``, + "application/xquery") +}; + +declare + %test:tearDown +function ho:cleanup() { + xmldb:remove("/db/xq3-test") +}; + +declare + %test:assertEquals("70") +function ho:named-function-reference() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +declare function ex:add($a, $b) { + $a + $b +}; + +let $f1 := ex:add#2 +return + $f1(20, 50)]``) +}; + +declare + %test:assertEquals("70") +function ho:named-function-reference-local-namespace() { + util:eval(``[xquery version "3.0"; + +declare function local:add($a, $b) { + $a + $b +}; + +let $f1 := local:add#2 +return + $f1(20, 50)]``) +}; + +declare + %test:assertEquals("3") +function ho:named-function-reference-internal-standard-function() { + util:eval(``[xquery version "1.0"; + +let $f := sum#1 +return + $f((1, 2))]``) +}; + +declare + %test:assertEquals("70") +function ho:backwards-compatibility-1() { + util:eval(``[xquery version "3.0"; + +declare function local:add($a, $b) { + $a + $b +}; + +let $f1 := util:function(xs:QName("local:add"), 2) +return + $f1(20, 50)]``) +}; + +declare + %test:assertEquals("70") +function ho:backwards-compatibility-2() { + util:eval(``[xquery version "3.0"; + +declare function local:add($a, $b) { + $a + $b +}; + +let $f1 := local:add#2 +return + util:call($f1, 20, 50)]``) +}; + +declare + %test:assertEquals("70") +function ho:annotation-support() { + util:eval(``[xquery version "3.0"; +declare namespace ex="http://exist-db.org/xquery/ex"; + +declare %ex:annotest function ex:add($a, $b) { + $a + $b +}; + +let $f1 := ex:add#2 +return + $f1(20, 50)]``) +}; + +declare + %test:assertEquals("HELLO", "WORLD!") +function ho:inline-function() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +declare function ex:apply($func, $list) { + for $item in $list return $func($item) +}; + +let $f2 := function($a) { upper-case($a) } +return + ex:apply($f2, ("Hello", "world!"))]``) +}; + +declare + %test:assertEquals("HELLO", "WORLD!") +function ho:inline-function-with-annotation() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +declare function ex:apply($func, $list) { + for $item in $list return $func($item) +}; + +let $f2 := %ex:annoex function($a) { upper-case($a) } +return + ex:apply($f2, ("Hello", "world!"))]``) +}; + +declare + %test:assertEquals("HELLO", "WORLD!") +function ho:inline-function-as-parameter() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +declare function ex:apply($func, $list) { + for $item in $list return $func($item) +}; + +ex:apply(function($a) { upper-case($a) }, ("Hello", "world!"))]``) +}; + +declare + %test:assertEquals("Hello") +function ho:immediate-execution-of-inline-function() { + util:eval(``[xquery version "3.0"; + +function($x) { $x }("Hello")]``) +}; + +declare + %test:assertEquals("HELLO", "WORLD!") +function ho:combine-named-reference-dynamic-calls-inline() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +declare function ex:apply($func, $list) { + for $item in $list return $func($item) +}; + +let $f1 := ex:apply#2 +return + $f1(function($a) { upper-case($a) }, ("Hello", "world!"))]``) +}; + +declare + %test:assertEquals("HELLO", "WORLD!") +function ho:function-sequence-type-any-function() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +declare function ex:apply($func as function(*), $list) { + for $item in $list return $func($item) +}; + +ex:apply(function($a) { upper-case($a) }, ("Hello", "world!"))]``) +}; + +declare + %test:assertEquals("HELLO", "WORLD!") +function ho:function-sequence-type-one-parameter() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +declare function ex:apply($func as function(xs:string*) as xs:string*, $list) { + for $item in $list return $func($item) +}; + +ex:apply(function($a) { upper-case($a) }, ("Hello", "world!"))]``) +}; + +declare + %test:assertEquals("15") +function ho:function-sequence-type-two-parameters-1() { + util:eval(``[ +(: copied from the XQuery spec :) +declare function local:fold-left( + $f as function(item()*, item()) as item()*, + $zero as item()*, + $seq as item()*) as item()* { + if (fn:empty($seq)) then $zero + else local:fold-left($f, $f($zero, $seq[1]), subsequence($seq, 2)) +}; + +local:fold-left(function($a, $b) { $a + $b }, 0, 1 to 5)]``) +}; + +declare + %test:assertEquals("210") +function ho:function-sequence-type-two-parameters-2() { + util:eval(``[ +declare function local:fold-left( + $f as function(item()*, item()) as item()*, + $zero as item()*, + $seq as item()*) as item()* { + if (fn:empty($seq)) then $zero + else local:fold-left($f, $f($zero, $seq[1]), subsequence($seq, 2)) +}; + +local:fold-left(function($a, $b) { $a * $b }, 1, (2,3,5,7))]``) +}; + +declare + %test:assertEquals("1", "4", "9", "16", "25") +function ho:function-sequence-type-two-parameters-3() { + util:eval(``[declare function local:map($f, $seq) { + if (fn:empty($seq)) + then () + else ($f($seq[1]), local:map($f, subsequence($seq, 2))) +}; + +local:map(function($a) { $a * $a }, 1 to 5)]``) +}; + +declare + %test:assertEquals("OK") +function ho:function-sequence-type-parenthesized-type() { + util:eval(``[ +declare function local:test( + $f as (function(item()*) as item()*)?) { + "OK" +}; + +local:test(())]``) +}; + +declare + %test:assertEquals("210") +function ho:calling-function-in-imported-module() { + util:eval(``[ +import module namespace ex2="http://exist-db.org/xquery/ex2" +at "xmldb:exist:///db/xq3-test/test1.xql"; + +let $f1 := ex2:fold-left#3 +return + $f1(function($a, $b) { $a * $b }, 1, (2,3,5,7))]``) +}; + +declare + %test:assertEquals("2") +function ho:dynfcall1() { + util:eval(``[xquery version "3.0"; + +declare function local:seq() { + (1, 2, 3) +}; + +let $seq := local:seq#0 +return + $seq()[2]]``) +}; + +declare + %test:assertEquals("3") +function ho:dynfcall2() { + util:eval(``[xquery version "3.0"; + +declare function local:seq() { + (1, 2, 3) +}; + +let $seq := local:seq#0 +return + $seq[1]()[3]]``) +}; + +declare + %test:assertEquals("1") +function ho:dynfcall3() { + util:eval(``[xquery version "3.0"; + +declare function local:seq() { + (1, 2, 3) +}; + +let $seq := local:seq#0 +return + ($seq)()[1]]``) +}; + +declare + %test:assertEquals("2", "3") +function ho:dynfcall4() { + util:eval(``[xquery version "3.0"; + +declare function local:seq() { + (1, 2, 3) +}; + +let $seq := local:seq#0 +return + $seq()[. > 1]]``) +}; + +declare + %test:assertEquals("Hello you") +function ho:dynfcall5() { + util:eval(``[xquery version "3.0"; +(: Immediate function evaluation :) +(function($a) { "Hello " || $a })("you")]``) +}; + +declare + %test:assertEquals("HELLO") +function ho:dynfcall6() { + util:eval(``[xquery version "3.0"; +declare function local:get-function() as function(xs:string) as xs:string { + function($str as xs:string) { upper-case($str) } +}; + +local:get-function()("Hello")]``) +}; + +declare + %test:assertEquals("70") +function ho:fn-function-lookup() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +declare function ex:add($a, $b) { + $a + $b +}; + +let $f1 := function-lookup(xs:QName("ex:add"), 2) +return + $f1(20, 50)]``) +}; + +declare + %test:assertEmpty +function ho:lookup-unknown() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +let $f1 := function-lookup(xs:QName("ex:add"), 2) +return + if (exists($f1)) then + $f1(20, 50) + else + ()]``) +}; + +declare + %test:assertEquals("210") +function ho:fn-function-lookup-on-imported-module() { + util:eval(``[xquery version "3.0"; + +import module namespace ex2="http://exist-db.org/xquery/ex2" +at "xmldb:exist:///db/xq3-test/test1.xql"; + +let $f1 := function-lookup(xs:QName("ex2:fold-left"), 3) +return + $f1(function($a, $b) { $a * $b }, 1, (2,3,5,7))]``) +}; + +declare + %test:assertEquals("2") +function ho:fn-function-arity() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +declare function ex:add($a, $b) { + $a + $b +}; + +let $f1 := ex:add#2 +return + function-arity($f1)]``) +}; + +declare + %test:assertEquals("ex:add") +function ho:fn-function-name() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +declare function ex:add($a, $b) { + $a + $b +}; + +let $f1 := ex:add#2 +return + xs:string(function-name($f1))]``) +}; + +declare + %test:assertEmpty +function ho:fn-function-name-anonymous() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +let $f1 := function($a, $b) { + $a + $b +} +return + function-name($f1)]``) +}; + +declare + %test:assertEquals("1", "4", "9", "16", "25") +function ho:fn-for-each-function() { + util:eval("fn:for-each(1 to 5, function($a) { $a * $a })") +}; + +declare + %test:assertEquals("106", "111", "104", "110", "106", "97", "110", "101") +function ho:fn-for-each-on-strings() { + util:eval(``[fn:for-each(("john", "jane"), fn:string-to-codepoints#1)]``) +}; + +declare + %test:assertEquals("23", "29") +function ho:fn-for-each-with-cast() { + util:eval(``[fn:for-each(("23", "29"), xs:int#1)]``) +}; + +declare + %test:assertError("err:XPTY0004") +function ho:fn-filter-wrong-argument-order-deprecated() { + util:eval("fn:filter(function($a) {$a mod 2 = 0}, (1 to 10))") +}; + +declare + %test:assertEquals("2", "4", "6", "8", "10") +function ho:fn-filter-function() { + util:eval("fn:filter(1 to 10, function($a) {$a mod 2 = 0})") +}; + +declare + %test:assertEquals("5", "4", "3", "2", "1") +function ho:fn-fold-left-1() { + util:eval("fn:fold-left(1 to 5, (), function($a, $b) {($b, $a)})") +}; + +declare + %test:assertEquals("210") +function ho:fn-fold-left-2() { + util:eval("fold-left((2,3,5,7), 1, function($a, $b) { $a * $b })") +}; + +declare + %test:assertEquals("$f($f($f($f($f($zero, 1), 2), 3), 4), 5)") +function ho:fn-fold-left-3() { + util:eval(``[fn:fold-left(1 to 5, "$zero", fn:concat("$f(", ?, ", ", ?, ")"))]``) +}; + +declare + %test:assertEquals(".1.2.3.4.5") +function ho:fn-fold-left-with-partial-application() { + util:eval(``[fn:fold-left(1 to 5, "", fn:concat(?, ".", ?))]``) +}; + +declare + %test:assertEquals("0.000001") +function ho:fn-fold-left-non-associative() { + util:eval("fold-left(1 to 1000000, 10, function($a, $b){ ($a - ($a - 1)) div $b })") +}; + +declare + %test:assertEquals("15") +function ho:fn-fold-right-1() { + util:eval("fn:fold-right(1 to 5, 0, function($a, $b) { $a + $b })") +}; + +declare + %test:assertEquals("1.2.3.4.5.") +function ho:fn-fold-right-2() { + util:eval(``[fn:fold-right(1 to 5, "", function($a, $b) { concat($a, ".", $b) })]``) +}; + +declare + %test:assertEquals("$f(1, $f(2, $f(3, $f(4, $f(5, $zero)))))") +function ho:fn-fold-right-3() { + util:eval(``[fn:fold-right(1 to 5, "$zero", concat("$f(", ?, ", ", ?, ")"))]``) +}; + +declare + %test:assertEquals("10") +function ho:fn-fold-right-non-associative() { + util:eval("fold-right(1 to 1000000, 10, function($a, $b){ ($a - ($a - 1)) div $b })") +}; + +declare + %test:assertEquals("11", "22", "33", "44", "55") +function ho:fn-for-each-pair-function() { + util:eval("fn:for-each-pair(1 to 5, 1 to 5, function($a, $b){10*$a + $b})") +}; + +declare + %test:assertEquals("ax", "by", "cz") +function ho:fn-for-each-pair-with-strings() { + util:eval(``[fn:for-each-pair(("a", "b", "c"), ("x", "y", "z"), concat#2)]``) +}; + +declare + %test:assertEquals("10", "20", "30", "40", "50", "60", "70", "80", "90", "100") +function ho:partial1() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +declare function ex:multiply($base, $number) { + $base * $number +}; + +(: Use function reference literal to find function at compile time :) +let $fMultiply := ex:multiply(10, ?) +return + for-each(1 to 10, $fMultiply)]``) +}; + +declare + %test:assertEquals("10", "20", "30", "40", "50", "60", "70", "80", "90", "100") +function ho:partial2() { + util:eval(``[xquery version "3.0"; + +declare namespace ex="http://exist-db.org/xquery/ex"; + +declare function ex:multiply($base, $number) { + $base * $number +}; + +(: Use function reference literal to find function at compile time :) +let $fMultiply := ex:multiply(10, ?) +for $i in 1 to 10 +return + $fMultiply($i)]``) +}; + +declare + %test:assertEquals("13") +function ho:partial3() { + util:eval(``[xquery version "1.0"; + +declare function local:func($a, $b, $c) { + $a + $b + $c +}; + +let $f := local:func#3 +let $ff := $f(5, ?, ?) +return + $ff(5, 3)]``) +}; + +declare + %test:assertEquals("true") +function ho:partial4() { + util:eval(``[xquery version "3.0"; + +declare variable $a := function($x as item()*, $f as function(item()*) as +xs:boolean) as xs:boolean { + $f($x) +}; + +let $list := ( + +) + +let $f := $a(?, function($x){ true() }) + +return $f($list)]``) +}; + +declare + %test:assertEquals("18") +function ho:partiald1() { + util:eval(``[xquery version "1.0"; + +declare function local:func($a, $b, $c) { + $a + $b + $c +}; + +let $f := local:func(10, ?, ?) +let $ff := $f(5, ?) +return + $ff(3)]``) +}; + +declare + %test:assertEquals("5", "10") +function ho:partiald2() { + util:eval(``[xquery version "1.0"; + +let $f := function ($a, $b) { $a * $b } +let $ff := $f(5, ?) +return + for-each((1, 2), $ff)]``) +}; + +declare + %test:assertEquals("a:b") +function ho:internal1() { + util:eval(``[xquery version "1.0"; + +let $f := string-join(?, ":") +return + $f(("a", "b"))]``) +}; + +declare + %test:assertEquals("Hello world!", "Hello Wolf!") +function ho:internal2() { + util:eval(``[xquery version "1.0"; + +let $f := concat("Hello ", ?) +return + for-each(("world!", "Wolf!"), $f)]``) +}; + +declare + %test:assertEquals("

Hello world!

") +function ho:simple-closure() { + util:eval(``[xquery version "3.0"; + +let $a := "Hello" +let $b := "Hello" || " world!" +let $f := function ($p) {

{$p}

} +return +$f($b)]``) +}; + +declare + %test:assertEquals("

Hello world!

") +function ho:closure-function-passed-to-other-function() { + util:eval(``[xquery version "3.0"; + +declare function local:test($f as function() as element()) { + $f() +}; + +let $a := "Hello" +let $b := "Hello" || " world!" +let $f := function () {

{$b}

} +return + local:test($f)]``) +}; + +declare + %test:assertEquals("

Hello world!

") +function ho:closure-inline-function-created-in-other-function() { + util:eval(``[xquery version "3.0"; + +declare function local:test($f as function() as element()) { + $f() +}; + +declare function local:create() { + let $a := "Hello" + let $b := "Hello" || " world!" + return function () {

{ $b }

} +}; + +let $f := local:create() +return + local:test($f)]``) +}; + +declare + %test:assertEquals("

Hello world!

") +function ho:closure-variable-dependencies() { + util:eval(``[xquery version "3.0"; + +declare function local:test($f as function() as element()) { + $f() +}; + +declare function local:create($a) { + let $b := $a || " world!" + return function () {

{$b}

} +}; + +let $f := local:create("Hello") +return + local:test($f)]``) +}; + +declare + %test:assertEquals("1", "2", "3", "4", "5", "6", "7", "8", "9", "10") +function ho:closure-variables-properly-copied() { + util:eval(``[xquery version "3.0"; + +declare function local:create() { + for $i in 1 to 10 + return + function () { $i } +}; + +for $f in local:create() +return + $f()]``) +}; + +declare + %test:assertEquals("root") +function ho:lookup-captures-focus-node-name() { + util:eval(``[xquery version "3.0"; +let $doc := document { } +return + $doc/root/function-lookup(xs:QName('fn:node-name'), 0)()]``) +}; + +declare + %test:assertEquals("root") +function ho:lookup-captures-focus-xqts-002-shape() { + util:eval(``[xquery version "3.0"; +let $doc := document { } +return + ($doc/root)/function-lookup(xs:QName('fn:node-name'), 0)()]``) +}; + +declare + %test:assertEquals("hello") +function ho:lookup-captures-focus-string() { + util:eval(``[xquery version "3.0"; +let $doc := document { hello } +return + $doc/root/child/function-lookup(xs:QName('fn:string'), 0)()]``) +}; + +declare + %test:assertEquals("true") +function ho:lookup-captures-focus-lang() { + util:eval(``[xquery version "3.0"; +let $doc := document { } +return + $doc/root/function-lookup(xs:QName('fn:lang'), 1)('en')]``) +}; + +declare + %test:assertEquals("true") +function ho:lookup-captures-focus-has-children() { + util:eval(``[xquery version "3.0"; +let $doc := document { } +return + $doc/function-lookup(xs:QName('fn:has-children'), 0)()]``) +}; + +declare + %test:assertEquals("0") +function ho:lookup-captures-focus-id-1-arg() { + util:eval(``[xquery version "3.0"; +let $doc := document { } +return + count($doc/function-lookup(xs:QName('fn:id'), 1)('nope'))]``) +}; + +declare + %test:assertEquals("ex:greeting") +function ho:lookup-captures-focus-name() { + util:eval(``[xquery version "3.0"; +declare namespace ex="http://exist-db.org/xquery/ex"; +let $doc := document { } +return + $doc/*/function-lookup(xs:QName('fn:name'), 0)()]``) +}; + +declare + %test:assertEquals("child") +function ho:lookup-captures-focus-local-name() { + util:eval(``[xquery version "3.0"; +let $doc := document { } +return + $doc/root/child/function-lookup(xs:QName('fn:local-name'), 0)()]``) +}; + +declare + %test:assertEquals("true") +function ho:lookup-captures-focus-base-uri() { + util:eval(``[xquery version "3.0"; +let $doc := document { } +let $direct := $doc/root/fn:base-uri() +let $viaLookup := $doc/root/function-lookup(xs:QName('fn:base-uri'), 0)() +return + ($direct = $viaLookup) or (empty($direct) and empty($viaLookup))]``) +}; + +declare + %test:assertEquals("/Q{}root[1]/Q{}child[1]") +function ho:lookup-captures-focus-path() { + util:eval(``[xquery version "3.0"; +let $doc := document { } +return + $doc/root/child/function-lookup(xs:QName('fn:path'), 0)()]``) +}; + +declare + %test:assertEquals("http://www.w3.org/2005/xpath-functions/collation/codepoint") +function ho:lookup-default-collation-closes-over-static-context() { + util:eval(``[xquery version "3.0"; +function-lookup(xs:QName('fn:default-collation'), 0)()]``) +}; + +declare + %test:assertEquals("true") +function ho:lookup-static-base-uri-closes-over-static-context() { + util:eval(``[xquery version "3.0"; +let $direct := fn:static-base-uri() +let $viaLookup := function-lookup(xs:QName('fn:static-base-uri'), 0)() +return + ($direct = $viaLookup) or (empty($direct) and empty($viaLookup))]``) +}; + +declare + %test:assertEquals("hello world") +function ho:lookup-fn-concat-not-context-dependent() { + util:eval(``[xquery version "3.0"; +function-lookup(xs:QName('fn:concat'), 2)('hello', ' world')]``) +}; + +declare + %test:assertEquals("5") +function ho:lookup-fn-string-length-1-arg-not-context-dependent() { + util:eval(``[xquery version "3.0"; +function-lookup(xs:QName('fn:string-length'), 1)('hello')]``) +}; + +declare + %test:assertEquals("5") +function ho:lookup-named-function-reference-no-captured-focus() { + util:eval(``[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)]``) +}; + +declare + %test:assertEquals("b") +function ho:closure-redefined-variable() { + util:eval(``[xquery version "3.0"; + +let $a := () +let $a := if ($a) then $a else "b" +return + function() { $a }()]``) +};