@qt4cg statuses
This page displays recent status updates about the QT4CG project.
The are also captured in an RSS feed.
By year: 2026, 2025, 2024, 2023, 2022, 2021, 2020
QT4 CG meeting 182 draft agenda #agenda-09-28
Draft agenda published.
Issue #2935 created #created-2935
Proposal: opt-in commercial arithmetic for XPath, XQuery and XSLT 4.0
Proposal: opt-in commercial arithmetic for XPath, XQuery and XSLT 4.0
Summary
Add two components to the static context, each set per module and with a default that keeps today's behaviour:
- Default rounding mode, used by
fn:roundwhen$modeis absent. Defaulthalf-to-ceiling, as today. - Arithmetic mode:
double(default, as today) ordecimal, as Michael Kay considered in #986. Withdecimal, a module calculates in decimal floating-point throughout: untyped values,fn:number, numeric literals and mixed decimal/double arithmetic givexs:decimal. Binary floating-point remains only for values that have no decimal equivalent (NaN,INF) and for functions defined onxs:double, such asmath:sqrt.
A stylesheet or query for commercial calculations, such as the validation of electronic invoices, could then declare once that it rounds commercially and calculates in decimal, instead of repeating casts and rounding modes in every expression. Nothing changes for modules that do not opt in.
Motivation
Commercial calculations have legal requirements that differ from the defaults of XPath:
- Commercial rounding. VAT in Germany must be rounded half away from zero
(§ 14 Abs. 4 UStG with Abschn. 14.5 Abs. 20 UStAE; DIN 1333), and the French
e-invoicing standard AFNOR XP Z12-012 (§ 4.4.6) requires the same, so that "rounding two
strictly opposite numbers gives strictly opposite rounded numbers". EN 16931 recommends
it as well.
fn:round(-2.5)returns −2, but −3 is required. - Decimal arithmetic. Amounts are decimal numbers. Without a schema, which is the normal
case in Schematron validation, every amount is
xs:untypedAtomic, and arithmetic converts it toxs:double: with<a>0.1</a>and<b>0.2</b>,a + bis 0.30000000000000004 anda + b = 0.3is false;round(<p>1.005</p>, 2)returns 1, not 1.01.
Rule authors can avoid both problems today, with xs:decimal() casts on every operand and
the $mode argument on every fn:round call. In practice they miss some. The official
EN 16931 validation artefacts (release 1.3.16),
maintained by CEN and used across the EU, are an example. Rule BR-S-08 in UBL tolerates
a deviation of the VAT category taxable amount below 1.00 and tests
xs:decimal(cbc:TaxableAmount + 1) > sum(…xs:decimal(cbc:LineExtensionAmount)…)
The cast comes after the addition, so the addition is done in xs:double. For an invoice
whose lines add up to 197.37 and whose declared taxable amount is 196.37, exactly 1.00 too
low, 196.37 + 1 gives 197.37000000000000455 in binary, which is greater than 197.37, and the
invalid invoice is accepted. The same amount 1.00 too high (198.37) is rejected, and so is the
same invoice in CII syntax, whose rule compares exactly. About half of all two-decimal amounts
behave like 196.37 in one of the two directions. The CII rules BR-AE-08, BR-E-08, BR-G-08,
BR-IC-08 and BR-Z-08 compute ../ram:BasisAmount - 1 on the untyped amount in the same way.
The verdict of the reference validation depends on binary arithmetic, not on the rule.
Test invoices and a reproducible comparison.
Rules like these are written by domain experts, not by XPath specialists. EN 16931 alone has several hundred of them, and national extensions (XRechnung, Factur-X/ZUGFeRD, Peppol) and company rules add more. A single declaration per rule set is realistic; a cast on every operand is not.
Relation to earlier work in this group
- #1187 and #1274 added the rounding modes to
fn:round, following a request about financial rounding (Saxon issue 6408). This proposal only adds a way to change the default of$modefor a module. - In #986, Michael Kay considered "an 'arithmetic mode' in the dynamic context, set to either
'double' or 'decimal'", and later proposed to "change conversion from untypedAtomic to
numeric to depend on the lexical form of the value, as it does for numeric literals".
#2218 adopted a related rule for general comparisons: an untyped value compared with a
number is cast to the type of the numeric operand, with
xs:doubleas fallback. This proposal takes the arithmetic mode further, to decimal arithmetic throughout a module, as an opt-in, so that existing code is not affected.
Proposal
1. Default rounding mode
A new static context component, default rounding mode, whose value is one of the modes
of fn:round. Its default is half-to-ceiling.
fn:round#1 and fn:round#2, and fn:round#3 with an empty $mode, use the default
rounding mode of the static context of the call. Dynamic calls such as round#1 bind it at
the point where the function item is created, as for the default collation.
2. Arithmetic mode
A new static context component, arithmetic mode, with the values double (the default)
and decimal. The mode double is today's behaviour. The mode decimal avoids binary
floating-point wherever a decimal value can represent the number, which gives exact results
for every calculation that XPath can express in decimals:
- Untyped values. Wherever an
xs:untypedAtomicvalue is converted to a number without an explicit required type ofxs:doubleorxs:float, a lexical form that is valid as a numeric literal (after whitespace normalization) is cast toxs:integerorxs:decimal, including scientific notation such as1.5E3. Other values, such asNaNandINF, are cast toxs:doubleas today, and invalid input raises the same errors. This applies to arithmetic operators,fn:sum,fn:avg,fn:min,fn:max, and the coercion of arguments whose required type isxs:numeric(such asfn:roundandfn:abs). fn:numberreturns anxs:decimalfor such a lexical form, andNaNas today for anything else.- Numeric literals. A
DoubleLiteralsuch as1.5e0denotes anxs:decimal. - Mixed arithmetic. An operation with an
xs:decimaland anxs:doubleorxs:floatoperand converts the binary operand toxs:decimaland returns anxs:decimal, the reverse of today's promotion, unless that operand isNaNor infinite.
Binary floating-point then remains only where decimal cannot represent the value (NaN,
positive and negative infinity), in functions defined on xs:double such as math:sqrt,
math:log or math:pow, whose results are mostly irrational, and in operations whose
operands are all explicitly typed as xs:double or xs:float. General comparisons keep the
rules of #2218.
Syntax
-
XSLT: standard attributes
[xsl:]default-rounding-modeand[xsl:]arithmetic-mode, allowed on any element and scoped like[xsl:]default-collation:<xsl:stylesheet version="4.0" default-rounding-mode="half-away-from-zero" arithmetic-mode="decimal" …> -
XQuery: prolog declarations such as
declare default rounding-mode "half-away-from-zero";anddeclare arithmetic-mode decimal; -
XPath hosted by other languages: static context properties set by the host language or API. Schematron could pass them through from a rule set to the generated XSLT; that is a matter for ISO Schematron, not for this group.
Why the static context
- Compatibility. The defaults are today's behaviour. Only modules that declare the new settings change, so no existing stylesheet or query breaks.
- Visibility. The declaration is part of the module. A reader sees how it calculates, and it gives the same results on every conforming processor, unlike a processor configuration.
- Scope. A validation service runs rule sets and unrelated stylesheets in the same process. A module-level setting affects only the rule set that asks for it.
- Precedent. XPath 1.0 compatibility mode is a static context component that already changes the semantics of arithmetic and comparisons for a whole module.
Costs of the decimal mode
Decimal floating-point is more accurate for every value that has a decimal representation, which is what commercial data consists of. Its main cost is performance: decimal arithmetic needs more memory and CPU time than hardware binary floating-point. It also changes some results, which is why the mode is opt-in:
- Division by zero raises
FOAR0001instead of returningINForNaN. - Nonterminating division, such as 1 div 3, is rounded to an implementation-defined
precision, as for
xs:decimaltoday, wherexs:doublegives about 17 significant digits.fn:divide-decimals(#1261) gives explicit control. - Extreme magnitudes such as
1e308are exact in decimal but need correspondingly large values; an implementation may convert exponents beyond a limit toxs:double.
Open questions
- How is an
xs:doubleoperand converted in mixed arithmetic: to its exact binary value (0.1000000000000000055511151231257827…), or to the shortest decimal that identifies it (0.1)? The second matches what users see and what the value usually came from. - Should the decimal mode also apply to the arithmetic of XPath 1.0 compatibility mode?
Implementation experience
Saxon-HE-enhanced-accuracy,
a fork of Saxon-HE 13.0, implements both settings as fixed defaults in a few hundred lines:
half away from zero, and decimal arithmetic for untyped values, fn:number, scientific
notation and XPath 1.0 compatibility mode. It keeps mixed arithmetic with explicitly typed
xs:double values binary. Saxon's existing rounding modes and the #2218 comparison code carried
most of the work. 32 example calculations
are run as tests against both the stock and the modified processor, and the official EN 16931
validation artefacts are run unchanged on both. The code is available under the Mozilla Public
License 2.0, as Saxon-HE, and could serve as a starting point for an opt-in implementation.
Pull request #2934 created #created-2934
2908 Nominative and Structural Record Types
This PR provides a more conservative alternative to the rather over-ambitious proposal made in PR #2815.
We already have two kinds of record type in our spec, this proposal attempts to differentiate them more clearly.
The terms "nominative record type" and "structural record type" are introduced: the term "named record type" was confusing because it could apply either to "declare type record" or "declare record", which have rather different semantics. The rules for type matching and subtyping in both cases are clarified. The special characteristics of "recursive record types" are now ascribed to "nominative record types" whether or not they happen to be recursive.
The PR paves the way for introducing nominative record types derived by extension and restriction in a separate proposal.
Fix #2908
Issue #2933 created #created-2933
Validation of records
I propose that it should be possible (perhaps by means of an annotation) to identify one of the methods on a record type as being a validation method. The validation method will be automatically invoked whenever an instance of the record type is constructed or modified (using "but with"), and will trigger a dynamic error if it returns false.
Alternatively we could allow a predicate on the record definition:
declare record (x as integer, y as integer, z as integer) where empty(duplicate-values((?x, ?y, ?z)))
Pull request #2932 created #created-2932
2919 XQFO: feature requests from users
Closes #2919 …and no further XQFO functions from our side.
Issue #2921 closed #closed-2921
Lax record coercions
Pull request #2931 created #created-2931
2930 fn:map-to-element: tweaks
…and some more, which I hope will improve usability:
- Layout selection: each element’s layout is chosen from the plan entry, then the
*fallback, or otherwise inferred from the value’s shape. - One table: each layout has one row that says which values it accepts and how the element is rebuilt.
- One child or several: the plan decides whether an array under a key means one child or repeated children.
- Stricter arrays: attributes come first, and forms that element-to-map never produces are rejected.
- Marker clash: a key starting with
@is read as an attribute, even with a custom attribute-marker. ()is"", so JSONnullfromparse-jsonworks.- Note: how to convert JSON objects with several keys, and JSON arrays, by wrapping them.
- Error list: simplified in
function-catalog.xml.
Only 2, 3 existing test cases will need to be changed.
Closes #2930
Issue #2930 created #created-2930
`fn:map-to-element`: tweaks
Proposed changes, to improve roundtripping:
- One child or several: under a child key, create one child if the value has the shape of the child’s planned layout, and one child per member if it’s an array of such values. Only
listtreats an array as the content of a single child;list-plusdoes not. - Fallback: if a value doesn't have the shape of its planned layout, use the plan’s
*layout. If that doesn’t fit either, or there is none, raise an error. - Shapes: add a table of what each layout’s value looks like, as the basis for 1 and 2.
- Limit: add a note that a fallback result which happens to have the shape of the planned layout is converted back with the planned layout, so the element comes back different.
Pull request #2929 created #created-2929
2928 file:read-text: minor unifications
…and some cleanups.
Closes #2928
Issue #2928 created #created-2928
`file:read-text`: minor unifications
file:read-text should be further aligned with fn:unparsed-text:
file:read-text,file:read-text-lines: Change default encoding fromutf-8to()file:read-text: Add optionnormalize-newlines.
Issue #2927 created #created-2927
JNode subtypes and naming
Discussion of JNode subtypes and naming conventions
Currently we have the jnode() type, jkey and jvalue() functions. It is quite likely that we will need a few more functions and a number of jnode() subtypes (useful when using path selectors and templates).
I think, we will need the following subtypes of jnode(): jmap(), jarray, jsequence(), jatom(), jempy-node().
Perhaps, a better naming option: map-jnode(), array-jnode(), sequence-jnode(), atom-jnode(), empty-jnode().
Does it make sense to move them all to a separate namespace with a default j prefix?
Using a separate namespace, the subtyping relationships are as follows:
j:empty-node() ≡ j:node(*, empty-sequence() )j:empty-node(N) ≡ j:node(N, empty-sequence() )j:item-node() ≡ j:node(*, item() )j:item-node(N) ≡ j:node(N, item() )j:sequence() ⊆ j:node()- note that an item can be an instance of both
j:sequence()andj:empty-node()
- note that an item can be an instance of both
j:sequence(N) ⊆ j:node(N)j:map() ≡ j:node(*, map(*) )j:map(N) ≡ j:node(N, map(*) )j:array() ≡ j:node(*, array(*) )j:array(N) ≡ j:node(N, array(*) )j:atom() ≡ j:node() \ ( j:sequence() | j:map() | j:array() )- its instances are all and only instances of
j:node()that are not instances of( j:sequence() | j:map() | j:array() ) - note that it includes instances of
j:empty-node()that are not instances ofj:sequence()(should that be changed?)
- its instances are all and only instances of
j:atom(N) ≡ j:node(N) \ ( j:sequence() | j:map() | j:array() )
In the JNode mapping scheme I propose, nodes of the j:sequence() type are located only on the seq:: axis, and this axis can contain only nodes of the j:sequence() type.
Update: j:item-node() added.
Issue #2926 created #created-2926
XSLT Shallow copy of a JNode map
11.9.1.2 Shallow Copy: JNodes currently says:
A new map is constructed as if by evaluating the instruction
<xsl:map select="S" duplicates="'use-last'"/>, and V1 is this map. Note: This means that each item in S must either be a map, or a JNode whose ·jvalue· is a map;
It seems, this spec does not meet a reasonable expectation that the following instruction should create a JNode whose jvalue is the same as in @select:
<xsl:copy select='{ "a":(10), "b":(20) }/.'>
<xsl:copy-of select="*"/>
</xsl:copy>
Since in this case each item of S is a JNode whose value is an instance of xs:integer (rather than map(*)).
In #2922 comment I suggested to transform S, by converting JNodes (or JTrees as far as they are parentless) to singleton maps, before applying map:merge. Their jkey value should be used for the map key.
QT4 CG meeting 181 draft minutes #minutes-09-22
Draft minutes published.
Issue #2789 closed #closed-2789
2787 XQuery changes to merge declare record and declare type
Issue #2909 closed #closed-2909
Binary functions: Base64 URL en-/decoding
Issue #2910 closed #closed-2910
2909 Binary functions: Base64 URL en-/decoding
Issue #1777 closed #closed-1777
Shallow copy in XSLT with maps and arrays
Issue #2912 closed #closed-2912
1777 Revise XSLT shallow copy for JNodes
Issue #2073 closed #closed-2073
JNodes and Sequences
Issue #2878 closed #closed-2878
2073 JNodes and Sequences
Issue #2861 closed #closed-2861
Edge cases with CSV serialization
Issue #2862 closed #closed-2862
2861 Clarify CSV serialization edge cases
Issue #2785 closed #closed-2785
Make XSLT tunnel parameters available dynamically as a map
Issue #2925 created #created-2925
`bin:to-base64url`, `bin:from-base64url`: other names?
https://github.com/qt4cg/qtspecs/pull/2910 was accepted today. We could discuss better names for the functions.
Issue #2924 created #created-2924
NodeTest for JNodes
JNode node test discussion
We could also redefine the node test
*, when applied to JNodes, so it only selects a JNode whose jvalue is a singleton map or array; to select other nodes, use the node test jnode(). That's a good analogy to the fact that with XNodes,*only selects elements.
Originally posted by @michaelhkay in #2922
Does that mean that foo selects only jnode(foo, (array(*)|map(*)|sequence(*)) ) (or sort of)?
In that case, we would need something like atom(bar) or @bar to select jnode(bar, *)[not(child::jnode(*))][not(self:(array(*)|map(*)) )].
That probably makes sense.
There is a differences here compared to XML. In XML, typically, most child nodes that have a name are elements, whereas most child nodes that are not elements are nameless. However, in map/array structures, most atomic nodes have a name, as well as not atomic nodes.
QT4 CG meeting 181 draft agenda #agenda-09-22
Draft agenda published.
Issue #2923 created #created-2923
Idempotent Coercion
We already discovered that coercing a function F to a required type T may be necessary even if F is already an instance of T, because the coerced function performs stronger checks on the types of the arguments supplied (see issue #1020).
We now have a test case:
<test-case name="recordTypeDecl-050" covers-40="PR1874">
<description>Record types: field order, subtyping and coercion</description>
<created by="Christian Gruen" on="2026-09-16"/>
<test><![CDATA[
declare record local:AB(a as xs:integer, b as xs:integer);
declare record local:BA(b as xs:integer, a as xs:integer);
declare function local:f($r as local:BA) { map:keys($r) };
local:f(local:AB(1, 2))
]]></test>
<result>
<assert-deep-eq>"b", "a"</assert-deep-eq>
</result>
</test-case>
which demonstrates that coercing a record R to type T may be necessary even if R is already an instance of T, because it changes the order of entries in the record (the instance of check does not require fields to be in the right order).
This feels very counter-intuitive to me. On an ordinary English-language usage, "coercion" means modifying something to meet a given requirement, and shouldn't be necessary if the requirement is already met. If the order of fields is important enough to justify re-ordering, then it should also be important enough for instance of to fail.
I would like to see if we can adopt a general principle that if a value V is an instance of a type T, then coercion of V to T should be idempotent. This principle is important to allow type checking to be performed statically.
(There's an argument that if a function expects a record of type R, then the implementation of the function body should be able to make assumptions about the order of fields, in order to enable more efficient access. But on that line of reasoning, "instance of" should be a stronger test, so that the same logic can be used (say) in a branch of a typeswitch).
This may involve looking again at coercion of function items, which I previously thought was the only exception to this rule.
Another way of solving the problem for function calls would be to change the "instance of" rules. For example, given the example from the current spec,
let $f as function(xs:integer) as item()* := function($x) { $x + 1 }
return $f(12.3)
we would need to change the definition of "instance of" so that function($x) { $x + 1 } is no longer an instance of function(xs:integer) as item()* (instead, it is merely coercible to that type). As far as I can tell, that simply means removing the "contravariance" provision on argument types (§3.3.2.6 rule 2(f)). It seems a reasonable change, because the supplied function doesn't do everything that the expected type requires - it doesn't reject non-integer arguments.
An alternative solution would be to revert to the 3.1 spec here: no coercion is performed in this case and the above query succeeds, returning 13.3.
Issue #2922 created #created-2922
Representation of sequence of items as a sequence of JNodes
Some ideas for discussion.
In the approach where sequences are represented as adjacent JNodes with the same jkey, a single JNode cannot serve to group an arbitrary sequence of items. Therefore,
xsl:array-membershould likely return a single-member array (and the sequence of them are concatenated witharray:join), much likexsl:map-entryreturns a single-entry map (and a sequence of them are concatenated withmap:merge).
Originally posted by @ruv in #2073
This approach was roughly described in my comments on 2025-12-04 and 2026-09-01.
Example:
<jnode key='()' kind='map' value='{ "d": (7, [ 8, (9,10) ], 11), "e": (12,13), "f":14 }'>
<jnode key='d' kind='leaf' value='7'/>
<jnode key='d' kind='array' value='[ 8, (9,10) ]'>
<jnode key='1' kind='leaf' value='8'/>
<jnode key='2' kind='leaf' value='9'/>
<jnode key='2' kind='leaf' value='10'/>
</jnode>
<jnode key='d' kind='leaf' value='11'/>
<jnode key='e' kind='leaf' value='12'/>
<jnode key='e' kind='leaf' value='13'/>
<jnode key='f' kind='leaf' value='14'/>
</jnode>
Some problems of this approach are:
- when copy a JNode (by
xsl:copy-oforxsl:copy), jkey should be erased if the parent type isjnode(*,array(.*)), but we still have to separate new jnodes per array members. - it is unclear how to handle jkey conflicts when constructing
jnode(*,map(.*)). - selecting the next JNode withing the same array-member (or map-entry) without knowing the jkey is too verbose.
To address these problems, a peer binary relation (a partial order) can be introduced in addition to the sibling relation. The peer relation should be preserved when JNodes are copied. Only JNodes that wrap items from the same sequence (e.g., from the same array-member) stand in the peer relationship to each other. Perhaps, it may be advisable to disjoin the peer and the sibling relations (that is, two JNodes cannot be in both peer and sibling relations at the same time).
The corresponding axes: preceding-peer::, following-peer::
/e[1] => jvalue() returns 12
/e[1]/following-peer::*[1] => jvalue() returns 13
/e[1]/following-sibling::*[1] => jvalue() returns 14
/d[last()] => jvalue() returns 11
/d[last()]/following-sibling::* => count() returns 3
/*[not(preceding-peer::*)] returns /( d[1] | e[1] | f[1] )
/*[not(following-peer::*)]/jvalue() returns (11, 13, 14)
To construct an array from a sequence $s of JNodes:
<xsl:array>
<xsl:for-each-group select="$s" group-start-with="*[ not(preceding-peer::*) ]">
<xsl:array-member select="current-group()/jvalue()"/>
</xsl:for-each>
</xsl:array>
Probably, it should be an error if peer JNodes in $s are not transitively adjacent.
If two nodes are not peers and have the same jkey, this is only case requiring duplicate keys handling.
Update: to make the "peer" relation useful for XNodes too, the "sibling" relation should not change (i.e., it should not be disjoint from "peer").
Issue #2921 created #created-2921
Lax record coercions
We’ve decided that…
let $coord as record(x, y) := { 'x': 1, 'y': 2, 'z': 3 }
return $coord
…raises an error. We could redesignate the deprecated extensibility syntax and use…
(: yields { 'x': 1, 'y': 2 } :)
let $coord as record(x, y, *) := { 'x': 1, 'y': 2, 'z': 3 }
return $coord
…to create a record with two entries that discards the remaining ones.
Pull request #2920 created #created-2920
2813 clarifications for xsl:result-document
The primary purpose of this PR is to clarify the situation when an xsl:result-document instruction has an empty or absent href attribute, and thus writes to the principal output destination.
However, it also takes the opportunity to add many more clarifications to the serialization part of the XSLT spec, to improve the order of presentation, and to remove some redundancy
Fix #2813
See 6067 more statuses in yearly archives.