diff --git a/basex-api/src/main/java/org/basex/http/web/WebModule.java b/basex-api/src/main/java/org/basex/http/web/WebModule.java index a6e52ea468..af5bd95bda 100644 --- a/basex-api/src/main/java/org/basex/http/web/WebModule.java +++ b/basex-api/src/main/java/org/basex/http/web/WebModule.java @@ -58,7 +58,7 @@ void parse(final Context ctx) throws QueryException, IOException { try(QueryContext qc = qc(ctx)) { // loop through all functions final String name = file.name(); - for(final StaticFunc sf : qc.functions.funcs()) { + for(final StaticFunc sf : qc.functions) { // only add functions that are defined in the same module (file) if(sf.expr != null && name.equals(new IOFile(sf.info.path()).name())) { final RestXqFunction rxf = new RestXqFunction(sf, this, qc); diff --git a/basex-core/src/main/java/org/basex/query/QueryContext.java b/basex-core/src/main/java/org/basex/query/QueryContext.java index 44325d2de3..661c571f36 100644 --- a/basex-core/src/main/java/org/basex/query/QueryContext.java +++ b/basex-core/src/main/java/org/basex/query/QueryContext.java @@ -259,14 +259,14 @@ public LibraryModule parseLibrary(final String query, final String uri) throws Q * @throws QueryException query exception */ public void assign(final StaticFunc func, final Expr... args) throws QueryException { - for(final StaticFunc sf : functions.funcs()) { + for(final StaticFunc sf : functions) { if(func.info.equals(sf.info)) { // disable inlining of called function to ensure explicit locks are considered if(!sf.anns.contains(Annotation._BASEX_INLINE)) { sf.anns = sf.anns.attach(new Ann(sf.info, Annotation._BASEX_INLINE, Itr.ZERO)); } // create and assign function call - final StaticFuncCall call = new StaticFuncCall(sf.name, args, null, sf.info, true); + final StaticFuncCall call = new StaticFuncCall(sf.name, args, null, sf.info); call.setFunc(sf); main = new MainModule(call, new VarScope(), sf.sc); updating = sf.updating(); diff --git a/basex-core/src/main/java/org/basex/query/QueryParser.java b/basex-core/src/main/java/org/basex/query/QueryParser.java index 504a0a3ebf..67661da154 100644 --- a/basex-core/src/main/java/org/basex/query/QueryParser.java +++ b/basex-core/src/main/java/org/basex/query/QueryParser.java @@ -68,8 +68,6 @@ public class QueryParser extends InputParser { SCHEMA_ELEMENT, PROCESSING_INSTRUCTION, TEXT, ARRAY, ENUM, FN, FUNCTION, GET, IF, ITEM, MAP, RECORD, SWITCH, TYPE, TYPESWITCH); - /** URIs of modules loaded by the current file. */ - public final TokenSet moduleURIs = new TokenSet(); /** Query context. */ public final QueryContext qc; /** Static context. */ @@ -84,8 +82,6 @@ public class QueryParser extends InputParser { private final ArrayList vars = new ArrayList<>(); /** Parsed functions. */ private final ArrayList funcs = new ArrayList<>(); - /** Function references. */ - private final ArrayList funcRefs = new ArrayList<>(); /** Types. */ private final QNmMap declaredTypes = new QNmMap<>(); /** Public types. */ @@ -161,7 +157,7 @@ final MainModule parseMain() throws QueryException { final VarScope vs = localVars.popContext(); final MainModule mm = new MainModule(expr, vs, sc); - mm.set(funcs, vars, publicTypes, moduleURIs, namespaces, options, moduleDoc); + mm.set(funcs, vars, publicTypes, sc.imports, namespaces, options, moduleDoc); finish(mm); check(mm); return mm; @@ -211,7 +207,7 @@ final LibraryModule parseLibrary(final boolean root) throws QueryException { qc.modStack.pop(); final LibraryModule lm = new LibraryModule(sc); - lm.set(funcs, vars, publicTypes, moduleURIs, namespaces, options, moduleDoc); + lm.set(funcs, vars, publicTypes, sc.imports, namespaces, options, moduleDoc); return lm; } catch(final QueryException expr) { mark(); @@ -271,13 +267,6 @@ private void finish(final MainModule mm) throws QueryException { qnames.assignURI(this, 0); if(sc.elemNS != null) sc.ns.add(EMPTY, sc.elemNS, null); RecordType.resolveRefs(recordTypeRefs, namedRecordTypes); - - for(final FuncRef fr : funcRefs) fr.resolve(); - - if(qc.contextValue != null) { - final Expr ctx = qc.contextValue.expr; - if(!sc.mixUpdates && ctx.has(Flag.UPD)) throw error(UPCTX, ctx); - } } /** @@ -288,15 +277,21 @@ private void finish(final MainModule mm) throws QueryException { private void check(final MainModule main) throws QueryException { // add record constructor functions for built-in record types for(final RecordType rt : Records.BUILT_IN.values()) { - if(qc.functions.get(rt.name(), rt.minFields()) == null) { + if(qc.functions.get(sc, rt.name(), rt.minFields()) == null) { declareRecordConstructor(rt, info()); } } - // check function calls and variable references - qc.functions.check(qc); + // resolve function calls + qc.functions.resolve(); + // check variable references qc.vars.check(); + if(qc.contextValue != null) { + final Expr ctx = qc.contextValue.expr; + if(!sc.mixUpdates && ctx.has(Flag.UPD)) throw error(UPCTX, ctx); + } + if(qc.updating) { // check updating semantics if updating expressions exist if(!sc.mixUpdates) { @@ -738,8 +733,8 @@ private void moduleImport() throws QueryException { final byte[] uri = trim(stringLiteral()); if(uri.length == 0) throw error(NSMODURI); if(!Uri.get(uri).isValid()) throw error(INVURI_X, uri); - if(moduleURIs.contains(token(uri))) throw error(DUPLMODULE_X, uri); - moduleURIs.add(uri); + if(sc.imports.contains(token(uri))) throw error(DUPLMODULE_X, uri); + sc.imports.add(uri); // add non-default namespace if(prefix != EMPTY) { @@ -943,7 +938,9 @@ private void functionDecl(final AnnList anns) throws QueryException { if(reserved(name)) throw error(RESERVED_X, name.local()); wsCheck("("); - if(sc.module != null && !eq(name.uri(), sc.module.uri())) throw error(MODULENS_X, name); + if(!anns.contains(Annotation.PRIVATE)) { + if(sc.module != null && !eq(name.uri(), sc.module.uri())) throw error(MODULENS_X, name); + } localVars.pushContext(false); final Params params = paramList(true); @@ -953,7 +950,7 @@ private void functionDecl(final AnnList anns) throws QueryException { final byte[] uri = name.uri(); if(NSGlobal.reserved(uri) || Functions.builtIn(name) != null) throw FNRESERVED_X.get(ii, name.string()); - final StaticFunc func = qc.functions.declare(name, params, expr, anns, doc, vs, ii); + final StaticFunc func = qc.functions.declare(sc, name, params, expr, anns, doc, vs, ii); funcs.add(func); } @@ -1079,7 +1076,8 @@ private void declareRecordConstructor(final RecordType rt, final InputInfo ii) final Expr expr = new CRecord(ii, rt, args); final String doc = docBuilder.toString(); final VarScope vs = localVars.popContext(); - final StaticFunc func = qc.functions.declare(name, params, expr, rt.anns(), doc, vs, info()); + final StaticFunc func = qc.functions.declare(sc, name, params, expr, rt.anns(), doc, vs, + info()); funcs.add(func); } @@ -2069,10 +2067,7 @@ private Expr arrow() throws QueryException { expr = Functions.dynamic(ex, fb); } else { final QNm funcName = name; - final boolean hasImport = moduleURIs.contains(funcName.uri()); - final FuncRef ref = new FuncRef(() -> Functions.get(funcName, fb, qc, hasImport)); - funcRefs.add(ref); - expr = ref; + expr = qc.functions.newRef(() -> Functions.get(funcName, fb, qc)); } if(mapping) { expr = new GFLWOR(ii, fr, expr); @@ -2747,10 +2742,7 @@ private Expr functionItem() throws QueryException { if(num instanceof final Itr itr) { final int i = (int) itr.itr(); final InputInfo info = info(); - final boolean hasImport = moduleURIs.contains(name.uri()); - final FuncRef fr = new FuncRef(() -> Functions.item(name, i, false, info, qc, hasImport)); - funcRefs.add(fr); - return fr; + return qc.functions.newRef(() -> Functions.item(name, i, false, info, qc)); } } } @@ -2975,10 +2967,7 @@ private Expr functionCall() throws QueryException { skipWs(); if(current('(')) { final FuncBuilder fb = argumentList(true, null); - final boolean hasImport = moduleURIs.contains(name.uri()); - final FuncRef fr = new FuncRef(() -> Functions.get(name, fb, qc, hasImport)); - funcRefs.add(fr); - return fr; + return qc.functions.newRef(() -> Functions.get(name, fb, qc)); } } pos = p; diff --git a/basex-core/src/main/java/org/basex/query/StaticContext.java b/basex-core/src/main/java/org/basex/query/StaticContext.java index 07ff6a03fc..9a6d95a532 100644 --- a/basex-core/src/main/java/org/basex/query/StaticContext.java +++ b/basex-core/src/main/java/org/basex/query/StaticContext.java @@ -42,6 +42,8 @@ public final class StaticContext { public byte[] funcNS; /** Name of module (not assigned for main module). */ public QNm module; + /** URIs of modules loaded by the current file. */ + public final TokenSet imports = new TokenSet(); /** Construction mode. */ public boolean strip; diff --git a/basex-core/src/main/java/org/basex/query/func/FuncRef.java b/basex-core/src/main/java/org/basex/query/func/FuncRef.java deleted file mode 100644 index 0f4ca3d5b2..0000000000 --- a/basex-core/src/main/java/org/basex/query/func/FuncRef.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.basex.query.func; - -import org.basex.query.*; -import org.basex.query.expr.*; -import org.basex.query.value.type.*; -import org.basex.query.var.*; -import org.basex.util.*; -import org.basex.util.hash.*; - -/** - * A reference to an unresolved function call or named function item, to be resolved after parsing, - * when all user-defined function declarations have been processed. - * - * @author BaseX Team, BSD License - * @author Gunther Rademacher - */ -public final class FuncRef extends Single { - /** Function to resolve this reference. */ - private final QuerySupplier resolve; - - /** - * Constructor. - * @param resolve function to resolve the reference - */ - public FuncRef(final QuerySupplier resolve) { - super(null, null, Types.ITEM_ZM); - this.resolve = resolve; - } - - /** - * Resolves the function reference. - * @throws QueryException query exception - */ - public void resolve() throws QueryException { - expr = resolve.get(); - } - - @Override - public void checkUp() throws QueryException { - expr.checkUp(); - } - - @Override - public boolean vacuous() { - return expr.vacuous(); - } - - @Override - public Expr compile(final CompileContext cc) throws QueryException { - return expr.compile(cc); - } - - @Override - public Expr copy(final CompileContext cc, final IntObjectMap vm) { - throw Util.notExpected(); - } - - @Override - public void toString(final QueryString qs) { - expr.toString(qs); - } -} diff --git a/basex-core/src/main/java/org/basex/query/func/Functions.java b/basex-core/src/main/java/org/basex/query/func/Functions.java index a01d3edf67..828078adb8 100644 --- a/basex-core/src/main/java/org/basex/query/func/Functions.java +++ b/basex-core/src/main/java/org/basex/query/func/Functions.java @@ -77,15 +77,14 @@ public static boolean staticURI(final byte[] uri) { * @param qnm function name * @param fb function arguments * @param qc query context - * @param hasImport indicates whether a module import for the function name's URI was present * @return function call * @throws QueryException query exception */ - public static Expr get(final QNm qnm, final FuncBuilder fb, final QueryContext qc, - final boolean hasImport) throws QueryException { + public static Expr get(final QNm qnm, final FuncBuilder fb, final QueryContext qc) + throws QueryException { // partial function call? - if(fb.placeholders > 0) return dynamic(item(qnm, fb.arity, false, fb.info, qc, hasImport), fb); + if(fb.placeholders > 0) return dynamic(item(qnm, fb.arity, false, fb.info, qc), fb); final QNm name = funcName(qnm, fb.arity, fb.info, qc); @@ -103,7 +102,7 @@ public static Expr get(final QNm qnm, final FuncBuilder fb, final QueryContext q } // user-defined function - return staticCall(name, fb, qc, hasImport); + return staticCall(name, fb, qc); } /** @@ -144,12 +143,11 @@ public static Expr dynamic(final Expr expr, final FuncBuilder fb) throws QueryEx * @param runtime {@code true} if this method is called at runtime * @param info input info (can be {@code null}) * @param qc query context - * @param hasImport indicates whether a module import for the function name's URI was present * @return literal if found, {@code null} otherwise * @throws QueryException query exception */ public static Expr item(final QNm qnm, final int arity, final boolean runtime, - final InputInfo info, final QueryContext qc, final boolean hasImport) throws QueryException { + final InputInfo info, final QueryContext qc) throws QueryException { final FuncBuilder fb = new FuncBuilder(info, arity, runtime); final QNm name = funcName(qnm, arity, info, qc); @@ -181,9 +179,9 @@ public static Expr item(final QNm qnm, final int arity, final boolean runtime, } // user-defined function - final StaticFunc sf = qc.functions.get(name, arity); + final StaticFunc sf = qc.functions.get(info.sc(), name, arity); if(sf != null) { - final Expr func = item(sf, fb, qc, hasImport); + final Expr func = item(sf, fb, qc); if(sf.updating) qc.updating(); return func; } @@ -200,12 +198,7 @@ public static Expr item(final QNm qnm, final int arity, final boolean runtime, } if(runtime) return null; - // closure - final StaticFuncCall call = staticCall(name, fb, qc, hasImport); - // safe cast (no context dependency, no runtime evaluation) - final Closure closure = (Closure) item(call, fb, null, name, false, false); - qc.functions.register(closure); - return closure; + throw qc.functions.unknownFunctionError(name, arity, info); } /** @@ -214,16 +207,16 @@ public static Expr item(final QNm qnm, final int arity, final boolean runtime, * with no namespace. * @param name function name * @param arity number of arguments - * @param info input info (can be {@code null}) + * @param info input info * @param qc query context * @return function name */ private static QNm funcName(final QNm name, final int arity, final InputInfo info, final QueryContext qc) { - if(name.hasURI() || qc.functions.get(name, arity) != null) return name; - final StaticContext sc = info != null ? info.sc() : null; - return new QNm(name.local(), sc != null && sc.funcNS != null ? sc.funcNS : FN_URI); + final StaticContext sc = info.sc(); + if(name.hasURI() || qc.functions.get(sc, name, arity) != null) return name; + return new QNm(name.local(), sc.funcNS != null ? sc.funcNS : FN_URI); } /** @@ -322,20 +315,18 @@ private static Cast constructorCall(final QNm name, final FuncBuilder fb) throws * @param name function name * @param fb function arguments * @param qc query context - * @param hasImport indicates whether a module import for the function name's URI was present * @return function call * @throws QueryException query exception */ private static StaticFuncCall staticCall(final QNm name, final FuncBuilder fb, - final QueryContext qc, final boolean hasImport) throws QueryException { + final QueryContext qc) throws QueryException { if(NSGlobal.reserved(name.uri()) && !Records.BUILT_IN.contains(name)) { throw qc.functions.similarError(name, fb.info); } - final StaticFuncCall call = new StaticFuncCall(name, fb.args(), fb.keywords, fb.info, - hasImport); - qc.functions.register(call); + final StaticFuncCall call = new StaticFuncCall(name, fb.args(), fb.keywords, fb.info); + qc.functions.setFunc(call, qc); return call; } @@ -344,19 +335,18 @@ private static StaticFuncCall staticCall(final QNm name, final FuncBuilder fb, * @param sf static function * @param fb function arguments * @param qc query context - * @param hasImport indicates whether a module import for the function name's URI was present * @return function item * @throws QueryException query exception */ - public static Expr item(final StaticFunc sf, final FuncBuilder fb, final QueryContext qc, - final boolean hasImport) throws QueryException { + public static Expr item(final StaticFunc sf, final FuncBuilder fb, final QueryContext qc) + throws QueryException { final FuncType sft = sf.funcType(); final int arity = fb.params.length; for(int a = 0; a < arity; a++) fb.add(sf.paramName(a), sft.argTypes[a], qc); final FuncType ft = FuncType.get(fb.anns, sft.declType, Arrays.copyOf(sft.argTypes, arity)); - final StaticFuncCall call = staticCall(sf.name, fb, qc, hasImport); + final StaticFuncCall call = staticCall(sf.name, fb, qc); if(call.func != null) fb.anns = call.func.anns; return item(call, fb, ft, sf.name, sf.updating, false); } diff --git a/basex-core/src/main/java/org/basex/query/func/StaticFuncCall.java b/basex-core/src/main/java/org/basex/query/func/StaticFuncCall.java index 0c93d3b1a7..61bd9c4633 100644 --- a/basex-core/src/main/java/org/basex/query/func/StaticFuncCall.java +++ b/basex-core/src/main/java/org/basex/query/func/StaticFuncCall.java @@ -34,8 +34,6 @@ public final class StaticFuncCall extends FuncCall { QNmMap keywords; /** Placeholder for an external call (if not {@code null}, will be returned by the compiler). */ ParseExpr external; - /** Indicates whether a module import for the function name's URI was present. */ - final boolean hasImport; /** * Function call constructor. @@ -43,11 +41,10 @@ public final class StaticFuncCall extends FuncCall { * @param args positional arguments * @param keywords keyword arguments (can be {@code null}) * @param info input info (can be {@code null}) - * @param hasImport indicates whether a module import for the function name's URI was present */ public StaticFuncCall(final QNm name, final Expr[] args, final QNmMap keywords, - final InputInfo info, final boolean hasImport) { - this(name, args, (StaticFunc) null, info, hasImport); + final InputInfo info) { + this(name, args, (StaticFunc) null, info); this.keywords = keywords; } @@ -57,14 +54,12 @@ public StaticFuncCall(final QNm name, final Expr[] args, final QNmMap keyw * @param args arguments * @param func referenced function (can be {@code null}) * @param info input info (can be {@code null}) - * @param hasImport indicates whether a module import for the function name's URI was present */ private StaticFuncCall(final QNm name, final Expr[] args, final StaticFunc func, - final InputInfo info, final boolean hasImport) { + final InputInfo info) { super(info, args); this.name = name; this.func = func; - this.hasImport = hasImport; } @Override @@ -127,7 +122,7 @@ public Value value(final QueryContext qc) throws QueryException { @Override public StaticFuncCall copy(final CompileContext cc, final IntObjectMap vm) { - return copyType(new StaticFuncCall(name, Arr.copyAll(cc, vm, exprs), func, info, hasImport)); + return copyType(new StaticFuncCall(name, Arr.copyAll(cc, vm, exprs), func, info)); } /** @@ -156,8 +151,10 @@ public void setFunc(final StaticFunc sf) throws QueryException { } } // check visibility - if(sf.anns.contains(Annotation.PRIVATE) && !sf.sc.baseURI().eq(sc().baseURI())) + if(sf.anns.contains(Annotation.PRIVATE) + && !Token.eq(QNm.uri(sf.sc.module), QNm.uri(sc().module))) { throw FUNCPRIVATE_X.get(info, name.string()); + } } /** diff --git a/basex-core/src/main/java/org/basex/query/func/StaticFuncs.java b/basex-core/src/main/java/org/basex/query/func/StaticFuncs.java index 0a70f4f799..87941e0748 100644 --- a/basex-core/src/main/java/org/basex/query/func/StaticFuncs.java +++ b/basex-core/src/main/java/org/basex/query/func/StaticFuncs.java @@ -1,13 +1,16 @@ package org.basex.query.func; import static org.basex.query.QueryError.*; +import static org.basex.query.QueryText.*; import java.util.*; import org.basex.core.*; import org.basex.query.*; +import org.basex.query.ann.*; import org.basex.query.expr.*; import org.basex.query.func.java.*; +import org.basex.query.util.hash.*; import org.basex.query.util.list.*; import org.basex.query.util.parse.*; import org.basex.query.value.item.*; @@ -24,14 +27,18 @@ * @author BaseX Team, BSD License * @author Christian Gruen */ -public final class StaticFuncs extends ExprInfo { - /** Function caches. */ - private final TokenObjectMap caches = new TokenObjectMap<>(); - /** Function calls. */ - private Map> callsMap; +public final class StaticFuncs extends ExprInfo implements Iterable { + /** Functions grouped by declaring module, then by QName, with lists of overloads. */ + private final TokenObjectMap>> funcsByModule = + new TokenObjectMap<>(); + /** Unresolved function references. */ + private final ArrayList unresolvedRefs = new ArrayList<>(); + /** Function calls by function. */ + private final Map> callsMap = new IdentityHashMap<>(); /** * Declares a new user-defined function. + * @param sc static context * @param name function name * @param params parameters with variables and optional default values * @param expr function body (can be {@code null}) @@ -42,39 +49,61 @@ public final class StaticFuncs extends ExprInfo { * @return static function reference * @throws QueryException query exception */ - public StaticFunc declare(final QNm name, final Params params, final Expr expr, - final AnnList anns, final String doc, final VarScope vs, final InputInfo info) - throws QueryException { + public StaticFunc declare(final StaticContext sc, final QNm name, final Params params, + final Expr expr, final AnnList anns, final String doc, final VarScope vs, + final InputInfo info) throws QueryException { + final byte[] modUri = Token.eq(name.uri(), FN_URI) ? FN_URI : QNm.uri(sc.module); final StaticFunc sf = new StaticFunc(name, params, expr, anns, vs, info, doc); - if(!cache(name.prefixId()).register(sf)) throw DUPLFUNC_X.get(sf.info, name.string()); + if(get(sc, name, sf.min, sf.arity()) != null) throw DUPLFUNC_X.get(info, name); + funcsByModule.computeIfAbsent(modUri, QNmMap::new).computeIfAbsent(name, ArrayList::new). + add(sf); return sf; } /** - * Registers a function call. - * @param call name function name - * @throws QueryException query exception + * Creates a new unresolved function reference. + * @param resolve function to resolve the reference + * @return unresolved function reference */ - void register(final StaticFuncCall call) throws QueryException { - cache(call.name.prefixId()).add(call); + public Expr newRef(final QuerySupplier resolve) { + final FuncRef fr = new FuncRef(resolve); + unresolvedRefs.add(fr); + return fr; } /** - * Registers a closure. - * @param closure wrapped literal + * Assigns a function to a static function call. + * @param call name function name + * @param qc query context + * @throws QueryException query exception */ - public void register(final Closure closure) { - cache(closure.funcName().prefixId()).add(closure); + void setFunc(final StaticFuncCall call, final QueryContext qc) throws QueryException { + final InputInfo info = call.info(); + final QNm name = call.name; + final int arity = call.arity(); + final StaticFunc func = get(info.sc(), name, arity); + if(func != null) { + if(func.expr == null) throw FUNCNOIMPL_X.get(func.info, func.name.prefixString()); + call.setFunc(func); + if(func.updating) qc.updating(); + // update map for direct lookups of function calls + callsMap.computeIfAbsent(func, k -> new ArrayList<>(1)).add(call); + } else { + final JavaCall java = JavaCall.get(name, call.exprs, qc, info); + if(java == null) throw unknownFunctionError(name, arity, info); + call.setExternal(java); + if(java.updating) qc.updating(); + } } /** - * Checks if all functions have been correctly declared, and initializes all function calls. - * @param qc query context + * Resolves all function calls. * @throws QueryException query exception */ - public void check(final QueryContext qc) throws QueryException { - for(final FuncCache cache : caches.values()) cache.init(qc); + public void resolve() throws QueryException { + for(final FuncRef fr : unresolvedRefs) fr.resolve(); + unresolvedRefs.clear(); } /** @@ -82,9 +111,7 @@ public void check(final QueryContext qc) throws QueryException { * @throws QueryException query exception */ public void checkUp() throws QueryException { - for(final FuncCache cache : caches()) { - for(final StaticFunc func : cache.funcs) func.checkUp(); - } + for(final StaticFunc func : this) func.checkUp(); } /** @@ -92,22 +119,54 @@ public void checkUp() throws QueryException { * @param cc compilation context */ public void compileAll(final CompileContext cc) { - for(final FuncCache cache : caches()) { - for(final StaticFunc func : cache.funcs) func.compile(cc); - } + for(final StaticFunc func : this) func.compile(cc); } /** * Returns the function with the given name and arity. + * @param sc static context * @param qname function name * @param arity function arity * @return function if found, {@code null} otherwise */ - public StaticFunc get(final QNm qname, final long arity) { - final FuncCache cache = caches.get(qname.prefixId()); - if(cache != null) { - for(final StaticFunc func : cache.funcs) { - if(arity >= func.min && arity <= func.arity()) return func; + public StaticFunc get(final StaticContext sc, final QNm qname, final int arity) { + return get(sc, qname, arity, arity); + } + + /** + * Returns a visible function with the given name and arity range. + * @param sc static context + * @param qname function name + * @param min minimum function arity + * @param max maximum function arity + * @return function if found, {@code null} otherwise + */ + private StaticFunc get(final StaticContext sc, final QNm qname, final int min, final int max) { + final byte[] funcUri = qname.uri(); + final byte[] modUri = Token.eq(funcUri, FN_URI) ? FN_URI : QNm.uri(sc.module); + StaticFunc func = get(modUri, qname, min, max); + if(func == null && sc.imports.contains(funcUri)) { + func = get(funcUri, qname, min, max); + if(func != null && func.anns.contains(Annotation.PRIVATE)) func = null; + } + return func; + } + + /** + * Returns a function with the given name, and arity in the given range, from the specified + * module. + * @param modUri module URI + * @param qname function name + * @param min minimum arity + * @param max maximum arity + * @return function if found, {@code null} otherwise + */ + private StaticFunc get(final byte[] modUri, final QNm qname, final int min, final int max) { + final QNmMap> funcsByName = funcsByModule.get(modUri); + if(funcsByName != null) { + final ArrayList funcs = funcsByName.get(qname); + if(funcs != null) { + for(final StaticFunc func : funcs) if(min <= func.arity() && max >= func.min) return func; } } return null; @@ -119,15 +178,6 @@ public StaticFunc get(final QNm qname, final long arity) { * @return sequence types, or {@code null} if function is not referenced */ SeqType[] seqTypes(final StaticFunc func) { - // initialize cache for direct lookups of function calls - if(callsMap == null) { - callsMap = new IdentityHashMap<>(); - for(final FuncCache cache : caches()) { - for(final StaticFuncCall call : cache.calls) { - callsMap.computeIfAbsent(call.func, k -> new ArrayList<>(1)).add(call); - } - } - } final ArrayList calls = callsMap.get(func); final int sl = func.arity(); if(calls == null || calls.isEmpty() || sl == 0) return null; @@ -142,211 +192,147 @@ SeqType[] seqTypes(final StaticFunc func) { return seqTypes; } + /** + * Creates an exception for an unknown function. + * @param name function name + * @param arity function arity + * @param info input info + * @return query exception + */ + QueryException unknownFunctionError(final QNm name, final int arity, + final InputInfo info) { + + final byte[] funcUri = name.uri(); + final StaticFunc sf = get(funcUri, name, arity, arity); + if(sf != null) return sf.anns.contains(Annotation.PRIVATE) ? FUNCPRIVATE_X.get(info, name) + : INVISIBLEFUNC_X.get(info, name); + final ArrayList modules = new ArrayList<>(2); + if(Token.eq(funcUri, FN_URI)) { + modules.add(FN_URI); + } else { + modules.add(QNm.uri(info.sc().module)); + if(info.sc().imports.contains(funcUri)) modules.add(funcUri); + } + final IntList arities = new IntList(); + for(final byte[] module : modules) { + final QNmMap> funcsByName = funcsByModule.get(module); + if(funcsByName != null) { + final ArrayList funcs = funcsByName.get(name); + if(funcs != null) { + for(final StaticFunc func : funcs) { + for(int a = func.min; a <= func.arity(); ++a) arities.add(a); + } + } + } + } + return arities.isEmpty() + ? similarError(name, info) + : Functions.wrongArity(arity, arities, false, info, name.prefixString()); + } + /** * Throws an exception if the name of a function is similar to the specified function name. * @param qname function name * @param info input info (can be {@code null}) * @return exception */ - public QueryException similarError(final QNm qname, final InputInfo info) { + QueryException similarError(final QNm qname, final InputInfo info) { // check local functions - final ArrayList list = new ArrayList<>(); - for(final FuncCache cache : caches()) { - for(final StaticFunc func : cache.funcs) { - if(func.expr != null) { - list.add(cache.qname()); - break; - } - } - } - final Object similar = Levenshtein.similar(qname.local(), list.toArray(QNm[]::new), + final QNmSet names = new QNmSet(); + for(final StaticFunc func : this) if(func.expr != null) names.add(func.name); + final QNm similar = (QNm) Levenshtein.similar(qname.local(), names.keys(), o -> ((QNm) o).local()); // return error for local or global function return WHICHFUNC_X.get(info, similar != null ? - similar(qname.prefixString(), ((QNm) similar).prefixString()) : + similar(qname.prefixString(), similar.prefixString()) : Functions.similar(qname)); } - /** - * Returns all user-defined functions. - * @return functions - */ - public StaticFunc[] funcs() { - final ArrayList list = new ArrayList<>(); - for(final FuncCache cache : caches()) list.addAll(cache.funcs); - return list.toArray(StaticFunc[]::new); + @Override + public Iterator iterator() { + return new Iterator<>() { + final Iterator>> modules = funcsByModule.values().iterator(); + Iterator> names = Collections.emptyIterator(); + Iterator funcs = Collections.emptyIterator(); + + @Override public boolean hasNext() { + while(!funcs.hasNext()) { + if(names.hasNext()) funcs = names.next().iterator(); + else if(modules.hasNext()) names = modules.next().values().iterator(); + else return false; + } + return true; + } + + @Override public StaticFunc next() { + if(!hasNext()) throw new NoSuchElementException(); + return funcs.next(); + } + }; } @Override public void toXml(final QueryPlan plan) { - if(!caches.isEmpty()) plan.add(plan.create(this), funcs()); + if(funcsByModule.isEmpty()) return; + final ArrayList list = new ArrayList<>(); + forEach(list::add); + plan.add(plan.create(this), list.toArray(StaticFunc[]::new)); } @Override public void toString(final QueryString qs) { - for(final FuncCache cache : caches()) { - for(final StaticFunc func : cache.funcs) { - if(func.compiled()) qs.token(func).token(Text.NL); - } - } - } - - /** - * Returns all user-defined function caches. - * @return caches - */ - private ArrayList caches() { - final ArrayList list = new ArrayList<>(); - for(final FuncCache cache : caches.values()) { - if(!cache.funcs.isEmpty()) list.add(cache); - } - return list; + for(final StaticFunc func : this) if(func.compiled()) qs.token(func).token(Text.NL); } /** - * Returns a function cache for the specified function ID. - * @param id function ID - * @return function cache + * A reference to an initially unresolved function call or named function item, to be resolved + * after parsing, when all user-defined function declarations have been processed. */ - private FuncCache cache(final byte[] id) { - return caches.computeIfAbsent(id, FuncCache::new); - } - - /** - * Function cache. - * - * @author BaseX Team, BSD License - * @author Christian Gruen - */ - private static final class FuncCache { - /** Functions. */ - final ArrayList funcs = new ArrayList<>(1); - /** Function calls. */ - final ArrayList calls = new ArrayList<>(0); - /** Function closures. */ - final ArrayList closures = new ArrayList<>(0); + private static final class FuncRef extends Single { + /** Function to resolve this reference. */ + private final QuerySupplier resolve; /** - * Initializes the function calls and closures. - * @param qc query context - * @throws QueryException query exception + * Constructor. + * @param resolve function to resolve the reference */ - void init(final QueryContext qc) throws QueryException { - // assign functions to function calls - for(final StaticFuncCall call : calls) { - if(call.func == null && !setFunc(call) && !setJava(call, qc)) { - // function is unknown: raise error - if(!calls.isEmpty()) { - final IntList arities = new IntList(); - for(final StaticFunc func : funcs) arities.add(func.min).add(func.arity()); - final InputInfo info = call.info(); - throw arities.isEmpty() ? qc.functions.similarError(qname(), info) : - Functions.wrongArity(call.arity(), arities, false, info, qname().prefixString()); - } - } else { - // check if all implementations exist for all functions, set updating flag - final StaticFunc func = call.func; - if(func != null) { - if(!call.hasImport) { - final QNm funcMod = func.sc.module; - final QNm callMod = call.info().sc().module; - final byte[] funcModUri = funcMod == null ? Token.EMPTY : funcMod.uri(); - final byte[] callModUri = callMod == null ? Token.EMPTY : callMod.uri(); - if(!Token.eq(funcModUri, callModUri)) { - throw INVISIBLEFUNC_X.get(call.info(), call.name); - } - } - if(func.updating) qc.updating(); - } else if(((JavaCall) call.external).updating) qc.updating(); - } - } - - // assign function signatures to function closures - for(final Closure closure : closures) { - final int arity = closure.arity(); - for(final StaticFunc func : funcs) { - if(arity >= func.min && arity <= func.arity()) { - closure.setSignature(func.funcType()); - break; - } - } - } + FuncRef(final QuerySupplier resolve) { + super(null, null, Types.ITEM_ZM); + this.resolve = resolve; } /** - * Registers the specified function. - * @param func function to assign - * @return success flag + * Resolves the function reference. + * @throws QueryException query exception */ - boolean register(final StaticFunc func) { - /* Reject a function with a conflicting arity range. Examples: - * f($a), f($b) - * f($a), f($a, $b := ()) */ - final int nargs = func.arity(); - for(final StaticFunc sf : funcs) { - if(nargs >= sf.min && func.min <= sf.arity()) return false; - } - funcs.add(func); - return true; + void resolve() throws QueryException { + expr = resolve.get(); } - /** - * Adds a function call. - * @param call function call - * @throws QueryException query exception - */ - void add(final StaticFuncCall call) throws QueryException { - calls.add(call); - setFunc(call); + @Override + public void checkUp() throws QueryException { + expr.checkUp(); } - /** - * Tries to assign a function to a static function call. - * @param call static function call - * @return success flag - * @throws QueryException query exception - */ - boolean setFunc(final StaticFuncCall call) throws QueryException { - final int arity = call.arity(); - for(final StaticFunc func : funcs) { - if(arity >= func.min && arity <= func.arity()) { - if(func.expr == null) throw FUNCNOIMPL_X.get(func.info, func.name.prefixString()); - call.setFunc(func); - return true; - } - } - return false; + @Override + public boolean vacuous() { + return expr.vacuous(); } - /** - * Tries to assign a Java function to a static function call. - * @param call static function call - * @param qc query context - * @return success flag - * @throws QueryException query exception - */ - boolean setJava(final StaticFuncCall call, final QueryContext qc) throws QueryException { - final JavaCall java = closures.isEmpty() ? - JavaCall.get(call.name, call.exprs, qc, call.info()) : null; - call.setExternal(java); - return java != null; + @Override + public Expr compile(final CompileContext cc) throws QueryException { + return expr.compile(cc); } - /** - * Adds a closure. - * @param closure closure - */ - void add(final Closure closure) { - closures.add(closure); + @Override + public Expr copy(final CompileContext cc, final IntObjectMap vm) { + throw Util.notExpected(); } - /** - * Returns the function's name. - * @return function name - */ - QNm qname() { - return funcs.isEmpty() ? calls.get(0).name : funcs.get(0).name; + @Override + public void toString(final QueryString qs) { + expr.toString(qs); } } } diff --git a/basex-core/src/main/java/org/basex/query/func/fn/FnFunctionLookup.java b/basex-core/src/main/java/org/basex/query/func/fn/FnFunctionLookup.java index d2ad003139..190e2bb742 100644 --- a/basex-core/src/main/java/org/basex/query/func/fn/FnFunctionLookup.java +++ b/basex-core/src/main/java/org/basex/query/func/fn/FnFunctionLookup.java @@ -50,7 +50,7 @@ private Expr item(final QueryContext qc) throws QueryException { final long arity = toLong(arg(1), qc); if(arity >= 0 && arity <= Integer.MAX_VALUE) { try { - return Functions.item(name, (int) arity, true, info, qc, true); + return Functions.item(name, (int) arity, true, info, qc); } catch(final QueryException ex) { Util.debug(ex); } diff --git a/basex-core/src/main/java/org/basex/query/func/fn/FnLoadXQueryModule.java b/basex-core/src/main/java/org/basex/query/func/fn/FnLoadXQueryModule.java index 098a5ee506..7d2cfa4ad5 100644 --- a/basex-core/src/main/java/org/basex/query/func/fn/FnLoadXQueryModule.java +++ b/basex-core/src/main/java/org/basex/query/func/fn/FnLoadXQueryModule.java @@ -137,12 +137,12 @@ public XQMap item(final QueryContext qc, final InputInfo ii) throws QueryExcepti } final QNmMap> funcs = new QNmMap<>(); - for(final StaticFunc sf : mqc.functions.funcs()) { + for(final StaticFunc sf : mqc.functions) { if(sf.updating()) mqc.updating(); if(!sf.anns.contains(Annotation.PRIVATE) && Token.eq(sf.name.uri(), modUri)) { for(int a = sf.minArity(); a <= sf.arity(); ++a) { - final FuncBuilder fb = new FuncBuilder(info, a, true); - final Expr item = Functions.item(sf, fb, mqc, true); + final FuncBuilder fb = new FuncBuilder(sf.info, a, true); + final Expr item = Functions.item(sf, fb, mqc); funcs.computeIfAbsent(sf.name, HashMap::new).put(a, item); } } diff --git a/basex-core/src/main/java/org/basex/query/func/fn/FnSchemaType.java b/basex-core/src/main/java/org/basex/query/func/fn/FnSchemaType.java index 26c6c29a53..1fdd6853e8 100644 --- a/basex-core/src/main/java/org/basex/query/func/fn/FnSchemaType.java +++ b/basex-core/src/main/java/org/basex/query/func/fn/FnSchemaType.java @@ -105,7 +105,7 @@ protected static Value annotate(final QueryContext qc, final InputInfo info, fin if(matches != null) mb.put("matches", matches); if(constructor) { mb.put("constructor", FuncType.get(Types.ANY_ATOMIC_TYPE_ZM, Types.ANY_ATOMIC_TYPE_ZO).cast( - (FuncItem) Functions.item(name, 1, true, info, qc, true), qc, info)); + (FuncItem) Functions.item(name, 1, true, info, qc), qc, info)); } vb.add(mb.map()); } diff --git a/basex-core/src/main/java/org/basex/query/func/inspect/InspectFunction.java b/basex-core/src/main/java/org/basex/query/func/inspect/InspectFunction.java index 511ada39ad..17275e7330 100644 --- a/basex-core/src/main/java/org/basex/query/func/inspect/InspectFunction.java +++ b/basex-core/src/main/java/org/basex/query/func/inspect/InspectFunction.java @@ -1,6 +1,7 @@ package org.basex.query.func.inspect; import org.basex.query.*; +import org.basex.query.ann.*; import org.basex.query.func.*; import org.basex.query.value.item.*; import org.basex.query.value.node.*; @@ -18,7 +19,20 @@ public FNode item(final QueryContext qc, final InputInfo ii) throws QueryExcepti final FItem function = toFunction(arg(0), qc); final QNm name = function.funcName(); - final StaticFunc sf = name == null ? null : qc.functions.get(name, function.arity()); - return new PlainDoc(qc, info).function(name, sf, function.funcType(), function.annotations()); + StaticFunc func = null; + if(name != null) { + final int arity = function.arity(); + func = qc.functions.get(ii.sc(), name, arity); + if(func == null) { + for(final StaticFunc sf : qc.functions) { + if(!sf.annotations().contains(Annotation.PRIVATE) && sf.funcName().eq(name) + && sf.minArity() <= arity && sf.arity() >= arity) { + func = sf; + break; + } + } + } + } + return new PlainDoc(qc, info).function(name, func, function.funcType(), function.annotations()); } } diff --git a/basex-core/src/main/java/org/basex/query/func/inspect/InspectFunctions.java b/basex-core/src/main/java/org/basex/query/func/inspect/InspectFunctions.java index c175038d3b..76a853a01f 100644 --- a/basex-core/src/main/java/org/basex/query/func/inspect/InspectFunctions.java +++ b/basex-core/src/main/java/org/basex/query/func/inspect/InspectFunctions.java @@ -27,7 +27,7 @@ public Value value(final QueryContext qc) throws QueryException { // returns all functions from the query context if(source == null) { final ValueBuilder vb = new ValueBuilder(qc); - for(final StaticFunc sf : qc.functions.funcs()) { + for(final StaticFunc sf : qc.functions) { if(!NSGlobal.reserved(sf.name.uri())) addItems(vb, sf, qc); } return vb.value(this); @@ -40,7 +40,7 @@ public Value value(final QueryContext qc) throws QueryException { // cache existing functions final HashSet old = new HashSet<>(); - Collections.addAll(old, qc.functions.funcs()); + qc.functions.forEach(old::add); try { qc.parse(src.toString(), src.path()); @@ -51,7 +51,7 @@ public Value value(final QueryContext qc) throws QueryException { // collect new functions final ValueBuilder vb = new ValueBuilder(qc); - for(final StaticFunc sf : qc.functions.funcs()) { + for(final StaticFunc sf : qc.functions) { if(!old.contains(sf)) addItems(vb, sf, qc); } funcs = vb.value(this); @@ -84,7 +84,7 @@ private static void addItems(final ValueBuilder vb, final StaticFunc sf, final Q for(int a = sf.minArity(); a <= sf.arity(); ++a) { final FuncBuilder fb = new FuncBuilder(sf.info, a, true); // safe cast (no context dependency, runtime evaluation) - vb.add((FuncItem) Functions.item(sf, fb, qc, true)); + vb.add((FuncItem) Functions.item(sf, fb, qc)); } } } diff --git a/basex-core/src/main/java/org/basex/query/func/inspect/PlainDoc.java b/basex-core/src/main/java/org/basex/query/func/inspect/PlainDoc.java index be5f853051..f69ca26688 100644 --- a/basex-core/src/main/java/org/basex/query/func/inspect/PlainDoc.java +++ b/basex-core/src/main/java/org/basex/query/func/inspect/PlainDoc.java @@ -40,7 +40,7 @@ final class PlainDoc extends Inspect { FNode context() throws QueryException { final FBuilder root = element("context"); for(final StaticVar sv : qc.vars) root.add(variable(sv)); - for(final StaticFunc sf : qc.functions.funcs()) { + for(final StaticFunc sf : qc.functions) { if(!NSGlobal.reserved(sf.name.uri())) { root.add(function(sf.name, sf, sf.funcType(), sf.anns)); } diff --git a/basex-core/src/main/java/org/basex/query/func/unit/Unit.java b/basex-core/src/main/java/org/basex/query/func/unit/Unit.java index b1afe1b44c..511cd529de 100644 --- a/basex-core/src/main/java/org/basex/query/func/unit/Unit.java +++ b/basex-core/src/main/java/org/basex/query/func/unit/Unit.java @@ -83,7 +83,7 @@ public void test(final FBuilder suites) throws IOException { qc.parse(input, file.path()); // loop through all functions - for(final StaticFunc sf : qc.functions.funcs()) { + for(final StaticFunc sf : qc.functions) { // find Unit annotations final AnnList anns = sf.anns; boolean unit = false; diff --git a/basex-core/src/main/java/org/basex/query/util/parse/LocalVars.java b/basex-core/src/main/java/org/basex/query/util/parse/LocalVars.java index 26855447d5..d9026543b3 100644 --- a/basex-core/src/main/java/org/basex/query/util/parse/LocalVars.java +++ b/basex-core/src/main/java/org/basex/query/util/parse/LocalVars.java @@ -102,7 +102,7 @@ public ParseExpr resolve(final QNm name, final InputInfo info) throws QueryExcep // - if a variable uses the module or an imported URI, or // - if it is specified in the main module final QNm module = parser.sc.module; - final boolean hasImport = parser.moduleURIs.contains(uri); + final boolean hasImport = parser.sc.imports.contains(uri); if(module == null || eq(module.uri(), uri) || hasImport) return parser.qc.vars.newRef(name, info, hasImport); diff --git a/basex-core/src/main/java/org/basex/query/value/item/QNm.java b/basex-core/src/main/java/org/basex/query/value/item/QNm.java index 4454358b31..1237e6d8fa 100644 --- a/basex-core/src/main/java/org/basex/query/value/item/QNm.java +++ b/basex-core/src/main/java/org/basex/query/value/item/QNm.java @@ -146,6 +146,15 @@ public byte[] uri() { return uri == null ? Token.EMPTY : uri; } + /** + * Returns the URI of the given QName, or an empty string if {@code null}. + * @param qnm name (can be {@code null}) + * @return URI + */ + public static byte[] uri(final QNm qnm) { + return qnm == null ? Token.EMPTY : qnm.uri(); + } + /** * Checks if the URI of this QName has been explicitly set. * @return result of check diff --git a/basex-core/src/test/java/org/basex/query/ModuleTest.java b/basex-core/src/test/java/org/basex/query/ModuleTest.java index fd03e5bab9..2d8fa1058f 100644 --- a/basex-core/src/test/java/org/basex/query/ModuleTest.java +++ b/basex-core/src/test/java/org/basex/query/ModuleTest.java @@ -145,6 +145,59 @@ public final class ModuleTest extends SandboxTest { + "', '" + o.path() + "') })", QueryError.MODULE_FOUND_OTHER_X_X); } + /** Tests function visibility. */ + @Test public void functionVisibility() { + final IOFile sandbox = sandbox(); + final IOFile p = new IOFile(sandbox, "p.xqm"); + write(p, "module namespace p = 'p';\n" + + "declare %private function p:x() {'module'};\n" + + "declare function p:f() {p:x()};"); + final IOFile q1 = new IOFile(sandbox, "q1.xqm"); + write(q1, "module namespace q = 'q';\n" + + "declare function q:f() {x()};"); + final IOFile q2 = new IOFile(sandbox, "q2.xqm"); + write(q2, "module namespace q = 'q';\n" + + "declare %private function x() {42};"); + final IOFile r = new IOFile(sandbox, "r.xqm"); + write(r, "module namespace r = 'r';\n" + + "declare function r:f() {\n" + + " fn:divided-decimals-record(1, 2),\n" + + " fn:divided-decimals-record#2(3, 4),\n" + + " 5 => fn:divided-decimals-record(6),\n" + + " function-lookup(#fn:divided-decimals-record, 2)(7, 8)\n" + + "};"); + final IOFile s = new IOFile(sandbox, "s.xqm"); + write(s, "module namespace s = 's';\n" + + "import module 's' at '" + s.path() + "';\n" + + "declare function s:f() {s:x()};"); + + // private function does not clash with the same name in another module + query("import module namespace p = 'p' at '" + p.path() + "';\n" + + "declare function p:x() {'main'};\n" + + "p:f(), p:x()", "module\nmain"); + + // private function is visible throughout module, even in different file + query("import module namespace q = 'q' at '" + q1.path() + "', '" + q2.path() + "';\n" + + "q:f()", 42); + + // built-in record constructor visible in library module + query("import module namespace r = 'r' at '" + r.path() + "';\n" + + "r:f()", + "{\"quotient\":1,\"remainder\":2}\n" + + "{\"quotient\":3,\"remainder\":4}\n" + + "{\"quotient\":5,\"remainder\":6}\n" + + "{\"quotient\":7,\"remainder\":8}"); + + // private function reported as such + error("import module namespace p = 'p' at '" + p.path() + "';\n" + + "p:x()", QueryError.FUNCPRIVATE_X); + + // function in main module is not visible in imported module + error("import module namespace s = 's' at '" + s.path() + "';\n" + + "declare function s:x() {42};\n" + + "s:f()", QueryError.WHICHFUNC_X); + } + /** Tests rejection of functions and variables, when their modules are not explicitly imported. */ @Test public void gh2048() { final IOFile sandbox = sandbox(); @@ -157,13 +210,11 @@ public final class ModuleTest extends SandboxTest { + "};\n" + "declare variable $c:hello := 'can you see me now';"); - // function is still visible to fn:function-lookup + // function is not visible to fn:function-lookup (not in dynamically known function definitions) query("import module namespace b = 'b' at '" + b.path() + "';\n" - + "declare namespace c = 'c';\n" - + "fn:function-lookup(xs:QName('c:hello'), 0)()", "can you see me now"); + + "fn:function-lookup(#Q{c}hello, 0)", ""); // function is still visible to inspect:functions query("import module namespace b = 'b' at '" + b.path() + "';\n" - + "declare namespace c = 'c';\n" + "inspect:functions()", "Q{c}hello#0"); error("import module namespace b = 'b' at '" + b.path() + "';\n"