diff --git a/basex-core/src/main/java/org/basex/query/func/Closure.java b/basex-core/src/main/java/org/basex/query/func/Closure.java index 74b307a254..337a0b868c 100644 --- a/basex-core/src/main/java/org/basex/query/func/Closure.java +++ b/basex-core/src/main/java/org/basex/query/func/Closure.java @@ -95,7 +95,7 @@ public Closure(final InputInfo info, final Expr expr, final Params params, final this.params = params; this.anns = anns; this.vs = vs; - this.global = global == null ? new HashMap<>() : new HashMap<>(global); + this.global = global == null ? new HashMap<>() : new LinkedHashMap<>(global); props = new FlagCache(flag -> { for(final Expr ex : this.global.values()) { if(ex.has(flag)) return true; @@ -197,7 +197,7 @@ public Expr optimize(final CompileContext cc) { cc.info(OPTINLINE_X, entry); for(final Entry expr2 : cl.global.entrySet()) { final Var var2 = cc.copy(expr2.getKey(), null); - if(add == null) add = new HashMap<>(); + if(add == null) add = new LinkedHashMap<>(); add.put(var2, expr2.getValue()); expr2.setValue(new VarRef(cl.info, var2).optimize(cc)); } @@ -335,7 +335,7 @@ public Expr inline(final InlineContext ic) throws QueryException { public Expr copy(final CompileContext cc, final IntObjectMap vm) { final VarScope scope = new VarScope(); - final HashMap outer = new HashMap<>(); + final HashMap outer = new LinkedHashMap<>(); global.forEach((key, value) -> outer.put(key, value.copy(cc, vm))); cc.pushScope(scope); @@ -343,7 +343,7 @@ public Expr copy(final CompileContext cc, final IntObjectMap vm) { final IntObjectMap innerVars = new IntObjectMap<>(); vs.copy(cc, innerVars); - final HashMap bindings = new HashMap<>(); + final HashMap bindings = new LinkedHashMap<>(); outer.forEach((key, value) -> bindings.put(innerVars.get(key.id), value)); final Var[] prms = params.clone(); diff --git a/basex-core/src/main/java/org/basex/query/func/PartFunc.java b/basex-core/src/main/java/org/basex/query/func/PartFunc.java index 4f5a924bda..6eeeee2e26 100644 --- a/basex-core/src/main/java/org/basex/query/func/PartFunc.java +++ b/basex-core/src/main/java/org/basex/query/func/PartFunc.java @@ -112,7 +112,7 @@ private Expr closure(final Expr func, final FuncType ft, final int nargs, cc.pushScope(vs); try { // the function and the supplied arguments are evaluated once, when the closure is created - final HashMap global = new HashMap<>(); + final HashMap global = new LinkedHashMap<>(); final Var fn = vs.addNew(new QNm(FUNCTION), func.seqType(), cc.qc, info); global.put(fn, func); 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 2bbcb29e5d..beb8f6dca7 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 @@ -119,7 +119,7 @@ private QNm[] inScopeNames() { * @return global map or {@code null} */ public HashMap pushContext(final boolean global) { - final HashMap map = global ? new HashMap<>() : null; + final HashMap map = global ? new LinkedHashMap<>() : null; vars.add(new VarContext(map)); return map; } diff --git a/basex-core/src/test/java/org/basex/query/ast/FuncItemTest.java b/basex-core/src/test/java/org/basex/query/ast/FuncItemTest.java index aac67c2663..0f5ca4e9d7 100644 --- a/basex-core/src/test/java/org/basex/query/ast/FuncItemTest.java +++ b/basex-core/src/test/java/org/basex/query/ast/FuncItemTest.java @@ -2,8 +2,10 @@ import static org.basex.query.QueryError.*; import static org.basex.query.func.Function.*; +import static org.junit.jupiter.api.Assertions.*; import org.basex.*; +import org.basex.query.*; import org.basex.query.expr.*; import org.basex.query.func.*; import org.basex.query.value.item.*; @@ -745,4 +747,37 @@ declare record local:rec(i as xs:integer, inc as fn(local:rec) as local:rec := f query(java + "let $f as fn() as xs:string* := $j return count($f())", 0); error(java + "let $f as fn() as xs:integer := $j return $f()", INVTYPE_X); } + + /** + * Plans of closures with several captured variables are identical across compilations. + * @throws Exception exception + */ + @Test public void closurePlanOrder() throws Exception { + final String vars = "let $a := , $b := , $c := , $d := "; + final String[] queries = { + // parsed closure + vars + "return fn() { ($a, $b, $c, $d) }", + "let $n := , $m := , $o := return " + + "deep-equal(fn($x) { fn() { $x, $n, $o } }, fn($y) { fn() { $y, $m, $o } })", + // nested closure is inlined into the surrounding closure + vars + "let $f := fn() { ($a, $b, $c, $d) } return fn() { $f() }", + // closure is copied when the function body is inlined + "declare %basex:inline(1000) function local:f($x) { " + + "let $a := , $b := , $c := return fn() { ($a, $b, $c, $x) } }; " + + "local:f(), local:f()", + // partial function application is rewritten to a closure + "replace(?, random:uuid(), random:uuid(), random:uuid())" + }; + for(final String query : queries) { + String first = null; + for(int r = 0; r < 100; r++) { + try(QueryProcessor qp = new QueryProcessor(query, context)) { + qp.optimize(); + final String plan = qp.toXml().serialize().toString(); + if(first == null) first = plan; + else assertEquals(first, plan, "Run " + r + ": " + query); + } + } + } + } }