From 40f5fb42b510284fd613f784dae7fd222203e8bf Mon Sep 17 00:00:00 2001 From: Gunther Rademacher Date: Wed, 8 Jul 2026 18:42:13 +0200 Subject: [PATCH 1/2] separate declared and refined function return type --- .../java/org/basex/query/func/Closure.java | 8 +- .../org/basex/query/func/DynFuncCall.java | 2 +- .../java/org/basex/query/func/PartFunc.java | 4 +- .../java/org/basex/query/func/StaticFunc.java | 4 +- .../basex/query/func/array/ArrayBuild.java | 2 +- .../basex/query/func/array/ArrayForEach.java | 2 +- .../query/func/array/ArrayForEachPair.java | 2 +- .../java/org/basex/query/func/fn/FnApply.java | 2 +- .../org/basex/query/func/fn/FnFoldLeft.java | 4 +- .../basex/query/func/fn/FnForEachPair.java | 4 +- .../org/basex/query/func/fn/FnGenerate.java | 2 +- .../basex/query/func/fn/FnPartialApply.java | 6 +- .../org/basex/query/func/fn/FnWhileDo.java | 4 +- .../basex/query/func/hof/HofFoldLeft1.java | 2 +- .../org/basex/query/func/hof/HofScanLeft.java | 2 +- .../org/basex/query/func/map/MapBuild.java | 4 +- .../org/basex/query/func/map/MapForEach.java | 4 +- .../org/basex/query/func/map/MapMerge.java | 2 +- .../query/func/xquery/XQueryForEach.java | 2 +- .../query/func/xquery/XQueryForkAny.java | 2 +- .../query/func/xquery/XQueryForkJoin.java | 2 +- .../basex/query/func/xquery/XQueryReduce.java | 4 +- .../org/basex/query/value/item/FItem.java | 7 +- .../org/basex/query/value/type/FuncType.java | 89 ++++++++++++++----- .../org/basex/query/value/type/SeqType.java | 7 +- .../org/basex/query/value/type/TypeRef.java | 11 +++ .../main/java/org/basex/query/var/Var.java | 6 +- .../java/org/basex/query/SerializerTest.java | 8 +- .../java/org/basex/query/expr/RecordTest.java | 16 ++++ .../basex/query/func/InspectModuleTest.java | 6 +- .../java/org/basex/query/simple/TypeTest.java | 30 +++++++ 31 files changed, 182 insertions(+), 68 deletions(-) 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 da1f825ced..637ac76d82 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 @@ -206,9 +206,11 @@ public Expr optimize(final CompileContext cc) { cc.removeScope(this); } - final SeqType st = expr.seqType(); - final SeqType dt = declType == null || st.instanceOf(declType) ? st : declType; - exprType.assign(FuncType.get(anns, dt, params)); + // the declared return type is observed by instance-of and function coercion; it is the type + // declared in the signature, or item()* if none was given (so 'fn() { "x" } instance of + // fn() as xs:string' yields false). The body's inferred type is kept as the refined return + // type, so that direct calls can still profit from the more specific result type. + exprType.assign(FuncType.get(anns, declType, params).withRefinedType(expr.seqType())); // only evaluate if: // - the closure is empty, so we don't lose variables diff --git a/basex-core/src/main/java/org/basex/query/func/DynFuncCall.java b/basex-core/src/main/java/org/basex/query/func/DynFuncCall.java index e1889ab55e..db4237c2ee 100644 --- a/basex-core/src/main/java/org/basex/query/func/DynFuncCall.java +++ b/basex-core/src/main/java/org/basex/query/func/DynFuncCall.java @@ -107,7 +107,7 @@ public Expr optimize(final CompileContext cc) throws QueryException { if(!updating && ft.anns.contains(Annotation.UPDATING)) { throw FUNCUP_X.get(info, func); } - exprType.assign(ft.declType); + exprType.assign(ft.refinedType); } if(func instanceof XQStruct) { 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 c8e96b2d50..7bb72d592c 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 @@ -67,7 +67,7 @@ public Expr optimize(final CompileContext cc) throws QueryException { args[placeholderPerm == null ? a : placeholderPerm[a]] = ft.argTypes[e]; ++a; } - exprType.assign(FuncType.get(ft.declType, args).seqType()); + exprType.assign(FuncType.get(ft.declType, args).withRefinedType(ft.refinedType).seqType()); } } return this; @@ -114,7 +114,7 @@ private FuncItem funcItem(final FItem func, final QueryContext qc) throws QueryE final boolean updating = anns.contains(Annotation.UPDATING); final DynFuncCall expr = new DynFuncCall(info, updating, false, func, args); - final FuncType type = FuncType.get(anns, ft.declType, params); + final FuncType type = FuncType.get(anns, ft.declType, params).withRefinedType(ft.refinedType); return new FuncItem(info, expr, params, anns, type, vs.stackSize(), null, qc.focus.copy()); } diff --git a/basex-core/src/main/java/org/basex/query/func/StaticFunc.java b/basex-core/src/main/java/org/basex/query/func/StaticFunc.java index 82db9d6430..daf93f6eaa 100644 --- a/basex-core/src/main/java/org/basex/query/func/StaticFunc.java +++ b/basex-core/src/main/java/org/basex/query/func/StaticFunc.java @@ -150,7 +150,9 @@ public QNm paramName(final int pos) { @Override public FuncType funcType() { - return FuncType.get(anns, declType, params); + // align the refined return type with seqType() (the declared type, or the body type once the + // declared type has been checked and cleared), so this type stays consistent for result typing + return FuncType.get(anns, declType, params).withRefinedType(seqType()); } @Override diff --git a/basex-core/src/main/java/org/basex/query/func/array/ArrayBuild.java b/basex-core/src/main/java/org/basex/query/func/array/ArrayBuild.java index 4daf68815a..f615305af4 100644 --- a/basex-core/src/main/java/org/basex/query/func/array/ArrayBuild.java +++ b/basex-core/src/main/java/org/basex/query/func/array/ArrayBuild.java @@ -43,7 +43,7 @@ protected Expr opt(final CompileContext cc) throws QueryException { arg(1, arg -> refineFunc(arg, cc, st.with(Occ.EXACTLY_ONE), Types.INTEGER_O)); final FuncType ft = arg(1).funcType(); - if(ft != null) exprType.assign(ArrayType.get(ft.declType)); + if(ft != null) exprType.assign(ArrayType.get(ft.refinedType)); return this; } diff --git a/basex-core/src/main/java/org/basex/query/func/array/ArrayForEach.java b/basex-core/src/main/java/org/basex/query/func/array/ArrayForEach.java index 445cbab156..80a0334642 100644 --- a/basex-core/src/main/java/org/basex/query/func/array/ArrayForEach.java +++ b/basex-core/src/main/java/org/basex/query/func/array/ArrayForEach.java @@ -41,7 +41,7 @@ protected Expr opt(final CompileContext cc) throws QueryException { // assign type after coercion (expression might have changed) final FuncType ft = arg(1).funcType(); - if(ft != null) exprType.assign(ArrayType.get(ft.declType)); + if(ft != null) exprType.assign(ArrayType.get(ft.refinedType)); return this; } diff --git a/basex-core/src/main/java/org/basex/query/func/array/ArrayForEachPair.java b/basex-core/src/main/java/org/basex/query/func/array/ArrayForEachPair.java index a7eec8dc83..0a36b73eff 100644 --- a/basex-core/src/main/java/org/basex/query/func/array/ArrayForEachPair.java +++ b/basex-core/src/main/java/org/basex/query/func/array/ArrayForEachPair.java @@ -47,7 +47,7 @@ protected Expr opt(final CompileContext cc) throws QueryException { // assign type after coercion (expression might have changed) final FuncType ft = arg(2).funcType(); - if(ft != null) exprType.assign(ArrayType.get(ft.declType)); + if(ft != null) exprType.assign(ArrayType.get(ft.refinedType)); return this; } diff --git a/basex-core/src/main/java/org/basex/query/func/fn/FnApply.java b/basex-core/src/main/java/org/basex/query/func/fn/FnApply.java index df05243977..ff6abd50c4 100644 --- a/basex-core/src/main/java/org/basex/query/func/fn/FnApply.java +++ b/basex-core/src/main/java/org/basex/query/func/fn/FnApply.java @@ -57,7 +57,7 @@ protected Expr opt(final CompileContext cc) throws QueryException { } ft = arg(0).funcType(); - if(ft != null) exprType.assign(ft.declType); + if(ft != null) exprType.assign(ft.refinedType); return this; } diff --git a/basex-core/src/main/java/org/basex/query/func/fn/FnFoldLeft.java b/basex-core/src/main/java/org/basex/query/func/fn/FnFoldLeft.java index 6c52e57af1..a9221a06e4 100644 --- a/basex-core/src/main/java/org/basex/query/func/fn/FnFoldLeft.java +++ b/basex-core/src/main/java/org/basex/query/func/fn/FnFoldLeft.java @@ -124,12 +124,12 @@ protected final Expr optType(final CompileContext cc, final boolean array, final final SeqType[] types = { left ? st : i1t, left ? i1t : st, Types.INTEGER_O }; arg(2, arg -> refineFunc(action, cc, types)); ost = st; - st = st.union(arg(2).funcType().declType); + st = st.union(arg(2).funcType().refinedType); } while(!st.eq(ost)); exprType.assign(st); } else { final FuncType ft = action.funcType(); - if(ft != null) exprType.assign(ist.oneOrMore() ? ft.declType : zst.union(ft.declType)); + if(ft != null) exprType.assign(ist.oneOrMore() ? ft.refinedType : zst.union(ft.refinedType)); } return this; } diff --git a/basex-core/src/main/java/org/basex/query/func/fn/FnForEachPair.java b/basex-core/src/main/java/org/basex/query/func/fn/FnForEachPair.java index 796d4b54e6..8a2dd88031 100644 --- a/basex-core/src/main/java/org/basex/query/func/fn/FnForEachPair.java +++ b/basex-core/src/main/java/org/basex/query/func/fn/FnForEachPair.java @@ -22,7 +22,7 @@ public final Iter iter(final QueryContext qc) throws QueryException { final FItem action = toFunction(arg(2), 3, this instanceof UpdateForEachPair, qc); return new Iter() { - final long size = action.funcType().declType.one() ? + final long size = action.funcType().refinedType.one() ? Math.min(input1.size(), input2.size()) : -1; final HofArgs args = new HofArgs(3, action); Iter iter = Empty.ITER; @@ -64,7 +64,7 @@ protected final Expr opt(final CompileContext cc) throws QueryException { // assign type after refinement final FuncType ft = arg(2).funcType(); if(ft != null) { - final SeqType declType = ft.declType; + final SeqType declType = ft.refinedType; final boolean oneOrMore = st1.oneOrMore() && st2.oneOrMore() && declType.oneOrMore(); final long size = declType.zero() ? 0 : declType.one() ? Math.min(input1.size(), input2.size()) : -1; diff --git a/basex-core/src/main/java/org/basex/query/func/fn/FnGenerate.java b/basex-core/src/main/java/org/basex/query/func/fn/FnGenerate.java index 576caece46..a0c2f5a098 100644 --- a/basex-core/src/main/java/org/basex/query/func/fn/FnGenerate.java +++ b/basex-core/src/main/java/org/basex/query/func/fn/FnGenerate.java @@ -45,7 +45,7 @@ protected Expr opt(final CompileContext cc) throws QueryException { final SeqType[] types = { st, Types.INTEGER_O }; arg(1, arg -> refineFunc(step, cc, types)); ost = st; - st = st.union(arg(1).funcType().declType); + st = st.union(arg(1).funcType().refinedType); } while(!st.eq(ost)); exprType.assign(st.with(Occ.ONE_OR_MORE)); } diff --git a/basex-core/src/main/java/org/basex/query/func/fn/FnPartialApply.java b/basex-core/src/main/java/org/basex/query/func/fn/FnPartialApply.java index 79fcd7fa64..6d34f0b22d 100644 --- a/basex-core/src/main/java/org/basex/query/func/fn/FnPartialApply.java +++ b/basex-core/src/main/java/org/basex/query/func/fn/FnPartialApply.java @@ -68,7 +68,7 @@ public Value value(final QueryContext qc) throws QueryException { funcCall = new DynFuncCall(info, new PartFunc(info, ExprList.concat(funcArgs, function), placeholders, null), args); } - return new FuncItem(info, funcCall, params, AnnList.EMPTY, FuncType.get(ft.declType, argTypes), - params.length, null); + return new FuncItem(info, funcCall, params, AnnList.EMPTY, + FuncType.get(ft.declType, argTypes).withRefinedType(ft.refinedType), params.length, null); } -} \ No newline at end of file +} diff --git a/basex-core/src/main/java/org/basex/query/func/fn/FnWhileDo.java b/basex-core/src/main/java/org/basex/query/func/fn/FnWhileDo.java index 7504c21882..63dea8574d 100644 --- a/basex-core/src/main/java/org/basex/query/func/fn/FnWhileDo.java +++ b/basex-core/src/main/java/org/basex/query/func/fn/FnWhileDo.java @@ -38,7 +38,7 @@ protected final Expr opt(final CompileContext cc) throws QueryException { final SeqType[] types = { st, Types.INTEGER_O }; arg(a, arg -> refineFunc(action, cc, types)); ost = st; - st = st.union(arg(a).funcType().declType); + st = st.union(arg(a).funcType().refinedType); } while(!st.eq(ost)); final SeqType[] types = { st, Types.INTEGER_O }; @@ -48,7 +48,7 @@ protected final Expr opt(final CompileContext cc) throws QueryException { exprType.assign(st); } else { final FuncType ft = action.funcType(); - if(ft != null) exprType.assign(st.union(ft.declType)); + if(ft != null) exprType.assign(st.union(ft.refinedType)); } return this; } diff --git a/basex-core/src/main/java/org/basex/query/func/hof/HofFoldLeft1.java b/basex-core/src/main/java/org/basex/query/func/hof/HofFoldLeft1.java index 3b2e69eb1c..d44a3b96cb 100644 --- a/basex-core/src/main/java/org/basex/query/func/hof/HofFoldLeft1.java +++ b/basex-core/src/main/java/org/basex/query/func/hof/HofFoldLeft1.java @@ -37,7 +37,7 @@ protected Expr opt(final CompileContext cc) { if(input.seqType().zero()) return input; final FuncType ft = action.funcType(); - if(ft != null) exprType.assign(ft.declType); + if(ft != null) exprType.assign(ft.refinedType); return this; } } diff --git a/basex-core/src/main/java/org/basex/query/func/hof/HofScanLeft.java b/basex-core/src/main/java/org/basex/query/func/hof/HofScanLeft.java index d3f326c5f9..db6c00397b 100644 --- a/basex-core/src/main/java/org/basex/query/func/hof/HofScanLeft.java +++ b/basex-core/src/main/java/org/basex/query/func/hof/HofScanLeft.java @@ -45,7 +45,7 @@ protected Expr opt(final CompileContext cc) throws QueryException { SeqType st = zero.seqType(); final FuncType ft = action.funcType(); - if(ft != null) st = st.union(ft.declType); + if(ft != null) st = st.union(ft.refinedType); exprType.assign(st.with(Occ.ZERO_OR_MORE)); return this; } diff --git a/basex-core/src/main/java/org/basex/query/func/map/MapBuild.java b/basex-core/src/main/java/org/basex/query/func/map/MapBuild.java index 8d114d5938..79fb9658a7 100644 --- a/basex-core/src/main/java/org/basex/query/func/map/MapBuild.java +++ b/basex-core/src/main/java/org/basex/query/func/map/MapBuild.java @@ -52,7 +52,7 @@ protected Expr opt(final CompileContext cc) throws QueryException { Type kt = fiKey || keys.size() == 0 ? s1t.type : BasicType.ITEM; if(fiKey) { arg(1, arg -> refineFunc(arg, cc, s1t)); - kt = arg(1).funcType().declType.type; + kt = arg(1).funcType().refinedType.type; } kt = kt.atomic(); @@ -60,7 +60,7 @@ protected Expr opt(final CompileContext cc) throws QueryException { SeqType vt = fiValue || value.size() == 0 ? s1t : Types.ITEM_ZM; if(fiValue) { arg(2, arg -> refineFunc(arg, cc, s1t)); - vt = arg(2).funcType().declType; + vt = arg(2).funcType().refinedType; } assignType(kt, vt); return this; diff --git a/basex-core/src/main/java/org/basex/query/func/map/MapForEach.java b/basex-core/src/main/java/org/basex/query/func/map/MapForEach.java index 9b135211f5..8ccd1464c7 100644 --- a/basex-core/src/main/java/org/basex/query/func/map/MapForEach.java +++ b/basex-core/src/main/java/org/basex/query/func/map/MapForEach.java @@ -23,7 +23,7 @@ public Iter iter(final QueryContext qc) throws QueryException { final FItem action = toFunction(arg(1), 3, this instanceof UpdateMapForEach, qc); return new Iter() { - final long size = action.funcType().declType.one() ? map.structSize() : -1; + final long size = action.funcType().refinedType.one() ? map.structSize() : -1; final BasicIter keys = map.keys().iter(); final HofArgs args = new HofArgs(3, action); Iter iter = Empty.ITER; @@ -65,7 +65,7 @@ protected final Expr opt(final CompileContext cc) throws QueryException { final FuncType ft = arg(1).funcType(); if(ft != null) { - final SeqType st = ft.declType; + final SeqType st = ft.refinedType; exprType.assign(st.type.seqType(Occ.ZERO_OR_MORE), st.one() ? map.structSize() : -1); } return this; diff --git a/basex-core/src/main/java/org/basex/query/func/map/MapMerge.java b/basex-core/src/main/java/org/basex/query/func/map/MapMerge.java index 0264abb447..4af5c03f31 100644 --- a/basex-core/src/main/java/org/basex/query/func/map/MapMerge.java +++ b/basex-core/src/main/java/org/basex/query/func/map/MapMerge.java @@ -245,7 +245,7 @@ Value get(final Item key, final Value old, final Value value, final QueryContext @Override SeqType type(final SeqType st) { - return st.union(function.funcType().declType); + return st.union(function.funcType().refinedType); } } } diff --git a/basex-core/src/main/java/org/basex/query/func/xquery/XQueryForEach.java b/basex-core/src/main/java/org/basex/query/func/xquery/XQueryForEach.java index 0c7ebe8155..cece5e3809 100644 --- a/basex-core/src/main/java/org/basex/query/func/xquery/XQueryForEach.java +++ b/basex-core/src/main/java/org/basex/query/func/xquery/XQueryForEach.java @@ -46,7 +46,7 @@ protected Expr opt(final CompileContext cc) throws QueryException { if(arg(2) == Empty.UNDEFINED) { final FuncType ft = action.funcType(); if(ft != null) { - final SeqType dt = ft.declType; + final SeqType dt = ft.refinedType; exprType.assign(dt.with(dt.occ.multiply(input.seqType().occ))); } } diff --git a/basex-core/src/main/java/org/basex/query/func/xquery/XQueryForkAny.java b/basex-core/src/main/java/org/basex/query/func/xquery/XQueryForkAny.java index f03369ff46..039197bf33 100644 --- a/basex-core/src/main/java/org/basex/query/func/xquery/XQueryForkAny.java +++ b/basex-core/src/main/java/org/basex/query/func/xquery/XQueryForkAny.java @@ -53,7 +53,7 @@ protected Expr opt(final CompileContext cc) throws QueryException { return new DynFuncCall(info, coerceFunc(0, cc)).optimize(cc); } final FuncType ft = functions.funcType(); - if(ft != null) exprType.assign(ft.declType); + if(ft != null) exprType.assign(ft.refinedType); return this; } } diff --git a/basex-core/src/main/java/org/basex/query/func/xquery/XQueryForkJoin.java b/basex-core/src/main/java/org/basex/query/func/xquery/XQueryForkJoin.java index 2df00c48e6..64c0854df8 100644 --- a/basex-core/src/main/java/org/basex/query/func/xquery/XQueryForkJoin.java +++ b/basex-core/src/main/java/org/basex/query/func/xquery/XQueryForkJoin.java @@ -54,7 +54,7 @@ protected Expr opt(final CompileContext cc) throws QueryException { if(results == Boolean.TRUE) { final FuncType ft = functions.funcType(); if(ft != null) { - final SeqType dt = ft.declType; + final SeqType dt = ft.refinedType; exprType.assign(dt.with(dt.occ.multiply(st.occ))); } } else if(results == Boolean.FALSE) { diff --git a/basex-core/src/main/java/org/basex/query/func/xquery/XQueryReduce.java b/basex-core/src/main/java/org/basex/query/func/xquery/XQueryReduce.java index 59e98f5814..d6c11e275a 100644 --- a/basex-core/src/main/java/org/basex/query/func/xquery/XQueryReduce.java +++ b/basex-core/src/main/java/org/basex/query/func/xquery/XQueryReduce.java @@ -35,8 +35,8 @@ protected Expr opt(final CompileContext cc) { // result type: union of the seed type and the declared action/combine types SeqType st = init.seqType(); final FuncType at = arg(2).funcType(), ct = arg(3).funcType(); - if(at != null) st = st.union(at.declType); - if(ct != null) st = st.union(ct.declType); + if(at != null) st = st.union(at.refinedType); + if(ct != null) st = st.union(ct.refinedType); exprType.assign(st); return this; } diff --git a/basex-core/src/main/java/org/basex/query/value/item/FItem.java b/basex-core/src/main/java/org/basex/query/value/item/FItem.java index 0cc3597be6..ac19a1d1dc 100644 --- a/basex-core/src/main/java/org/basex/query/value/item/FItem.java +++ b/basex-core/src/main/java/org/basex/query/value/item/FItem.java @@ -107,15 +107,14 @@ public FItem coerceTo(final FuncType ft, final QueryContext qc, final CompileCon // add type check if return types differ final SeqType dt = ft.declType; - FuncType tp = funcType(); if(!body.seqType().instanceOf(dt)) { body = new TypeCheck(info, body, dt); if(cc != null) body = body.optimize(cc); } - // adopt type of optimized body if it is more specific than passed on type - final SeqType bt = body.seqType(); - tp = cc != null && !bt.eq(dt) && bt.instanceOf(dt) ? FuncType.get(bt, argTypes) : ft; + // the coerced item advertises the target type (observed by instance-of); the body's inferred + // type is kept as the refined return type so result typing still profits from it + final FuncType tp = cc != null ? ft.withRefinedType(body.seqType()) : ft; body.markTailCalls(null); return new FuncItem(info, body, vars, annotations(), tp, vs.stackSize(), funcName()); } finally { diff --git a/basex-core/src/main/java/org/basex/query/value/type/FuncType.java b/basex-core/src/main/java/org/basex/query/value/type/FuncType.java index a4c1f45711..9efae5db08 100644 --- a/basex-core/src/main/java/org/basex/query/value/type/FuncType.java +++ b/basex-core/src/main/java/org/basex/query/value/type/FuncType.java @@ -22,8 +22,14 @@ public final class FuncType extends FType { /** Annotations. */ public final AnnList anns; - /** Return type of the function. */ + /** Declared return type (observed by instance-of, coercion and subtyping). */ public final SeqType declType; + /** + * Refined return type, a subtype of {@link #declType} inferred from the function body. Used for + * result typing (e.g. dynamic calls, folds); never observed by instance-of, so that a function + * whose body is more specific than its declared return type still matches only its declaration. + */ + public final SeqType refinedType; /** Argument types (can be {@code null}, indicates that no types were specified). */ public final SeqType[] argTypes; @@ -33,21 +39,36 @@ public final class FuncType extends FType { * @param argTypes argument types (can be {@code null}) */ FuncType(final SeqType declType, final SeqType... argTypes) { - this(AnnList.EMPTY, declType, argTypes); + this(AnnList.EMPTY, declType, null, argTypes); } /** * Constructor. * @param anns annotations * @param declType declared return type (can be {@code null}) + * @param refinedType refined return type ({@code null}: same as declared type) * @param argTypes argument types (can be {@code null}) */ - private FuncType(final AnnList anns, final SeqType declType, final SeqType... argTypes) { + private FuncType(final AnnList anns, final SeqType declType, final SeqType refinedType, + final SeqType[] argTypes) { this.anns = anns; this.declType = declType == null ? ITEM_ZM : declType; + // the refined type must be a subtype of the declared type; a body type that is not (e.g. the + // integer body of 'fn() as xs:double { 1 }', whose result is coerced) contributes no refinement + this.refinedType = refinedType == null || !refinedType.instanceOf(this.declType) ? this.declType + : refinedType; this.argTypes = argTypes; } + /** + * Returns a copy of this function type with a refined (more specific) return type. + * @param rt refined return type + * @return function type + */ + public FuncType withRefinedType(final SeqType rt) { + return rt.eq(refinedType) ? this : new FuncType(anns, declType, rt, argTypes); + } + /** * Getter for function types. * @param anns annotations @@ -56,7 +77,7 @@ private FuncType(final AnnList anns, final SeqType declType, final SeqType... ar * @return function type */ public static FuncType get(final AnnList anns, final SeqType declType, final SeqType... args) { - return new FuncType(anns, declType, args); + return new FuncType(anns, declType, null, args); } /** @@ -82,7 +103,7 @@ public static FuncType get(final AnnList anns, final SeqType declType, final Var for(int p = 0; p < pl; p++) { argTypes[p] = params[p] == null ? ITEM_ZM : params[p].declaredType(); } - return new FuncType(anns, declType, argTypes); + return new FuncType(anns, declType, null, argTypes); } /** @@ -150,29 +171,54 @@ public boolean instanceOf(final Type type) { @Override public Type union(final Type type) { + if(this == type) return this; if(type instanceof ChoiceItemType) return type.union(this); - if(type.instanceOf(this)) return this; - if(instanceOf(type)) return type; - final FuncType ft = type.funcType(); - if(ft == null) return BasicType.ITEM; - final int arity = argTypes.length, nargs = ft.argTypes.length; - if(arity != nargs) return FUNCTION; - - final SeqType[] arg = new SeqType[arity]; - for(int a = 0; a < arity; a++) { - arg[a] = argTypes[a].intersect(ft.argTypes[a]); - if(arg[a] == null) return FUNCTION; + // two function types (a map or array is viewed through its function type) with matching arity: + // union argument, declared and refined return types structurally. This must happen before the + // instance-of short-circuits below, as those ignore the refined type: returning a single + // operand would drop the other's refined return type, unsoundly narrowing the result type of a + // call on the union. + if(this != FUNCTION && ft != null && ft != FUNCTION && argTypes != null && + ft.argTypes != null && argTypes.length == ft.argTypes.length) { + final int arity = argTypes.length; + final SeqType[] arg = new SeqType[arity]; + for(int a = 0; a < arity; a++) { + arg[a] = argTypes[a].intersect(ft.argTypes[a]); + if(arg[a] == null) return FUNCTION; + } + return new FuncType(anns.union(ft.anns), declType.union(ft.declType), + refinedType.union(ft.refinedType), arg); } - return get(anns.union(ft.anns), declType.union(ft.declType), arg); + + if(type.instanceOf(this)) return this; + if(instanceOf(type)) return type; + return ft == null ? BasicType.ITEM : FUNCTION; } @Override public Type intersect(final Type type) { if(type instanceof ChoiceItemType) return type.intersect(this); - if(instanceOf(type)) return this; - if(type.instanceOf(this)) return type; + + // if one function type is a subtype of the other, the intersection is that operand. Its refined + // return type is still met with the other's (the instance-of check ignores the refined type), + // yielding the most specific result type for calls on the intersection; if the refined types + // are disjoint, the operand is kept unchanged (no less precise than before). + if(instanceOf(type)) { + if(type instanceof final FuncType ft) { + final SeqType rf = refinedType.intersect(ft.refinedType); + return rf == null ? this : withRefinedType(rf); + } + return this; + } + if(type.instanceOf(this)) { + if(type instanceof final FuncType ft) { + final SeqType rf = refinedType.intersect(ft.refinedType); + return rf == null ? type : ft.withRefinedType(rf); + } + return type; + } if(type instanceof MapType || type instanceof ArrayType) return type.intersect(this); @@ -186,10 +232,11 @@ public Type intersect(final Type type) { if(an == null) return null; final SeqType dt = declType.intersect(ft.declType); if(dt == null) return null; + final SeqType rf = refinedType.intersect(ft.refinedType); final SeqType[] arg = new SeqType[arity]; for(int a = 0; a < arity; a++) arg[a] = argTypes[a].union(ft.argTypes[a]); - return get(an, dt, arg); + return new FuncType(an, dt, rf, arg); } /** @@ -198,7 +245,7 @@ public Type intersect(final Type type) { * @return function type */ public FuncType with(final int arity) { - return get(anns, declType, Arrays.copyOf(argTypes, arity)); + return new FuncType(anns, declType, refinedType, Arrays.copyOf(argTypes, arity)); } @Override diff --git a/basex-core/src/main/java/org/basex/query/value/type/SeqType.java b/basex-core/src/main/java/org/basex/query/value/type/SeqType.java index 2a03328c6f..39af972cd5 100644 --- a/basex-core/src/main/java/org/basex/query/value/type/SeqType.java +++ b/basex-core/src/main/java/org/basex/query/value/type/SeqType.java @@ -506,7 +506,12 @@ public boolean instanceOf(final SeqType st) { * @return result of check */ public boolean eq(final SeqType st) { - return this == st || TypeRef.deref(type).eq(TypeRef.deref(st.type)) && occ == st.occ; + if(this == st) return true; + if(occ != st.occ) return false; + // an unresolved forward reference is a distinct placeholder: do not compare it via its + // temporary item() dereference (else e.g. a recursive record type would look like item()) + if(TypeRef.unresolved(type) || TypeRef.unresolved(st.type)) return type == st.type; + return TypeRef.deref(type).eq(TypeRef.deref(st.type)); } @Override diff --git a/basex-core/src/main/java/org/basex/query/value/type/TypeRef.java b/basex-core/src/main/java/org/basex/query/value/type/TypeRef.java index 39fdd9f7dd..ea76b80784 100644 --- a/basex-core/src/main/java/org/basex/query/value/type/TypeRef.java +++ b/basex-core/src/main/java/org/basex/query/value/type/TypeRef.java @@ -61,6 +61,17 @@ public boolean resolved() { return info == null; } + /** + * Checks if the specified type is a still-unresolved forward reference. Such a reference is a + * distinct placeholder that dereferences to {@code item()}, so it must not be treated as equal to + * (or an instance of) the types it happens to dereference to before it has been resolved. + * @param type type + * @return result of check + */ + public static boolean unresolved(final Type type) { + return type instanceof final TypeRef ref && !ref.resolved(); + } + /** * Returns the input info of an unresolved reference. * @return input info, or {@code null} if resolved diff --git a/basex-core/src/main/java/org/basex/query/var/Var.java b/basex-core/src/main/java/org/basex/query/var/Var.java index fc72fd5d12..727c45715c 100644 --- a/basex-core/src/main/java/org/basex/query/var/Var.java +++ b/basex-core/src/main/java/org/basex/query/var/Var.java @@ -81,8 +81,10 @@ public Var(final Var var, final QueryContext qc) { */ public SeqType seqType() { final SeqType st = exprType.seqType(), dt = declType; - final SeqType it = dt != null ? dt.intersect(st) : null; - return it != null ? it : dt != null ? dt : st; + if(dt == null) return st; + // refine only if the bound type is a subtype of the declared type; if the value was coerced, + // the declared type is authoritative + return st.instanceOf(dt) ? st : dt; } /** diff --git a/basex-core/src/test/java/org/basex/query/SerializerTest.java b/basex-core/src/test/java/org/basex/query/SerializerTest.java index 9c2aa38924..989d4915b4 100644 --- a/basex-core/src/test/java/org/basex/query/SerializerTest.java +++ b/basex-core/src/test/java/org/basex/query/SerializerTest.java @@ -273,7 +273,7 @@ public final class SerializerTest extends SandboxTest { query(option + "xs:dayTimeDuration('P1D')", "xs:duration(\"P1D\")"); query(option + "B", "B"); query(option + "true#0", "fn:true#0"); - query(option + "fn() {}", "fn() as empty-sequence() { () }"); + query(option + "fn() {}", "fn() as item()* { () }"); query(option + "xs:float(1)", "xs:float(\"1\")"); query(option + "xs:float('-0')", "xs:float(\"-0\")"); @@ -294,7 +294,7 @@ public final class SerializerTest extends SandboxTest { query(option + "[ xs:dayTimeDuration('P1D') ]", "[xs:duration(\"P1D\")]"); query(option + "[ B ]", "[B]"); query(option + "[ true#0 ]", "[fn:true#0]"); - query(option + "[ fn() {} ]", "[fn() as empty-sequence() { () }]"); + query(option + "[ fn() {} ]", "[fn() as item()* { () }]"); query(option + "[ xs:float(1) ]", "[xs:float(\"1\")]"); query(option + "[ xs:float('-0')]", "[xs:float(\"-0\")]"); @@ -320,7 +320,7 @@ public final class SerializerTest extends SandboxTest { query("xs:dayTimeDuration('P1D')", "P1D"); query("B", "B"); query("true#0", "fn:true#0"); - query("fn() {}", "fn() as empty-sequence() { () }"); + query("fn() {}", "fn() as item()* { () }"); query("xs:float(1)", 1); query("xs:float('-0')", "-0"); @@ -340,7 +340,7 @@ public final class SerializerTest extends SandboxTest { query("[ xs:dayTimeDuration('P1D') ]", "[\"P1D\"]"); query("[ B ]", "[B]"); query("[ true#0 ]", "[fn:true#0]"); - query("[ fn() {} ]", "[fn() as empty-sequence() { () }]"); + query("[ fn() {} ]", "[fn() as item()* { () }]"); query("[ xs:float(1) ]", "[\"1\"]"); // should be revised for 'adaptive' method query("{ 1: (), 2: 3, 4: (5, 6) }", "{1:(),2:3,4:(5,6)}"); diff --git a/basex-core/src/test/java/org/basex/query/expr/RecordTest.java b/basex-core/src/test/java/org/basex/query/expr/RecordTest.java index 4b50283894..a32fec9ea1 100644 --- a/basex-core/src/test/java/org/basex/query/expr/RecordTest.java +++ b/basex-core/src/test/java/org/basex/query/expr/RecordTest.java @@ -55,6 +55,22 @@ public final class RecordTest extends SandboxTest { query("{ 'columns': ('a', 'b'), 'column-index': { 'a': 1 }, 'rows': [ 'p' ],\n" + " 'get': fn($r as xs:positiveInteger, $c as (xs:positiveInteger | xs:string))" + " as xs:string { 'x' } } instance of fn:parsed-csv-structure-record", true); + query("{ 'columns': ('a', 'b', 'c'), 'column-index': { 'a': 1, 'b': 2, 'c': 3 },\n" + + " 'rows': ([ 'p', 'q', 'r' ], [ 's', 't', 'u' ]),\n" + + " 'get': fn($row as xs:positiveInteger, $col as (xs:positiveInteger | xs:string))" + + " as xs:anyAtomicType? { 'banana' } } instance of fn:parsed-csv-structure-record", false); + query("{ 'number': 0.937e0, 'next': fn() { fn:random-number-generator() },\n" + + " 'permute': fn($in) { reverse($in) } }" + + " instance of fn:random-number-generator-record", false); + query("{ 'name': xs:QName('platonic'), 'is-simple': true(),\n" + + " 'base-type': fn() as fn:schema-type-record { atomic-type-annotation(3) },\n" + + " 'primitive-type': fn() as fn:schema-type-record { atomic-type-annotation(3) },\n" + + " 'variety': 'atomic',\n" + + " 'members': fn() as fn:schema-type-record* { () },\n" + + " 'simple-content-type': fn() as fn:schema-type-record { atomic-type-annotation(3) },\n" + + " 'matches': fn($x as xs:anyAtomicType) as xs:boolean { true() },\n" + + " 'constructor': xs:integer#1 } instance of fn:schema-type-record", true); + error("fn() as fn:schema-type-record* { 1 }()", INVTYPE_X); query("let $map := (\n" + " {'x':5, 'y':6}\n" + " => map:put(xs:NCName('x'), true())\n" diff --git a/basex-core/src/test/java/org/basex/query/func/InspectModuleTest.java b/basex-core/src/test/java/org/basex/query/func/InspectModuleTest.java index 175f873a18..8873749c8e 100644 --- a/basex-core/src/test/java/org/basex/query/func/InspectModuleTest.java +++ b/basex-core/src/test/java/org/basex/query/func/InspectModuleTest.java @@ -80,7 +80,7 @@ public final class InspectModuleTest extends SandboxTest { query(query + "/argument/@type/data()", "xs:int"); query(query + "/annotation/@name/data()", "private"); query(query + "/annotation/@uri/data()", "http://www.w3.org/2012/xquery"); - query(query + "/return/@type/data()", "xs:int"); + query(query + "/return/@type/data()", "xs:integer"); query(query + "/return/@occurrence/data()", ""); // unknown annotation @@ -208,8 +208,8 @@ public final class InspectModuleTest extends SandboxTest { query(func.args(" { 'a': 'b' }"), "record(a)"); query(func.args(" array { 1, }"), "array(item())"); query(func.args(" array { 1, 2 }"), "array(xs:integer)"); - query(func.args(" function() { 1 }"), "fn() as xs:integer"); - query(func.args(" fn() { 1 }"), "fn() as xs:integer"); + query(func.args(" function() { 1 }"), "fn() as item()*"); + query(func.args(" fn() { 1 }"), "fn() as item()*"); query(func.args(" 1 to 2", " { 'item': true() }"), "xs:integer"); query(func.args(" (, )[name() = 'a']", " { 'mode': 'expression' }"), diff --git a/basex-core/src/test/java/org/basex/query/simple/TypeTest.java b/basex-core/src/test/java/org/basex/query/simple/TypeTest.java index b69bb23e16..d5657430d3 100644 --- a/basex-core/src/test/java/org/basex/query/simple/TypeTest.java +++ b/basex-core/src/test/java/org/basex/query/simple/TypeTest.java @@ -79,6 +79,36 @@ public final class TypeTest extends QueryTest { "fn($x as xs:int) as xs:int {'x'} instance of fn(xs:int) as (xs:error | xs:int)"}, { "Subtyping 8", booleans(true), "fn($x as xs:int) as (xs:error | xs:int) {'x'} instance of fn(xs:int) as xs:int"}, + { "Subtyping 9", booleans(false), + "fn() as xs:string? { 'x' } instance of fn() as xs:string"}, + { "Subtyping 10", booleans(false), + "fn() as xs:string* { 'x' } instance of fn() as xs:string"}, + { "Subtyping 11", booleans(false), + "fn() as xs:anyAtomicType? { 'x' } instance of fn() as xs:string"}, + { "Subtyping 12", booleans(true), + "fn() as xs:string { 'x' } instance of fn() as xs:string"}, + { "Subtyping 13", booleans(true), + "fn() as xs:string { 'x' } instance of fn() as xs:string?"}, + { "Subtyping 14", booleans(true), + "fn() as xs:string { 'x' } instance of fn() as xs:anyAtomicType"}, + { "Subtyping 15", booleans(false), + "let $g as function(xs:anyAtomicType) as xs:anyAtomicType := " + + "fn($x as xs:integer) as xs:string { 'x' } " + + "return ($g, $g)[1] instance of function(xs:integer) as xs:string"}, + { "Subtyping 16", booleans(false), + "for $c in (1 to 1) " + + "let $g as function(xs:anyAtomicType) as xs:anyAtomicType := " + + "fn($x as xs:integer) as xs:string { concat('x', $c) } " + + "return $g instance of function(xs:integer) as xs:string"}, + { "Subtyping 17", booleans(false), + "(fn($f as function(xs:anyAtomicType) as xs:anyAtomicType) { ($f, $f)[1] })" + + "(fn($x as xs:integer) as xs:string { 'x' }) " + + "instance of function(xs:integer) as xs:string"}, + { "Subtyping 18", booleans(false), + "let $f := (fn() as item() { 'a' }, fn() as item() { 1 })[trace(2)] " + + "return $f() instance of xs:string"}, + { "Subtyping 19", booleans(true), "fn() { 'x' } instance of function() as item()*"}, + { "Subtyping 20", booleans(false), "fn() { 'x' } instance of function() as xs:string"}, }; } } From 1a86d26fa90ebc58a91a81344c2d67bb496b8a83 Mon Sep 17 00:00:00 2001 From: Gunther Rademacher Date: Wed, 8 Jul 2026 19:34:43 +0200 Subject: [PATCH 2/2] shorten comments --- .../java/org/basex/query/func/Closure.java | 5 +---- .../java/org/basex/query/func/StaticFunc.java | 3 +-- .../org/basex/query/value/item/FItem.java | 3 +-- .../org/basex/query/value/type/FuncType.java | 22 +++++-------------- .../org/basex/query/value/type/SeqType.java | 3 +-- .../org/basex/query/value/type/TypeRef.java | 4 +--- .../main/java/org/basex/query/var/Var.java | 3 +-- 7 files changed, 11 insertions(+), 32 deletions(-) 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 637ac76d82..acab26021a 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 @@ -206,10 +206,7 @@ public Expr optimize(final CompileContext cc) { cc.removeScope(this); } - // the declared return type is observed by instance-of and function coercion; it is the type - // declared in the signature, or item()* if none was given (so 'fn() { "x" } instance of - // fn() as xs:string' yields false). The body's inferred type is kept as the refined return - // type, so that direct calls can still profit from the more specific result type. + // declared type for instance-of/coercion (item()* if none), body type as refined return type exprType.assign(FuncType.get(anns, declType, params).withRefinedType(expr.seqType())); // only evaluate if: diff --git a/basex-core/src/main/java/org/basex/query/func/StaticFunc.java b/basex-core/src/main/java/org/basex/query/func/StaticFunc.java index daf93f6eaa..a836bd1742 100644 --- a/basex-core/src/main/java/org/basex/query/func/StaticFunc.java +++ b/basex-core/src/main/java/org/basex/query/func/StaticFunc.java @@ -150,8 +150,7 @@ public QNm paramName(final int pos) { @Override public FuncType funcType() { - // align the refined return type with seqType() (the declared type, or the body type once the - // declared type has been checked and cleared), so this type stays consistent for result typing + // refined return type: the body type (via seqType), consistent with call-result typing return FuncType.get(anns, declType, params).withRefinedType(seqType()); } diff --git a/basex-core/src/main/java/org/basex/query/value/item/FItem.java b/basex-core/src/main/java/org/basex/query/value/item/FItem.java index ac19a1d1dc..995c7cb381 100644 --- a/basex-core/src/main/java/org/basex/query/value/item/FItem.java +++ b/basex-core/src/main/java/org/basex/query/value/item/FItem.java @@ -112,8 +112,7 @@ public FItem coerceTo(final FuncType ft, final QueryContext qc, final CompileCon if(cc != null) body = body.optimize(cc); } - // the coerced item advertises the target type (observed by instance-of); the body's inferred - // type is kept as the refined return type so result typing still profits from it + // advertise the target type; keep the body type as refined return type for result typing final FuncType tp = cc != null ? ft.withRefinedType(body.seqType()) : ft; body.markTailCalls(null); return new FuncItem(info, body, vars, annotations(), tp, vs.stackSize(), funcName()); diff --git a/basex-core/src/main/java/org/basex/query/value/type/FuncType.java b/basex-core/src/main/java/org/basex/query/value/type/FuncType.java index 9efae5db08..988feccd7d 100644 --- a/basex-core/src/main/java/org/basex/query/value/type/FuncType.java +++ b/basex-core/src/main/java/org/basex/query/value/type/FuncType.java @@ -22,13 +22,9 @@ public final class FuncType extends FType { /** Annotations. */ public final AnnList anns; - /** Declared return type (observed by instance-of, coercion and subtyping). */ + /** Declared return type (used by instance-of, coercion, subtyping). */ public final SeqType declType; - /** - * Refined return type, a subtype of {@link #declType} inferred from the function body. Used for - * result typing (e.g. dynamic calls, folds); never observed by instance-of, so that a function - * whose body is more specific than its declared return type still matches only its declaration. - */ + /** Refined return type (a subtype of {@link #declType}); used for result typing. */ public final SeqType refinedType; /** Argument types (can be {@code null}, indicates that no types were specified). */ public final SeqType[] argTypes; @@ -53,8 +49,7 @@ private FuncType(final AnnList anns, final SeqType declType, final SeqType refin final SeqType[] argTypes) { this.anns = anns; this.declType = declType == null ? ITEM_ZM : declType; - // the refined type must be a subtype of the declared type; a body type that is not (e.g. the - // integer body of 'fn() as xs:double { 1 }', whose result is coerced) contributes no refinement + // the refined type must be a subtype of the declared type; otherwise it adds no refinement this.refinedType = refinedType == null || !refinedType.instanceOf(this.declType) ? this.declType : refinedType; this.argTypes = argTypes; @@ -175,11 +170,7 @@ public Type union(final Type type) { if(type instanceof ChoiceItemType) return type.union(this); final FuncType ft = type.funcType(); - // two function types (a map or array is viewed through its function type) with matching arity: - // union argument, declared and refined return types structurally. This must happen before the - // instance-of short-circuits below, as those ignore the refined type: returning a single - // operand would drop the other's refined return type, unsoundly narrowing the result type of a - // call on the union. + // structural union, run before the instance-of short-circuits so the refined type is merged too if(this != FUNCTION && ft != null && ft != FUNCTION && argTypes != null && ft.argTypes != null && argTypes.length == ft.argTypes.length) { final int arity = argTypes.length; @@ -201,10 +192,7 @@ public Type union(final Type type) { public Type intersect(final Type type) { if(type instanceof ChoiceItemType) return type.intersect(this); - // if one function type is a subtype of the other, the intersection is that operand. Its refined - // return type is still met with the other's (the instance-of check ignores the refined type), - // yielding the most specific result type for calls on the intersection; if the refined types - // are disjoint, the operand is kept unchanged (no less precise than before). + // subtype: the intersection is that operand, but still meet its refined type with the other's if(instanceOf(type)) { if(type instanceof final FuncType ft) { final SeqType rf = refinedType.intersect(ft.refinedType); diff --git a/basex-core/src/main/java/org/basex/query/value/type/SeqType.java b/basex-core/src/main/java/org/basex/query/value/type/SeqType.java index 39af972cd5..d5a3738a6a 100644 --- a/basex-core/src/main/java/org/basex/query/value/type/SeqType.java +++ b/basex-core/src/main/java/org/basex/query/value/type/SeqType.java @@ -508,8 +508,7 @@ public boolean instanceOf(final SeqType st) { public boolean eq(final SeqType st) { if(this == st) return true; if(occ != st.occ) return false; - // an unresolved forward reference is a distinct placeholder: do not compare it via its - // temporary item() dereference (else e.g. a recursive record type would look like item()) + // an unresolved forward reference is a distinct placeholder, not its temporary item() deref if(TypeRef.unresolved(type) || TypeRef.unresolved(st.type)) return type == st.type; return TypeRef.deref(type).eq(TypeRef.deref(st.type)); } diff --git a/basex-core/src/main/java/org/basex/query/value/type/TypeRef.java b/basex-core/src/main/java/org/basex/query/value/type/TypeRef.java index ea76b80784..3d517c3649 100644 --- a/basex-core/src/main/java/org/basex/query/value/type/TypeRef.java +++ b/basex-core/src/main/java/org/basex/query/value/type/TypeRef.java @@ -62,9 +62,7 @@ public boolean resolved() { } /** - * Checks if the specified type is a still-unresolved forward reference. Such a reference is a - * distinct placeholder that dereferences to {@code item()}, so it must not be treated as equal to - * (or an instance of) the types it happens to dereference to before it has been resolved. + * Checks if the specified type is a still-unresolved forward reference. * @param type type * @return result of check */ diff --git a/basex-core/src/main/java/org/basex/query/var/Var.java b/basex-core/src/main/java/org/basex/query/var/Var.java index 727c45715c..52d1a5a7d2 100644 --- a/basex-core/src/main/java/org/basex/query/var/Var.java +++ b/basex-core/src/main/java/org/basex/query/var/Var.java @@ -82,8 +82,7 @@ public Var(final Var var, final QueryContext qc) { public SeqType seqType() { final SeqType st = exprType.seqType(), dt = declType; if(dt == null) return st; - // refine only if the bound type is a subtype of the declared type; if the value was coerced, - // the declared type is authoritative + // refine to the bound type only if it is a subtype; else the declared type is authoritative return st.instanceOf(dt) ? st : dt; }