leptris 1.9.292.0-ppc64le-linux → 1.9.304.0-ppc64le-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/Rakefile +1 -1
  4. data/lib/leptris/version.rb +1 -1
  5. data/lib/leptris/xml/element.rb +112 -4
  6. data/lib/libleptris.so +0 -0
  7. data/vendor-src/README.md +1 -1
  8. data/vendor-src/libleptris/CHANGELOG.md +398 -0
  9. data/vendor-src/libleptris/CMakeLists.txt +6 -1
  10. data/vendor-src/libleptris/benchmarks/modification/bench_append_attr_pugixml.cpp +36 -0
  11. data/vendor-src/libleptris/bindings/rust/Cargo.lock +1 -1
  12. data/vendor-src/libleptris/bindings/rust/Cargo.toml +3 -1
  13. data/vendor-src/libleptris/cli/CLI_ARCHITECTURE.md +1 -1
  14. data/vendor-src/libleptris/cli/CMakeLists.txt +1 -0
  15. data/vendor-src/libleptris/cli/cli.c +36 -1
  16. data/vendor-src/libleptris/cli/cli.h +39 -2
  17. data/vendor-src/libleptris/cli/commands/xquery.c +1 -31
  18. data/vendor-src/libleptris/cli/commands/xslt.c +118 -0
  19. data/vendor-src/libleptris/cli/main.c +24 -22
  20. data/vendor-src/libleptris/cli/man/leptris-diff.1.adoc +40 -0
  21. data/vendor-src/libleptris/cli/man/leptris-validate.1.adoc +43 -0
  22. data/vendor-src/libleptris/cli/man/leptris-xquery.1.adoc +44 -0
  23. data/vendor-src/libleptris/cli/man/leptris-xslt.1.adoc +49 -0
  24. data/vendor-src/libleptris/src/CMakeLists.txt +26 -0
  25. data/vendor-src/libleptris/src/leptris/dom/compact.c +4 -0
  26. data/vendor-src/libleptris/src/leptris/dom/element_modify.c +83 -18
  27. data/vendor-src/libleptris/src/leptris/dom/mut_recycle.c +112 -0
  28. data/vendor-src/libleptris/src/leptris/dom/mut_recycle.h +50 -0
  29. data/vendor-src/libleptris/src/leptris/leptris.c +10 -4
  30. data/vendor-src/libleptris/src/leptris/leptris_internal.h +7 -0
  31. data/vendor-src/libleptris/src/leptris/xpath/collation.c +494 -0
  32. data/vendor-src/libleptris/src/leptris/xpath/collation.h +66 -0
  33. data/vendor-src/libleptris/src/leptris/xpath/ducet_table.h +21994 -0
  34. data/vendor-src/libleptris/src/leptris/xpath/evaluator_internal.h +6 -0
  35. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +82 -5
  36. data/vendor-src/libleptris/src/leptris/xpath/evaluator_types.c +53 -8
  37. data/vendor-src/libleptris/src/leptris/xpath/functions.c +605 -68
  38. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +636 -65
  39. data/vendor-src/libleptris/src/leptris/xpath/vm.c +20 -4
  40. data/vendor-src/libleptris/src/leptris/xpath/xpath_public.c +3 -1
  41. data/vendor-src/libleptris/src/leptris/xquery/xquery.c +23 -1
  42. data/vendor-src/libleptris/test/CMakeLists.txt +14 -0
  43. data/vendor-src/libleptris/test/abi/test_dom_mutation_bugs.cpp +127 -0
  44. data/vendor-src/libleptris/test/cli/test_cli.cpp +61 -0
  45. data/vendor-src/libleptris/test/memory/test_mut_recycle.cpp +64 -0
  46. data/vendor-src/libleptris/test/xpath/test_collation.cpp +147 -0
  47. data/vendor-src/libleptris/test/xpath/test_xslt.cpp +114 -0
  48. data/vendor-src/libleptris/test/xquery/qt3/fn/abs.xml +2085 -0
  49. data/vendor-src/libleptris/test/xquery/qt3/fn/apply.xml +214 -0
  50. data/vendor-src/libleptris/test/xquery/qt3/fn/avg.xml +2199 -0
  51. data/vendor-src/libleptris/test/xquery/qt3/fn/ceiling.xml +961 -0
  52. data/vendor-src/libleptris/test/xquery/qt3/fn/collation-key.xml +331 -0
  53. data/vendor-src/libleptris/test/xquery/qt3/fn/filter.xml +387 -0
  54. data/vendor-src/libleptris/test/xquery/qt3/fn/floor.xml +968 -0
  55. data/vendor-src/libleptris/test/xquery/qt3/fn/fold-left.xml +426 -0
  56. data/vendor-src/libleptris/test/xquery/qt3/fn/fold-right.xml +363 -0
  57. data/vendor-src/libleptris/test/xquery/qt3/fn/for-each-pair.xml +585 -0
  58. data/vendor-src/libleptris/test/xquery/qt3/fn/for-each.xml +170 -0
  59. data/vendor-src/libleptris/test/xquery/qt3/fn/max.xml +2018 -0
  60. data/vendor-src/libleptris/test/xquery/qt3/fn/min.xml +1973 -0
  61. data/vendor-src/libleptris/test/xquery/qt3/fn/round-half-to-even.xml +1336 -0
  62. data/vendor-src/libleptris/test/xquery/qt3/fn/round.xml +2539 -0
  63. data/vendor-src/libleptris/test/xquery/qt3/fn/sum.xml +2043 -0
  64. data/vendor-src/libleptris/test/xquery/qt3/misc/UCACollation.xml +1120 -0
  65. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +196 -14
  66. data/vendor-src/libleptris/test/xquery/test_xquery.cpp +293 -0
  67. data/vendor-src/libleptris/tools/gen_ducet_table.py +110 -0
  68. data/vendor-src/libleptris/vcpkg.json +1 -1
  69. data/vendor-src/libleptris/vendor/ducet/allkeys.txt +39435 -0
  70. metadata +33 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: deba2902336772763d8daf6c1bf66cf6381a1c1536b83a7ea604f27fd6dd8e2f
4
- data.tar.gz: 52c284c22ac8ebc747410589daf061afbb8dcfd9e900ce3fec0f45110d190d97
3
+ metadata.gz: 785d9b7e66ab4d337f9002a5d4bc39d67c681a74e1dc04967990279e866e034c
4
+ data.tar.gz: e804fd26e2b9ab2d42cbdcd1e0af3972f5d1fc8dda4c6c690c0308bb9b0030ae
5
5
  SHA512:
6
- metadata.gz: 7eaa86daa077f9c4ef1adcf01e78340d744ba6c9f73a5bb30fdd16de4efc3cb5dfb75fee50912c348f332ac545aba7422497b1fa92e47d6c685d8b4c6fd44c11
7
- data.tar.gz: 6ae7b5c085e442ce31d94391da70fb4db563b38af1b91275cc0bb5087bdb1d5ee840965c9dcc8501f1fec7e31067dd23e63bfa0a804d32968edd4e2cb883da75
6
+ metadata.gz: 36067b9300f15de58055ce88d04ba035410585c79134270ce26fb54ff177f6366e904e248554548c23c7670666f6e90045cf6f756c51a037e5ccddb334770de4
7
+ data.tar.gz: 37873702eb98d9aa34dddbd8b876e650a0b1a9abb6a84798922161007a548953df1c3565bf624476862540bd76ff9716b6374ee73a424005182cf71ef79512eb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,47 @@
1
+ ## [1.9.304.0] - 2026-10-05
2
+
3
+ ### Fixed
4
+
5
+ - **Cross-document appends adopt by copy — the xpath.rb:39 crash
6
+ class (engine #1528, libleptris 1.9.304)**: the engine's four
7
+ splice seams (append/prepend/insert_before/insert_after) no
8
+ longer splice foreign-document pointers raw — a scratch-doc
9
+ node appended into a live tree then freed with its document
10
+ dangled every adopted node and detonated the next compiled-XPath
11
+ eval (the metanorma sectioned-cleanup crash). The binding now
12
+ completes the contract: cross-document appends return the
13
+ INSTALLED in-tree wrapper (add_child, append_children, children=,
14
+ prepend_child, add_previous_sibling, add_next_sibling, wrap),
15
+ the #178 namespace lift runs before the splice, and the source
16
+ original is removed after (the historical move semantics —
17
+ source loses the node). Same-document appends stay
18
+ pointer-identical on the native fast path. The wrap face moves
19
+ self into the INSTALLED wrapper. Adoption specs include the
20
+ scratch-free + compiled-XPath crash shape.
21
+
22
+ ## [1.9.302.0] - 2026-10-05
23
+
24
+ ### Engine sync (libleptris 1.9.293-1.9.302)
25
+
26
+ Ten upstream releases ride in this pin — internal fixes and
27
+ performance with NO binding-surface changes (zero header deltas
28
+ 292..302; the audit gate confirms 363/362 unchanged). Highlights:
29
+
30
+ - **XQuery correctness**: a false() bound anywhere read as truthy;
31
+ fn:not uses the effective boolean value; fn:abs(()) is empty;
32
+ string lt/gt are codepoint comparisons; fn:avg edge shapes;
33
+ aggregates atomize array arguments; `instance of xs:integer`
34
+ demands integrality; xs:byte/unsignedByte/unsignedInt
35
+ constructors; fn:sum#2's zero argument.
36
+ - **Collation**: UCA over the vendored DUCET table (slices 1-4) —
37
+ fn:normalize-unicode F&O conformance, collation-aware string
38
+ functions, 71+1078 QT3 cases adopted, implicit weights and
39
+ alternate=blanked.
40
+ - **Performance**: per-thread recycle of mutation blocks (DOM
41
+ create/build/free).
42
+ - **Rust bindings**: first tokenless crates.io release (leptris
43
+ v1.4.1) from CI.
44
+
1
45
  ## [1.9.292.0] - 2026-10-03
2
46
 
3
47
  ### Fixed
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
8
8
  # Pin for `rake compile` and the platform-gem builds. Keep in lockstep
9
9
  # with .github/workflows/build.yml (which calls `rake compile`) and the
10
10
  # CHANGELOG when libleptris releases.
11
- LIBLEPTRIS_VERSION = "1.9.292"
11
+ LIBLEPTRIS_VERSION = "1.9.304"
12
12
  # Vendored alongside libleptris for fn:normalize-unicode (TODO
13
13
  # .restructure/20): built per platform with a RELOCATABLE @rpath
14
14
  # install name, loaded by ffi.rb before libleptris so the
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.292.0"
4
+ VERSION = "1.9.304.0"
5
5
  end
@@ -419,6 +419,21 @@ class Leptris::XML::Element < Leptris::XML::Node
419
419
  # C-bound insert (TODO.perf/14): gates + predicate + version
420
420
  # bump + engine insert in one dispatch; Qnil = the child needs
421
421
  # the namespace lift — the full path below handles it.
422
+ # Cross-document nodes skip the fast path entirely (1.9.304,
423
+ # #1528): the engine deep-copies them at the splice, so the
424
+ # full path's lift must run FIRST and the source original is
425
+ # removed after (move semantics).
426
+ if node.document && !node.document.equal?(@document)
427
+ ensure_writable!
428
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
429
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
430
+ end
431
+ Leptris::XML::FFI.check_status(
432
+ Leptris::XML::FFI.leptris_element_prepend_child(c_ptr, node.c_ptr))
433
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
434
+ node.parent&.remove_child(node)
435
+ return children.to_a.first
436
+ end
422
437
  if native_fast_children?
423
438
  unless Leptris::XML::Native.insert_binding_child(
424
439
  @document, @c_address, node.c_ptr.address, 1).nil?
@@ -440,6 +455,20 @@ class Leptris::XML::Element < Leptris::XML::Node
440
455
  # C-bound insert (TODO.perf/14): gates + predicate + version
441
456
  # bump + engine insert in one dispatch; Qnil = the child needs
442
457
  # the namespace lift — the full path below handles it.
458
+ # Cross-document: full path first (lift + engine copy), then
459
+ # the source original is removed; returns the installed copy
460
+ # (self's new next sibling).
461
+ if node.document && !node.document.equal?(@document)
462
+ ensure_writable!
463
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
464
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
465
+ end
466
+ Leptris::XML::FFI.check_status(
467
+ Leptris::XML::FFI.leptris_element_insert_after(c_ptr, node.c_ptr))
468
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
469
+ node.parent&.remove_child(node)
470
+ return self.next_sibling
471
+ end
443
472
  if native_fast_children?
444
473
  unless Leptris::XML::Native.insert_binding_child(
445
474
  @document, @c_address, node.c_ptr.address, 2).nil?
@@ -461,6 +490,20 @@ class Leptris::XML::Element < Leptris::XML::Node
461
490
  # C-bound insert (TODO.perf/14): gates + predicate + version
462
491
  # bump + engine insert in one dispatch; Qnil = the child needs
463
492
  # the namespace lift — the full path below handles it.
493
+ # Cross-document: full path first (lift + engine copy), then
494
+ # the source original is removed; returns the installed copy
495
+ # (self's new previous sibling).
496
+ if node.document && !node.document.equal?(@document)
497
+ ensure_writable!
498
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
499
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
500
+ end
501
+ Leptris::XML::FFI.check_status(
502
+ Leptris::XML::FFI.leptris_element_insert_before(c_ptr, node.c_ptr))
503
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
504
+ node.parent&.remove_child(node)
505
+ return self.previous_sibling
506
+ end
464
507
  if native_fast_children?
465
508
  unless Leptris::XML::Native.insert_binding_child(
466
509
  @document, @c_address, node.c_ptr.address, 3).nil?
@@ -508,6 +551,9 @@ class Leptris::XML::Element < Leptris::XML::Node
508
551
  nodes.each { |n| add_child(n) }
509
552
  return
510
553
  end
554
+ cross = nodes.any? do |n|
555
+ n.document && !n.document.equal?(@document)
556
+ end
511
557
  if native_fast_children?
512
558
  addrs = nodes.map { |n| n.c_ptr.address }
513
559
  done = Leptris::XML::Native.append_binding_children(
@@ -516,14 +562,25 @@ class Leptris::XML::Element < Leptris::XML::Node
516
562
  if done >= nodes.size
517
563
  nodes.each do |n|
518
564
  Leptris::XML::Node.invalidate_cross_document!(n, @document)
565
+ remove_from_source_original(n) if cross
519
566
  end
520
- return nodes
567
+ # 1.9.304 (#1528): foreign children were adopted by COPY —
568
+ # hand back the installed in-tree wrappers.
569
+ return cross ? children.to_a.last(nodes.size) : nodes
521
570
  end
522
571
  nodes[0, done].each do |n|
523
572
  Leptris::XML::Node.invalidate_cross_document!(n, @document)
573
+ remove_from_source_original(n) if cross
524
574
  end
525
575
  nodes = nodes[done..]
526
576
  end
577
+ if cross
578
+ nodes.each do |n|
579
+ append_installed(n)
580
+ remove_from_source_original(n)
581
+ end
582
+ return children.to_a.last(nodes.size)
583
+ end
527
584
  nodes.each { |n| add_child(n) }
528
585
  nodes
529
586
  end
@@ -565,9 +622,12 @@ class Leptris::XML::Element < Leptris::XML::Node
565
622
  # add_child moves self (unlinks from old parent first), so no explicit
566
623
  # remove_child needed — and trying to remove after the move corrupts
567
624
  # the C tree (libleptris silently handles non-child args badly).
568
- add_next_sibling(wrapper)
569
- wrapper.add_child(self)
570
- self
625
+ # The wrapper may be cross-document (parsed into a scratch doc,
626
+ # 1.9.304 #1528) — add_next_sibling returns the INSTALLED copy;
627
+ # move self into THAT.
628
+ installed = add_next_sibling(wrapper)
629
+ installed.add_child(self)
630
+ installed
571
631
  end
572
632
 
573
633
  # Deep copy in a NEW document via Document.copy_of (the single
@@ -608,6 +668,13 @@ class Leptris::XML::Element < Leptris::XML::Node
608
668
  ensure_writable!
609
669
  case node_or_markup
610
670
  when Leptris::XML::Node
671
+ # Cross-document adoption (1.9.304, #1528): the engine
672
+ # deep-copies foreign nodes at the splice — the returned
673
+ # wrapper must be the INSTALLED copy, not the detached
674
+ # source. Route those through the re-resolving path.
675
+ cross = node_or_markup.document &&
676
+ !node_or_markup.document.equal?(@document)
677
+ return append_installed(node_or_markup) if cross
611
678
  # C-bound append (TODO.perf/08-09): one dispatch runs the
612
679
  # readonly/liveness gates, the provable no-op lift
613
680
  # predicate, the version bump, and the engine append. Qnil
@@ -659,6 +726,47 @@ class Leptris::XML::Element < Leptris::XML::Node
659
726
  raise ArgumentError, "add_child expects a Node or String, got #{node_or_markup.class}"
660
727
  end
661
728
  end
729
+ # Cross-document append (1.9.304, #1528): the engine deep-copies
730
+ # the foreign node at the splice, so the installed node is this
731
+ # element's LAST child — re-wrap it and return that. The binding
732
+ # keeps its historical MOVE semantics on top: namespaces are
733
+ # lifted BEFORE the copy (declarations survive), and the source
734
+ # original is removed after (the source loses the node, as
735
+ # every pre-304 caller observed).
736
+ def append_installed(node)
737
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
738
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
739
+ end
740
+ Leptris::XML::FFI.check_status(
741
+ Leptris::XML::FFI.leptris_element_append_child(c_ptr, node.c_ptr))
742
+ Leptris::XML::Node.invalidate_cross_document!(node, @document)
743
+ remove_from_source_original(node)
744
+ last_installed
745
+ end
746
+ private :append_installed
747
+
748
+ # 1.9.304 (#1528): a foreign child appended to this document was
749
+ # adopted by COPY — remove the ORIGINAL from its source document
750
+ # (the binding's move semantics). The wrapper's @parent may be
751
+ # unset for lazily minted wrappers, so ask the ENGINE for the
752
+ # source parent and remove through a wrapped handle.
753
+ def remove_from_source_original(node)
754
+ return unless node.document && !node.document.equal?(@document)
755
+
756
+ srcp = Leptris::XML::FFI.leptris_node_parent(node.c_ptr)
757
+ return if srcp.nil? || srcp.null?
758
+
759
+ Leptris::XML::Node.wrap(srcp, node.document).remove_child(node)
760
+ end
761
+ private :remove_from_source_original
762
+
763
+ # The last child, wrapped fresh through the document's identity
764
+ # cache (bypasses any stale self-side wrapper of the same node).
765
+ def last_installed kids = children.to_a
766
+ kids.last
767
+ end
768
+ private :last_installed
769
+
662
770
  # The markup-append face appends in fragment order; the added
663
771
  # nodes are this element's last +count+ children.
664
772
  def last_added(count)
data/lib/libleptris.so CHANGED
Binary file
data/vendor-src/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Sources vendored in this gem (recompile rights; the ruby
2
2
  variant builds them at install via extconf.rb):
3
3
 
4
- - libleptris v1.9.292
4
+ - libleptris v1.9.304
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,403 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.304] - 2026-10-05
4
+
5
+ ### Fixed
6
+
7
+ - **DOM: nodes from a different document are adopted by copy, never
8
+ spliced raw (#1528)** — `leptris_element_append_child`,
9
+ `prepend_child`, `insert_before` and `insert_after` accepted a
10
+ pointer from ANY document and spliced it in as-is. A node parsed
11
+ into a scratch document (the cleanup pipelines' replacement
12
+ markup) left the live tree holding nodes and text content from
13
+ the scratch pool; freeing that scratch document dangled every
14
+ adopted node, and the next native walk crashed — the
15
+ `leptris_xpath_compiled_eval` segfault in the metanorma
16
+ sectioned-cleanup path, same family as #358/#362/#371. The four
17
+ seams now adopt foreign nodes through `leptris_element_copy`
18
+ (the `set_root` adoption contract since #371). Same-document
19
+ moves stay pointer-identical and zero-copy; detached chains keep
20
+ their link-only contract; the mutation hot path reuses its
21
+ existing document resolution. RED spec under ASAN
22
+ (`CrossDocumentAdoption` in the mutation suite: splice a parsed
23
+ scratch root, free the scratch document, then compiled-XPath and
24
+ serialize the live tree).
25
+
26
+
27
+
28
+ ## [1.9.303] - 2026-10-05
29
+
30
+ ### Added
31
+
32
+ - **XQuery: `fn:min`/`fn:max` coerce NaN** — any numeric NaN in the
33
+ input forces the NaN result; the comparison operators never select
34
+ it (`d > best` is false for NaN), so it is tracked explicitly
35
+ (K-SeqMAXFunc-16/27/28/31 and the MIN twins).
36
+ - **XQuery: duration comparisons in `fn:min`/`fn:max`/`fn:sum`/
37
+ `fn:avg`** — `xs:yearMonthDuration` items compare by months,
38
+ `xs:dayTimeDuration` by seconds, through the shared duration
39
+ formatters (`max(1 to 10 months)` = `P10M`, `cbcl-max-015` =
40
+ `P1Y1M`). Duration results of the four aggregates carry their
41
+ atomic type, so `instance of xs:*Duration` now holds
42
+ (fn-sum-1/3/4/6, fn-max/min-4/6/7, K-SeqSUMFunc-30, fn-avg-3/5).
43
+ - **XQuery: `fn:min`/`fn:max` over `xs:boolean`** — false < true,
44
+ and the result is a boolean, not a string (a string "false" would
45
+ read truthy at every assert-false site; cbcl-max-001, cbcl-min-002).
46
+
47
+ ### Fixed
48
+
49
+ - **XQuery: `fn:sum#2` zero semantics** — the zero is the
50
+ EMPTY-sequence answer only, never an addend:
51
+ `sum((1 to 3), 17)` is 6 (was 23, fn-sum-12); an empty zero
52
+ leaves the empty result empty (`sum((), ())`, K-SeqSUMFunc-5);
53
+ a non-integer numeric zero adds back only for the empty sequence.
54
+ The libxslt suite (221/221) pins the XPath 1.0 side unchanged.
55
+ - **instance of the duration types** — the `'d'`-marked member check
56
+ recovers the flavor from the lexical shape (months-only is
57
+ yearMonth; dayTime never spells months) and the scalar check
58
+ requires the matching atomic type; both branches sit ahead of the
59
+ `is_num_ty` catch-all that would otherwise swallow every
60
+ non-string/boolean/node base name.
61
+ - **fn:sum atomization leak** — the duration prescan freed the
62
+ flattened array-carrier members' container but not the member
63
+ strings (Linux ASAN caught it: 10 bytes per `sum([1,2,3,4,5])`).
64
+ - QT3 re-pins from one verified run: fn/avg 173→175, fn/max
65
+ 167→176, fn/min 167→175, fn/sum 204→209 (+24 adopted cases;
66
+ total pins 1148 → 1172; 61/61 suites green).
67
+
68
+
69
+
70
+ ## [1.9.302] - 2026-10-04
71
+
72
+ ### Added
73
+
74
+ - **XQuery: aggregates atomize array arguments** — `fn:avg`,
75
+ `fn:sum`, `fn:min` and `fn:max` take `xs:anyAtomicType*`, so a
76
+ dense array argument now contributes its members instead of
77
+ leaking the internal carrier: `avg([1,2,3,4,5])` = 3,
78
+ `sum([1,2,3,4,5])` = 15 (fn-avg-10, fn-sum-11, fn-min-19,
79
+ fn-max-19). A new shared decoder turns carriers keyed exactly
80
+ 1..n into positional members, recursing into nested arrays
81
+ (`[[1,2],[3]]` → 1,2,3); maps and sparse carriers stay single
82
+ items, and `item()`-typed functions such as `fn:count` still see
83
+ an array as one item, per XDM. `fn:sum`'s per-item accumulation
84
+ moved into a helper so atomized members feed the existing
85
+ int64-exact and xs:float channels unchanged.
86
+ - **XQuery: `instance of` over the integer family demands
87
+ integrality** — `xs:integer` and its derived types
88
+ (int/long/short/byte/unsigned*/nonNegative…) now require an
89
+ integral value on both the sequence-member and scalar paths:
90
+ `fn:abs(1.1) instance of xs:integer` is false,
91
+ `fn:abs(-3) instance of xs:integer` is true.
92
+
93
+ ### Fixed
94
+
95
+ - **XQuery: `fn:avg` edge shapes** — `avg(())` returns the empty
96
+ sequence under XQuery (XPath 1.0 parity untouched), and averages
97
+ over `xs:yearMonthDuration` format through the shared months
98
+ formatter (cbcl-avg-006: `P1Y`). Also freed a nested-array
99
+ carrier on an allocation-failure path in the atomizing decoder.
100
+ - QT3 re-pins from one verified run: fn/avg 164→173, fn/max
101
+ 165→167, fn/min 165→167, fn/sum 203→204 (+14 adopted cases;
102
+ total pins 1134 → 1148; 61/61 suites green).
103
+
104
+
105
+
106
+ ## [1.9.301] - 2026-10-04
107
+
108
+ ### Fixed
109
+
110
+ - **XQuery: a false() bound anywhere read as truthy** — values
111
+ stringified to a marker-less "false" that every effective-boolean-
112
+ value site saw as a non-empty string, so `let $x := false() return
113
+ if ($x)...` took the then-branch and fold accumulators ignored a
114
+ false() zero (fold-left-004). Variable bindings, local:function
115
+ thunk arguments and the fold accumulators/items now carry the
116
+ boolean type marker; the EBV read honors it on singleton synthetic
117
+ members (XML 1.0 forbids raw control characters in text content,
118
+ so the marker cannot collide with document data); named-reference
119
+ closure calls spell marked booleans back to literals; both string
120
+ consumers render them as true/false. (PR #1522)
121
+
122
+ - **fn:not uses the effective boolean value** — not(false()) is
123
+ true while fn:empty((false())) stays false; real nodes keep the
124
+ XPath 1.0 existence rule, so libxml2 parity is unchanged.
125
+ (PR #1522)
126
+
127
+ - **fn:abs(()) is the empty sequence** under the XQuery surface;
128
+ **fn:sum((), $zero) returns a non-numeric zero verbatim**
129
+ (sum((), xs:dayTimeDuration("PT0S")) = PT0S); **fn:avg over
130
+ dayTimeDurations** divides the total seconds and formats
131
+ (fn-avg-4: PT5H30M); and the kept lexical zero in fn:sum is freed
132
+ on every result path (a Linux ASAN catch — macOS has no leak
133
+ sanitizer). (PR #1522)
134
+
135
+
136
+
137
+ ## [1.9.300] - 2026-10-04
138
+
139
+ ### Added
140
+
141
+ - **XQuery: the xs:float and int64 typed-atom channels survive the
142
+ numeric function hop (+49 QT3 agreements; the fifteen-family batch
143
+ pins rise 1078→1127 and fn:round is fully green at 220/220)**.
144
+ fn:abs, fn:round, fn:round-half-to-even and the avg/min/max
145
+ aggregates copy the float tag onto their results (round over
146
+ xs:float("3.4028235E38") spells 3.4028235E38, not the widened
147
+ double digits); fn:sum keeps an exact long-long total when every
148
+ member is an integer lexical (fn:sum#2's zero seeds it); fn:floor,
149
+ fn:ceiling, fn:round and fn:abs pass int64 atoms through unchanged
150
+ (floor(xs:integer("999999999999999999")) stays exact, not 1e18).
151
+ (PR #1520)
152
+
153
+ - **xs:byte, xs:unsignedByte and xs:unsignedInt constructors** —
154
+ the three integer subtypes were unregistered and errored as
155
+ unknown functions. (PR #1520)
156
+
157
+ ### Fixed
158
+
159
+ - **is_int results now also carry the double field** — eq and
160
+ boolean() comparisons read number_value, which the int64 path
161
+ left unset (sum((3,4,5)) eq 12 was false). (PR #1520)
162
+
163
+ - **The float-precision speller snaps into the float domain before
164
+ spelling** — double arithmetic can leave a non-float-representable
165
+ value whose shortest float round-trip never terminates, spelling
166
+ 18-digit mantissas. (PR #1520)
167
+
168
+
169
+
170
+ ## [1.9.299] - 2026-10-04
171
+
172
+ ### Added
173
+
174
+ - **XQuery: 1078 QT3 cases adopted across fifteen fn families** —
175
+ the numeric aggregates and rounding set (abs, avg, ceiling, floor,
176
+ max, min, round, round-half-to-even, sum) plus the higher-order
177
+ combiners (apply, filter, for-each, for-each-pair, fold-left,
178
+ fold-right), vendored verbatim from the upstream QT3 mirror. The
179
+ named per-suite excludes document the typed-atom lane's remaining
180
+ levers (xs:float spelling through aggregates, int64-exact
181
+ multi-argument sums, instance-of results). (PR #1518)
182
+
183
+ - **fn:sum#2** — the zero argument returns its value for the empty
184
+ sequence: fn:sum((1,2,3), 10) = 16, fn:sum((), 5) = 5.
185
+ (PR #1518)
186
+
187
+ ### Fixed
188
+
189
+ - **Number spelling destroyed integral magnitudes** — the shared
190
+ trailing-zero trim ran on dot-free integer renderings, so 1e18
191
+ spelled "1"; it now runs only when a decimal point is present.
192
+ Negative zero also spells "-0" (fn:round(-0.01) = -0), and
193
+ fn:round preserves -0 for inputs in (-0.5, 0). (PR #1518)
194
+
195
+ - **Value carriers overflowed at DBL_MAX** — the ext31 string
196
+ carrier used the libxml2-parity printer, which renders DBL_MAX
197
+ with 18 significant digits; strtod read-back of that string
198
+ overflows to infinity, so fn:avg((1.7976931348623157E308)) was
199
+ INF. Carriers now use the injective shortest-round-trip spelling.
200
+ (PR #1518)
201
+
202
+ - **fn:sum rejected non-nodeset arguments** — single-item sequences
203
+ (passed as scalars by the XQuery adapter) errored out. (PR #1518)
204
+
205
+ - **fn:round()/fn:round-half-to-even(()) now return the empty
206
+ sequence** under the XQuery surface; XPath 1.0 coercion stays NaN
207
+ (libxml2 parity unchanged). (PR #1518)
208
+
209
+ - **fn:max/fn:min over non-numeric items** (xs:string, xs:anyURI)
210
+ compare in codepoint order and return the extreme item instead of
211
+ coercing to zero. (PR #1518)
212
+
213
+ - **QT3 runner: the standard `emptydoc`/`empty` environments
214
+ resolve to the empty context** — cases carrying them were
215
+ silently skipped (the abs/ceiling/floor/fold-* families adopted
216
+ zero cases before this). (PR #1518)
217
+
218
+
219
+
220
+ ## [1.9.298] - 2026-10-04
221
+
222
+ ### Added
223
+
224
+ - **XPath/XQuery: the six remaining string functions honor UCA
225
+ collations (DUCET lane closed)** — starts-with, ends-with,
226
+ contains, substring-before, substring-after and deep-equal now
227
+ route `http://www.w3.org/2013/collation/UCA` URIs through the
228
+ collation module. Matching scans codepoint-aligned windows under
229
+ the collation compare (every collation-unit boundary falls
230
+ between codepoints, so the scan is exact across contractions and
231
+ normalization); substring-before/after slice the leftmost-shortest
232
+ occurrence. deep-equal threads the collation into plain string
233
+ atoms and map values, while \x03-marked carriers keep their
234
+ value-space equality. **QT3 adoption rises by 73 cases**:
235
+ contains 46→63, starts-with 43→56, ends-with 34→48,
236
+ substring-before 12→26, substring-after 16→29, deep-equal
237
+ 221→223. (PR #1515)
238
+
239
+ - **Collation: UCA 7.1.1 implicit weights** — codepoints with no
240
+ DUCET mapping sort by the two-CE algorithm: the registered
241
+ @implicitweights ranges (Tangut/Tangut-Supplement base FB00,
242
+ Khitan FB02, Nushu FB01) order ahead of the unassigned groups
243
+ (base 0xFB80 + 2*(cp>>15)), each codepoint emitting an
244
+ even/odd CE pair that orders within its group by codepoint.
245
+ (PR #1515)
246
+
247
+ - **Collation: `alternate=blanked`** (fn-contains-35..38) — the
248
+ third W3C UCA variable-weighting value is accepted, and variable
249
+ collation elements now leave every ordinary level under
250
+ shifted/blanked weighting (shifted retains its quaternary
251
+ re-add; blanked drops them wholly — ICU variable semantics).
252
+ (PR #1515)
253
+
254
+ - **CLI: `leptris xslt` command** — apply a stylesheet to a
255
+ document and print the serialized result tree:
256
+ `leptris xslt -s style.xsl input.xml`. Conventions match
257
+ xsltproc: INPUT is a file path, `-` or absent reads stdin. The
258
+ stylesheet compiles via leptris_xslt_parse_file, so
259
+ xsl:include/xsl:import resolve relative to the sheet. New
260
+ CLI_ERROR_XSLT exit code (7). (PR #1516)
261
+
262
+ - **CLI: man pages for every command** — new leptris-xslt.1 plus
263
+ the missing leptris-xquery.1, leptris-diff.1 and
264
+ leptris-validate.1, all registered for generation. (PR #1516)
265
+
266
+ ### Fixed
267
+
268
+ - **CLI: `--help` was missing registered commands** — the usage
269
+ text hand-maintained its own command list, so the registered
270
+ `diff` and `xquery` commands were invisible. The help listing
271
+ now iterates the command registry (the descriptors are the
272
+ single source of truth), and adding a command can no longer
273
+ leave help stale. xquery's private stream reader is promoted to
274
+ shared cli_read_stream/cli_read_file. (PR #1516)
275
+
276
+
277
+
278
+ ## [1.9.297] - 2026-10-03
279
+
280
+ ### Fixed
281
+
282
+ - **XQuery: string lt/gt are codepoint value comparisons** — the
283
+ shared XPath 1.0 comparison numeric-coerced strings, so two
284
+ collation-keys that both spell numbers compared as equal numbers.
285
+ Both comparison sites now codepoint-compare string operands when
286
+ the evaluation is XQuery-flavored (the evaluator's string branch
287
+ and the VM twin); XPath 1.0 evaluation is unchanged.
288
+ (PR #1513)
289
+
290
+ - **XQuery: fn:codepoints-to-string encodes every item of its
291
+ sequence argument** — the collector collapsed a parenthesized
292
+ sequence to one scalar (its space-joined spelling), so
293
+ (37, 65500, 37) encoded as just the first codepoint. Collation-key
294
+ adoption rises to 12/56 run-and-agree. (PR #1513)
295
+
296
+
297
+
298
+ ## [1.9.296] - 2026-10-03
299
+
300
+ ### Added
301
+
302
+ - **XPath/XQuery: 71 QT3 collation cases adopted (DUCET slice 4)** —
303
+ the misc/UCACollation corpus (62 cases: the UCA ordering battery
304
+ over DUCET weights) and fn/collation-key (9 cases). The collation
305
+ URI grammar now parses the W3C semicolon-separated parameter form,
306
+ accepts lang= (DUCET is the root locale), and applies
307
+ caseFirst=upper|lower by swapping the DUCET case tertiaries.
308
+ fn:collation-key gains its 1-argument form (the static default
309
+ collation; codepoint keys are the UTF-8 bytes) and its keys are
310
+ lowercase hex — injective AND order-preserving, so lt/gt on two
311
+ keys is byte-wise key order. The QT3 runner now credits an
312
+ error alternative inside any-of for a failed evaluation, gates the
313
+ optional simple-uca-fallback feature (this engine raises FOCH0002
314
+ per the strict F&O reading), and lifts the pre-DUCET
315
+ collation/UCA exclusion under a DUCET build. (PR #1511)
316
+
317
+ ### Fixed
318
+
319
+ - the DUCET implicit-weight fallback dropped a mask that broke
320
+ ordering between high codepoints. (PR #1511)
321
+
322
+
323
+
324
+ ## [1.9.295] - 2026-10-03
325
+
326
+ ### Added
327
+
328
+ - **XPath: collation-aware functions over the vendored DUCET
329
+ (slices 2-3)** — fn:compare's 3-argument form honors its collation
330
+ URI (unknown needed collations raise FOCH0002; unknown-but-unneeded
331
+ ones are ignored per the QT3 any-of expectations);
332
+ fn:collation-key($key, $collation) is new, returning the UCA sort
333
+ key as base64 text (the result-type enum is ABI-frozen with no
334
+ binary kind; base64 is injective, so key equality is byte equality);
335
+ fn:index-of, fn:distinct-values and fn:sort route plain-string
336
+ comparisons through the collation while typed atoms keep their
337
+ value-space equality. DUCET-gated: builds without utf8proc keep the
338
+ previous behavior. (PRs #1508, #1509)
339
+
340
+
341
+
342
+ ## [1.9.294] - 2026-10-03
343
+
344
+ ### Added
345
+
346
+ - **XPath: UCA collation over the vendored DUCET table (slice 1)** —
347
+ `http://www.w3.org/2013/collation/UCA` collation URIs parse
348
+ `strength` and `alternate` parameters per F&O 3.x (defaults:
349
+ tertiary, non-ignorable) and compare through multi-level sort keys:
350
+ NFD (utf8proc) → DUCET weights (Unicode 16.0.0, 38,443 single
351
+ codepoints + 964 contractions, vendored with an offline table
352
+ generator) → primary/secondary/tertiary levels with 0x00
353
+ separators; `alternate=shifted` demotes variable primaries to the
354
+ quaternary level. Codepoint and html-ascii-case-insensitive
355
+ collations keep their semantics. Unmapped codepoints get a coarse
356
+ deterministic fallback (the full UCA 7.1.1 implicit-weight
357
+ algorithm is a later slice). New CMake option
358
+ `LEPTRIS_ENABLE_DUCET` (default on, requires utf8proc; builds
359
+ without utf8proc degrade to codepoint collation). Function
360
+ wiring (fn:compare, fn:collation-key, default collation) and
361
+ QT3 UCA case adoption are the next slices. (PR #1506)
362
+
363
+ - **XQuery: fn:normalize-unicode F&O conformance** — the
364
+ single-argument form normalizes with NFC instead of returning the
365
+ input unchanged; unknown normalization forms raise FOCH0003
366
+ instead of silently falling back to NFC; the form argument is
367
+ case-insensitive with surrounding whitespace tolerated
368
+ (FULLY-NORMALIZED is gone from F&O 3.x; its satisfied="false"
369
+ twins run). utf8proc-gated end to end; the 29-run
370
+ fn/normalize-unicode QT3 family is adopted. (PR #1505)
371
+
372
+ ### Fixed
373
+
374
+ - DUCET builds without utf8proc (cppcheck leg, Rust lane, ILP32,
375
+ Windows static) degrade to codepoint collation with a warning
376
+ instead of failing the configure. (PR #1506)
377
+
378
+
379
+
380
+ ## [1.9.293] - 2026-10-03
381
+
382
+ ### Performance
383
+
384
+ - DOM: per-thread recycle of mutation blocks. Document create/build/free
385
+ cycles now park their elem (45KB), name (4KB) and attr (5KB) blocks on
386
+ thread-local free-lists instead of returning them to malloc — the next
387
+ cycle carves from hot pages instead of re-faulting fresh ones. Byte-capped
388
+ (8MB elem, 4MB name/attr); POSIX threads drain via a pthread_key
389
+ destructor at thread exit, and `leptris_explicit_cleanup()` drains
390
+ synchronously. Mutation doc-cycle row: 19.4 → 15.1 ns/child (−22%), gap
391
+ to pugixml on that shape narrows 1.66x → 1.29x. Single-document append
392
+ throughput is unchanged. (PR #1503)
393
+
394
+ ### Rust bindings
395
+
396
+ - First fully tokenless crates.io release: leptris v1.4.1 published from CI
397
+ via crates.io Trusted Publishing (`rust-lang/crates-io-auth-action` —
398
+ cargo itself has no native OIDC support). The Rust release lane now needs
399
+ no registry token of any kind. (PRs #1497, #1499–#1502)
400
+
3
401
  ## [1.9.292] - 2026-10-02
4
402
 
5
403
  ### Added