leptris 1.9.308.0-ppc64le-linux → 1.9.311.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cfc66df50629142cbe39e40a9ceff903b6e6faa20d73bf7069dfe1cfe3ac348
4
- data.tar.gz: d4c0ab543e0415728ea55245e907be8897bde8d4237d89459fda7926e41cf742
3
+ metadata.gz: 253cc4928e73c6151db3426cec6564a0c1e9b7e6d735921210de3498d07c9f80
4
+ data.tar.gz: 944c12f01b50a20fb5c909532467488a83dd94554465b1b55126f48f608c2a63
5
5
  SHA512:
6
- metadata.gz: f735355268e9665d9ad5cc54d0467f8c4985811f41e42cbfd0407210812e1e5d8b5e15dd7c13f83753f2cbb612e29db574b67785d5f5b4b3372d576de359d54b
7
- data.tar.gz: 6eac52f373adc8c87cd2e90c78afa30f47a2ad8b193254d38365f90148d8ef9fa03f802f7c841accd0d443f5ec9c2fce7c158368f8343f94710f04a909b63215
6
+ metadata.gz: a459b9b1dd995cd2cb553c0941f1741a308aa585351eaa2cde3034ed4fcdda7f5eb3c5100d58603cc1d8046995892b5d7b7ea723e198277ae03423386aac6146
7
+ data.tar.gz: 2ecc975d6e07f68bc76d458ba62e3ba4fffe97c577bb2c7f24e41896729a9049a3bb7cccb0d26d23274dc3a237e40da2ea64ee70a15bf3d082e93a1f5646caf4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,46 @@
1
+ ## [1.9.311.0] - 2026-10-06
2
+
3
+ ### Added
4
+
5
+ - **Document#absorb — move semantics for doomed splice sources
6
+ (libleptris 1.9.311, engine #1548)**: one call transfers a
7
+ scratch document's pool ownership into the destination, so
8
+ subsequent cross-document splices of its nodes move BY REFERENCE
9
+ — O(1), no deep copy, no live-set doubling (the sectioned-
10
+ cleanup flow's 7GB cg3 overrun; completes without the copy
11
+ blow-up). The absorbed document's #free is handle-only (the
12
+ destination's free performs the real release); splices into any
13
+ THIRD document still deep-copy; chaining rejects. The splice
14
+ seams skip source-side removal for absorbed sources (the move
15
+ already transferred the node — nothing to remove).
16
+ leptris_document_absorb attached (audit 370/369).
17
+
18
+ ### Fixed — engine sync (1.9.309-1.9.311)
19
+
20
+ - 1.9.309: XPath signed exponent literals (`1E+1` truncated at
21
+ the E; the sign is part of the literal now).
22
+ - 1.9.310: a typed cast stored the operator frame's stack buffer
23
+ in the result's borrowed atomic_type slot (ASAN stack-use-after-
24
+ return); recursive closures sharing parameter names with an
25
+ outer frame no longer drop the outer bindings.
26
+
27
+ ## [1.9.308.0] - 2026-10-05
28
+
29
+ ### Engine sync (libleptris 1.9.308)
30
+
31
+ - **QT3 #1182 tail (leptris#1540)** — the 13 lever cases:
32
+ fn:avg's numeric promotion ladder with int64-exact decimal
33
+ division, the O(xs:double) carrier mark, typeswitch case `$v as
34
+ TYPE` binding arms, fn:apply function conversion (zero-arity,
35
+ atomization, sequence members), and fn:sum's per-type canonical
36
+ duration zero. Full QT3 61/61.
37
+ - **Per-engine crates, step 1 (leptris#1541)** — the shared
38
+ leptris-descriptor ABI crate in a new Cargo workspace, with the
39
+ runtime layout gate (Rust size_of vs engine introspection +
40
+ build/walk round trip) and the build.rs stale-link cache fix.
41
+ Ruby-side impact: none (Rust-side surface; zero header deltas,
42
+ audit unchanged 369/368, suite 866/0).
43
+
1
44
  ## [1.9.307.0] - 2026-10-05
2
45
 
3
46
  ### 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.308"
11
+ LIBLEPTRIS_VERSION = "1.9.311"
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.308.0"
4
+ VERSION = "1.9.311.0"
5
5
  end
@@ -561,10 +561,51 @@ class Leptris::XML::Document
561
561
  end
562
562
  alias_method :c14n, :canonicalize
563
563
 
564
+ # Move semantics for doomed splice sources (1.9.311, engine
565
+ # #1548): transfer +other+'s pool ownership into this document
566
+ # in one call. Every subsequent cross-document splice of other's
567
+ # nodes into THIS document moves by reference — O(1), no deep
568
+ # copy, no live-set doubling (the sectioned-cleanup flow's 7GB
569
+ # cg3 overrun). other's handle becomes handle-only: its #free
570
+ # releases just the wrapper; this document's free performs the
571
+ # real release. Chaining rejects (an absorbed document cannot
572
+ # absorb or be absorbed again). Returns self.
573
+ def absorb(other)
574
+ raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
575
+ unless other.is_a?(Leptris::XML::Document)
576
+ raise ArgumentError, "absorb expects a Leptris::XML::Document"
577
+ end
578
+
579
+ Leptris::XML::FFI.check_status(
580
+ Leptris::XML::FFI.leptris_document_absorb(c_ptr, other.c_ptr))
581
+ other.mark_absorbed_into(self)
582
+ self
583
+ end
584
+
585
+ # Absorption bookkeeping (private seam — never set ivars across
586
+ # objects by hand): the absorbed document's #free skips the
587
+ # engine release, and splice seams skip source-side removals
588
+ # because absorbed splices MOVE the node (nothing to remove).
589
+ def mark_absorbed_into(destination)
590
+ @absorbed_into = destination
591
+ end
592
+
593
+ def absorbed_into?(destination)
594
+ @absorbed_into&.equal?(destination)
595
+ end
596
+
597
+ def absorbed?
598
+ !@absorbed_into.nil?
599
+ end
600
+ private :absorbed?
564
601
  def free
565
602
  return if @freed.state == :freed
566
603
  @freed.state = :freed
567
- Leptris::XML::FFI.leptris_document_free(c_ptr) unless c_ptr.nil?
604
+ # An absorbed source's pool is owned by its destination — this
605
+ # free is handle-only; the destination's free releases memory.
606
+ unless absorbed? || c_ptr.nil?
607
+ Leptris::XML::FFI.leptris_document_free(c_ptr)
608
+ end
568
609
  @c_ptr = nil
569
610
  @c_address = nil
570
611
  @wrapper_cache&.clear
@@ -752,6 +752,11 @@ class Leptris::XML::Element < Leptris::XML::Node
752
752
  # source parent and remove through a wrapped handle.
753
753
  def remove_from_source_original(node)
754
754
  return unless node.document && !node.document.equal?(@document)
755
+ # An ABSORBED source's splices move by reference (engine
756
+ # #1548): the node is already owned by this document — there is
757
+ # no source-side original to remove, and the engine parent is
758
+ # this tree's own parent.
759
+ return if node.document.absorbed_into?(@document)
755
760
 
756
761
  srcp = Leptris::XML::FFI.leptris_node_parent(node.c_ptr)
757
762
  return if srcp.nil? || srcp.null?
@@ -490,6 +490,14 @@ attach_function :leptris_parse_string,
490
490
  [:string, :pointer], :pointer
491
491
  attach_function :leptris_document_free,
492
492
  [:leptris_document], :void
493
+ # 1.9.311 (#1548): move semantics for doomed splice sources —
494
+ # one call transfers a source document's pool ownership into
495
+ # the destination, so subsequent cross-document splices move
496
+ # nodes by reference (O(1), no deep copy, no live-set
497
+ # doubling). The caller's src handle becomes handle-only;
498
+ # dst's free performs the real release. Chaining rejects.
499
+ attach_function :leptris_document_absorb,
500
+ [:leptris_document, :leptris_document], :leptris_status
493
501
  attach_function :leptris_document_create,
494
502
  [], :leptris_document
495
503
  attach_function :leptris_document_set_root,
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.308
4
+ - libleptris v1.9.311
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,109 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.311] - 2026-10-06
4
+
5
+ ### Added
6
+
7
+ - **`leptris_document_absorb(dst, src)` — move semantics for doomed
8
+ splice sources (#1548).** The #1528 cross-document adoption gate
9
+ deep-copies every foreign splice, doubling the live set; the
10
+ sectioned-semantic cleanup flow (scratch-document splices) pushed
11
+ the cg3 corpus past a 7GB budget (fatal alloc ~396s; completes at
12
+ 16GB). One call before the splices transfers a doomed source
13
+ document's lifetime into the destination (riding the existing
14
+ xi:include ownership chain): the adoption gate then moves nodes by
15
+ reference — O(1) per seam, no copy, no live-set doubling, and
16
+ existing splice code is unchanged. The caller's later
17
+ `leptris_document_free(src)` releases only the handle; the
18
+ destination's free performs the real release. Splicing an absorbed
19
+ document's nodes into any OTHER document still deep-copies, and
20
+ the no-absorb copy contract is unchanged. No chaining: an
21
+ absorbed document cannot absorb or be absorbed again.
22
+
23
+
24
+
25
+ ## [1.9.310] - 2026-10-06
26
+
27
+ ### Fixed
28
+
29
+ - **Memory safety: a typed cast stored the operator frame's stack
30
+ buffer in the result's borrowed `atomic_type` slot — read after
31
+ return by the sequence fold (ASAN stack-use-after-return, caught
32
+ by Linux CI on this very batch).** Cast targets now assign
33
+ lifetime-stable family-table labels.
34
+ - **Recursive closures: a nested call sharing parameter names with
35
+ an outer frame removed the outer frame's bindings on exit —
36
+ `$a` vanished mid-recursion.** Both dynamic-call sites
37
+ detach/save the prior nodeset and restore it (fold-left/
38
+ fold-right-101..104: the spec'd recursive folds over `tail()`).
39
+
40
+ ### Added
41
+
42
+ - **XQuery closure-call carriers, end-to-end — 19 more QT3
43
+ adoptions (for-each-010, for-each-pair-024/026/027, fold-left
44
+ -019/020/021/101..104, fold-right-013/019/020/101..104,
45
+ K2-SeqReverseFunc-1, cbcl-distinct-values-002).**
46
+ - `declare function` parameter types skip depth-aware — a
47
+ function-test type's own commas
48
+ (`$f as function(item()*, item()) as item()*`) broke the
49
+ parameter list; inline function literals accept `as TYPE`
50
+ annotations on params and the return.
51
+ - Callback arguments and accumulators keep their shape across
52
+ every hop: sequences ride the member carrier, empty sequences
53
+ bind zero nodes (`empty($acc)` on a `()` zero is true — the
54
+ average fold yields 8.625, min folds return 1), booleans stay
55
+ falsy through variable round-trips (and-folds return false).
56
+ - `xs:untypedAtomic` rides its own family mark: value-eq casts
57
+ it to the counterpart (cbcl-002 matches Saxon byte-for-byte)
58
+ while a plain `xs:string` never merges with typed values.
59
+ - QName tail: `namespace-uri-from-QName` resolves prefixed
60
+ lexical QNames in the static namespaces (`xs:int#1` → the
61
+ XMLSchema namespace); `castable/cast as xs:QName` accept
62
+ NCName:NCName lexicals; QName eq compares full lexicals.
63
+ - Range sequences and nodeset growth admit 1,000,000 items —
64
+ `fold-left(1 to 1000000, 0, ...)` computes the full million
65
+ (previously truncated at exactly 524288 items).
66
+ - Remaining QT3 pins are down to three, each documented in the
67
+ harness: fold-left-009/016 (node-typed accumulators must be
68
+ real nodes for path/identity shapes) and cbcl-002b (XQ10 vs
69
+ XQ30 value-eq contradiction).
70
+
71
+
72
+ ## [1.9.309] - 2026-10-05
73
+
74
+ ### Fixed
75
+
76
+ - **XPath lexer: signed exponent literals truncated at the `E` —
77
+ `1E+1` evaluated to `2` and `3.567812E+3 eq 3567.812` was false.**
78
+ The number scanner consumed the exponent digits but not its sign,
79
+ so `1E+1` tokenized as `1E` followed by `+ 1`. The sign is now part
80
+ of the literal (also `5.2E-3` and friends).
81
+
82
+ ### Added
83
+
84
+ - **XQuery numeric literal typing (XQuery 3.1 §3.13.1) with
85
+ end-to-end type discrimination — 17 more QT3 cases adopted
86
+ (abs +5, ceiling +3, floor +3, round-half-to-even +6), 32 with the
87
+ same-batch string-to-codepoints and min/max gates.**
88
+ - Exponent literals are `xs:double`, decimals with a point are
89
+ `xs:decimal`, plain integers `xs:integer`; `1.1 instance of
90
+ xs:double` is now `false` per spec.
91
+ - `instance of` / `typeswitch` match labeled numeric results by
92
+ XDM derivation (`xs:integer` <: `xs:decimal`); `case $v as TYPE`
93
+ arms bind their variable inside the arm. Unlabeled numerics keep
94
+ the historical any-numeric match — no XPath 1.0 surface change.
95
+ - `fn:abs` preserves its argument's numeric type (integer/decimal/
96
+ float/double, float through float32); `fn:ceiling`/`fn:floor`
97
+ label float results; `fn:round-half-to-even` computes its scale
98
+ in double (no `int` truncation UB at 2^32) and guards huge
99
+ scales; `fn:min`/`fn:max` label results by promoted input type;
100
+ FLWOR bindings preserve float/decimal/double marks.
101
+ - Vendor corpus: `fn/string-to-codepoints.xml` (+28), `fn/min`
102
+ and `fn/max` +1 each.
103
+ - Remaining pinned QT3 reds are documented by lever in the test
104
+ harness (HOF node-carrier, map iteration order, typed grouping).
105
+
106
+
3
107
  ## [1.9.308] - 2026-10-05
4
108
 
5
109
  ### Added
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.308
6
+ VERSION 1.9.311
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -1068,6 +1068,31 @@ LEPTRIS_API void leptris_document_free(LeptrisDocument doc);
1068
1068
  LEPTRIS_API void leptris_document_adopt_child(LeptrisDocument parent,
1069
1069
  LeptrisDocument child);
1070
1070
 
1071
+ /**
1072
+ * Absorb a DOOMED source document into the destination: the source's
1073
+ * node pool is released with the destination, so cross-document
1074
+ * splices from source to destination move nodes by reference instead
1075
+ * of deep-copying (#1548 — the #1528 adoption copy doubled the live
1076
+ * set on scratch-document splice flows).
1077
+ *
1078
+ * After a successful absorb the caller must treat `src` as consumed:
1079
+ * its tree may be spliced into `dst` (zero-copy), and the caller's
1080
+ * leptris_document_free(src) releases only the handle — the memory
1081
+ * dies with `dst`. Splicing an absorbed document's nodes into any
1082
+ * OTHER document still deep-copies (#1528 semantics).
1083
+ *
1084
+ * @param dst Destination document (survives; owns src's pool)
1085
+ * @param src Doomed source document (handle consumed)
1086
+ * @return LEPTRIS_OK; LEPTRIS_ERROR_NULL_ARG on NULL arguments;
1087
+ * LEPTRIS_ERROR_INVALID_ARG when src == dst or src is
1088
+ * already absorbed (chaining is not supported).
1089
+ *
1090
+ * Memory: src's pool ownership transfers to dst. The caller's src
1091
+ * handle becomes handle-only.
1092
+ */
1093
+ LEPTRIS_API LeptrisStatus leptris_document_absorb(LeptrisDocument dst,
1094
+ LeptrisDocument src);
1095
+
1071
1096
  /**
1072
1097
  * Get root element of document
1073
1098
  *
@@ -416,6 +416,10 @@ static LeptrisNode* adopt_for_splice(LeptrisNode* node,
416
416
  src = leaf_owner_doc(top);
417
417
  }
418
418
  if (!src || src == target) return node;
419
+ /* #1548: the source is a doomed scratch doc absorbed into
420
+ * the target — its pool dies with the target, so the node
421
+ * moves by reference. */
422
+ if (src->absorbed_by == target) return node;
419
423
  LeptrisNode* copy = copy_leaf_chain(node, target);
420
424
  return copy;
421
425
  }
@@ -430,6 +434,10 @@ static LeptrisNode* adopt_for_splice(LeptrisNode* node,
430
434
  struct leptris_document* src =
431
435
  leptris_element_get_document((LeptrisElement)top);
432
436
  if (!src || src == target) return node;
437
+ /* #1548: the source is a doomed scratch doc absorbed into
438
+ * the target — its pool dies with the target, so the subtree
439
+ * moves by reference (O(1), no live-set doubling). */
440
+ if (src->absorbed_by == target) return node;
433
441
  LeptrisElement copy =
434
442
  leptris_element_copy((LeptrisElement)node, target);
435
443
  return copy ? (LeptrisNode*)copy : NULL;
@@ -884,6 +884,24 @@ LEPTRIS_API LeptrisDocument leptris_parse_file(const char* filepath, LeptrisStat
884
884
  /**
885
885
  * Free document and all its contents
886
886
  */
887
+ LEPTRIS_API LeptrisStatus leptris_document_absorb(LeptrisDocument dst,
888
+ LeptrisDocument src) {
889
+ if (!dst || !src) return LEPTRIS_ERROR_NULL_ARG;
890
+ if (dst == src) return LEPTRIS_ERROR_INVALID_ARG;
891
+ /* No chains: an absorbed document's lifetime already belongs to
892
+ * its absorber; re-absorbing would give one pool two owners. */
893
+ if (src->absorbed_by || dst->absorbed_by)
894
+ return LEPTRIS_ERROR_INVALID_ARG;
895
+
896
+ /* Ride the xi:include child_docs chain (TODO 117): document_free
897
+ * releases adopted children with the parent, AFTER its own tree
898
+ * references are gone — exactly the #1548 lifetime. */
899
+ leptris_document_adopt_child(dst, src);
900
+ src->absorbed_by = dst;
901
+ src->absorbed_handle = 1;
902
+ return LEPTRIS_OK;
903
+ }
904
+
887
905
  LEPTRIS_API void leptris_document_adopt_child(LeptrisDocument parent,
888
906
  LeptrisDocument child) {
889
907
  if (!parent || !child) return;
@@ -913,6 +931,14 @@ LEPTRIS_API void leptris_document_free(struct leptris_document* doc) {
913
931
  }
914
932
  if (!doc) return;
915
933
 
934
+ /* #1548: an absorbed document's MEMORY belongs to its absorber
935
+ * (it rides the absorber's child_docs chain). A caller free
936
+ * releases only the outstanding handle. */
937
+ if (doc->absorbed_by) {
938
+ doc->absorbed_handle = 0;
939
+ return;
940
+ }
941
+
916
942
  /* Decrement reference count */
917
943
  if (doc->ref_count > 0) {
918
944
  doc->ref_count--;
@@ -995,6 +1021,11 @@ LEPTRIS_API void leptris_document_free(struct leptris_document* doc) {
995
1021
  * doesn't try to walk the
996
1022
  * adopted subtree. */
997
1023
  ch->root = NULL;
1024
+ /* #1548: the anchor's free is the REAL one — clear the
1025
+ * absorbed state so the early handle-return above does
1026
+ * not swallow it. */
1027
+ ch->absorbed_by = NULL;
1028
+ ch->absorbed_handle = 0;
998
1029
  leptris_document_free(ch);
999
1030
  ch = next;
1000
1031
  }
@@ -179,10 +179,22 @@ struct leptris_document {
179
179
  * copies / analyze-string result trees, released with THIS
180
180
  * document so borrowed nodes live exactly as long as the
181
181
  * source tree (the ctx owned_docs chain dies at public-eval
182
- * return and cannot serve plain leptris_xpath_eval results). */
182
+ * return and cannot serve plain leptris_xpath_eval results).
183
+ * Absorbed documents (#1548) ride the SAME chain: a doomed
184
+ * scratch doc's pool is released with the absorbing document,
185
+ * letting the splice adoption gate move nodes by reference. */
183
186
  struct leptris_document** anchored_docs;
184
187
  size_t n_anchored_docs;
185
188
  size_t cap_anchored_docs;
189
+ /* #1548 absorb-adoption: set by leptris_document_absorb on the
190
+ * SOURCE. absorbed_by owns this document's lifetime (it is in
191
+ * absorbed_by->anchored_docs); cross-document splice adoption
192
+ * into absorbed_by moves nodes by reference instead of
193
+ * copying. absorbed_handle counts outstanding CALLER handles:
194
+ * the caller's leptris_document_free releases only the handle
195
+ * (the anchor frees the memory later). */
196
+ struct leptris_document* absorbed_by;
197
+ int absorbed_handle;
186
198
  /* XML Declaration support */
187
199
  char* xml_version; /* "1.0", "1.1", etc. or NULL if not present */
188
200
  int standalone; /* -1=not set, 0=no, 1=yes */
@@ -650,7 +650,7 @@ void xpath_nodeset_add(XPathNodeSet* nodeset, void* node) {
650
650
 
651
651
  /* SAFETY: Validate nodeset structure before reallocation
652
652
  * Corruption in count/capacity can cause heap corruption during realloc */
653
- if (nodeset->count > nodeset->capacity || nodeset->capacity > 1000000) {
653
+ if (nodeset->count > nodeset->capacity || nodeset->capacity > 4000000) {
654
654
  /* Corrupted structure - skip this addition */
655
655
  return;
656
656
  }
@@ -659,8 +659,11 @@ void xpath_nodeset_add(XPathNodeSet* nodeset, void* node) {
659
659
  if (nodeset->count >= nodeset->capacity) {
660
660
  size_t new_capacity = nodeset->capacity == 0 ? 4 : nodeset->capacity * 2;
661
661
 
662
- /* SAFETY: Check for overflow */
663
- if (new_capacity > 1000000) {
662
+ /* SAFETY: Check for overflow. 4M admits the corpus's
663
+ * million-item range folds (`1 to 1000000` needs a
664
+ * power-of-two capacity ABOVE 1M — the old 1M bound
665
+ * truncated the sequence at exactly 524288). */
666
+ if (new_capacity > 4000000) {
664
667
  return;
665
668
  }
666
669
 
@@ -973,6 +976,10 @@ static struct leptris_xpath_result* evaluate_literal(XPathContext* ctx,
973
976
  struct leptris_xpath_result* result = xpath_result_new(XPATH_RESULT_NUMBER);
974
977
  if (!result) return NULL;
975
978
  result->value.number_value = ast->number_value;
979
+ /* XQuery literal typing from the lexical (the parser
980
+ * stashed "xs:decimal"/"xs:double" in value; integers
981
+ * stay unlabeled). */
982
+ result->atomic_type = ast->value;
976
983
  return result;
977
984
  }
978
985