@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

28 Sep at 10:30:00 GMT

Draft agenda published.

Issue #2935 created #created-2935

28 Sep at 08:11:47 GMT
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:

  1. Default rounding mode, used by fn:round when $mode is absent. Default half-to-ceiling, as today.
  2. Arithmetic mode: double (default, as today) or decimal, as Michael Kay considered in #986. With decimal, a module calculates in decimal floating-point throughout: untyped values, fn:number, numeric literals and mixed decimal/double arithmetic give xs:decimal. Binary floating-point remains only for values that have no decimal equivalent (NaN, INF) and for functions defined on xs:double, such as math: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 to xs:double: with <a>0.1</a> and <b>0.2</b>, a + b is 0.30000000000000004 and a + b = 0.3 is 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 $mode for 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:double as 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:untypedAtomic value is converted to a number without an explicit required type of xs:double or xs:float, a lexical form that is valid as a numeric literal (after whitespace normalization) is cast to xs:integer or xs:decimal, including scientific notation such as 1.5E3. Other values, such as NaN and INF, are cast to xs:double as 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 is xs:numeric (such as fn:round and fn:abs).
  • fn:number returns an xs:decimal for such a lexical form, and NaN as today for anything else.
  • Numeric literals. A DoubleLiteral such as 1.5e0 denotes an xs:decimal.
  • Mixed arithmetic. An operation with an xs:decimal and an xs:double or xs:float operand converts the binary operand to xs:decimal and returns an xs:decimal, the reverse of today's promotion, unless that operand is NaN or 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-mode and [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"; and declare 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 FOAR0001 instead of returning INF or NaN.
  • Nonterminating division, such as 1 div 3, is rounded to an implementation-defined precision, as for xs:decimal today, where xs:double gives about 17 significant digits. fn:divide-decimals (#1261) gives explicit control.
  • Extreme magnitudes such as 1e308 are exact in decimal but need correspondingly large values; an implementation may convert exponents beyond a limit to xs:double.

Open questions

  • How is an xs:double operand 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

27 Sep at 18:11:28 GMT
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

27 Sep at 16:13:48 GMT
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

27 Sep at 14:24:57 GMT
2919 XQFO: feature requests from users

Closes #2919 …and no further XQFO functions from our side.

Issue #2921 closed #closed-2921

27 Sep at 09:16:03 GMT

Lax record coercions

Pull request #2931 created #created-2931

27 Sep at 08:53:23 GMT
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 JSON null from parse-json works.
  • 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

27 Sep at 08:09:22 GMT
`fn:map-to-element`: tweaks

Proposed changes, to improve roundtripping:

  1. 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 list treats an array as the content of a single child; list-plus does not.
  2. 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.
  3. Shapes: add a table of what each layout’s value looks like, as the basis for 1 and 2.
  4. 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

27 Sep at 07:41:19 GMT
2928 file:read-text: minor unifications

…and some cleanups.

Closes #2928

Issue #2928 created #created-2928

27 Sep at 07:28:26 GMT
`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 from utf-8 to ()
  • file:read-text: Add option normalize-newlines.

Issue #2927 created #created-2927

25 Sep at 08:54:39 GMT
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() and j:empty-node()
  • 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 of j:sequence() (should that be changed?)
  • 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

23 Sep at 14:15:22 GMT
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

22 Sep at 16:40:00 GMT

Draft minutes published.

Issue #2789 closed #closed-2789

22 Sep at 17:02:26 GMT

2787 XQuery changes to merge declare record and declare type

Issue #2909 closed #closed-2909

22 Sep at 16:32:04 GMT

Binary functions: Base64 URL en-/decoding

Issue #2910 closed #closed-2910

22 Sep at 16:32:03 GMT

2909 Binary functions: Base64 URL en-/decoding

Issue #1777 closed #closed-1777

22 Sep at 16:26:52 GMT

Shallow copy in XSLT with maps and arrays

Issue #2912 closed #closed-2912

22 Sep at 16:26:51 GMT

1777 Revise XSLT shallow copy for JNodes

Issue #2073 closed #closed-2073

22 Sep at 16:20:47 GMT

JNodes and Sequences

Issue #2878 closed #closed-2878

22 Sep at 16:20:45 GMT

2073 JNodes and Sequences

Issue #2861 closed #closed-2861

22 Sep at 16:17:20 GMT

Edge cases with CSV serialization

Issue #2862 closed #closed-2862

22 Sep at 16:17:19 GMT

2861 Clarify CSV serialization edge cases

Issue #2785 closed #closed-2785

22 Sep at 16:14:52 GMT

Make XSLT tunnel parameters available dynamically as a map

Issue #2925 created #created-2925

22 Sep at 16:08:23 GMT
`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

22 Sep at 06:13:42 GMT
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

21 Sep at 11:00:00 GMT

Draft agenda published.

Issue #2923 created #created-2923

21 Sep at 10:11:31 GMT
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

18 Sep at 02:11:20 GMT
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-member should likely return a single-member array (and the sequence of them are concatenated with array:join), much like xsl:map-entry returns a single-entry map (and a sequence of them are concatenated with map: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-of or xsl:copy), jkey should be erased if the parent type is jnode(*,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

17 Sep at 20:43:59 GMT
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

16 Sep at 22:12:51 GMT
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.