leptris 1.9.380.2-arm-linux → 1.9.381.0-arm-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: db3ec4fdfeddcf213dff77dd14341bffd1dad711ea60877e08cdd0adad3fb05c
4
- data.tar.gz: cfff61372aebafdcb8911bfc554107cc5816a03beca575fc50c3c5640a08f5d0
3
+ metadata.gz: a14d1d324ee4925bbeb41aed36f0efaf3ccaed6ce6a89c1c643460eccfbd56a1
4
+ data.tar.gz: 359ee93d64dca7b0f6de0d9e6028da147fa19aef034c9009663f23260fbcc34e
5
5
  SHA512:
6
- metadata.gz: 0d11c5af4528032ee1b067bd7433dad8ad7b4f4cce9e986794ecf8f5075ab1f7307e349c8cc1e0b76b1d325f4792ba56fd75d0f043c8032f1a55572ab010e7f1
7
- data.tar.gz: 807a7f8407a86c141810f4ec074ef473ef8bdf846d3441e752239ccfaac27d0712de57c4fa628504e86bdcd63e19fef1ecc38d5650888e4e82531e09b2c916fa
6
+ metadata.gz: b65285e8a95fd881703df532d3c5196043c56d20c6f7576cb481ab2097a3014b79429b484f30f3ce46846237f34d22dbf59838414b2c36c0e7367991d6385469
7
+ data.tar.gz: 3b866259b7b0e2e5c82196bbdf89dc8d3dffa43e603d32f96f55e24c71614deb7637af0c7463d7ed8ff2b94665ac0674fc937b56b4cbe4e0fdb57dcf729d5b51
data/CHANGELOG.md CHANGED
@@ -17,6 +17,14 @@
17
17
  parameters and marks all-native results; 1.9.380.1 shipped before
18
18
  the PR merged, so the face rides in 1.9.380.2.
19
19
 
20
+ ## [1.9.381.0] - 2026-10-22
21
+
22
+ Ride: libleptris 1.9.381 — the XSLT identity/copy-of quadratic fix
23
+ (#1839): identity transforms 13.0 s -> 57 ms on a 20k-book catalog;
24
+ every CLI row now beats xsltproc end-to-end. The Rakefile pin moves
25
+ 1.9.380 -> 1.9.381 in the same commit so the platform gems vendor
26
+ this core.
27
+
20
28
  ## [1.9.379.3] - 2026-10-22
21
29
 
22
30
  ### 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.380"
11
+ LIBLEPTRIS_VERSION = "1.9.381"
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
@@ -2999,18 +2999,41 @@ static void *create_elem_core(void *doc_ptr, void *parent,
2999
2999
  return elem;
3000
3000
  }
3001
3001
 
3002
+ /* leptris-ruby#539: the ATTACHED construction faces are
3003
+ * wrapper-visible mutations — they splice a built child under a
3004
+ * parent — so they run the same freed/readonly gates and version
3005
+ * bump as every other write face (bump before the call: a failed
3006
+ * mutation merely discards memos). The raw-address face keeps the
3007
+ * legacy Integer-document contract (moxml's pre-#340 callers pass
3008
+ * doc.c_address): a bare engine address cannot reach the Document
3009
+ * object, so the gate runs only when the caller names it. */
3002
3010
  static VALUE nf_create_element_with_attrs(VALUE self, VALUE document,
3003
3011
  VALUE parent_addr, VALUE name,
3004
3012
  VALUE attrs)
3005
3013
  {
3006
- void *doc_ptr = NIL_P(document)
3007
- ? NULL
3008
- : (void *)(uintptr_t)NUM2ULL(document);
3009
- void *parent = NIL_P(parent_addr)
3010
- ? NULL
3011
- : (void *)(uintptr_t)NUM2ULL(parent_addr);
3012
- void *elem = create_elem_core(doc_ptr, parent,
3013
- StringValueCStr(name), attrs);
3014
+ void *doc_ptr;
3015
+ void *parent;
3016
+ void *elem;
3017
+
3018
+ parent = NIL_P(parent_addr)
3019
+ ? NULL
3020
+ : (void *)(uintptr_t)NUM2ULL(parent_addr);
3021
+ if (!NIL_P(document) && !RB_INTEGER_TYPE_P(document)) {
3022
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
3023
+ rb_raise(c_use_after_free_error,
3024
+ "owning document has been freed");
3025
+ /* The bump rides the ATTACH: a free-floating create mutates
3026
+ * no tree (detached-create discipline, no bump). */
3027
+ if (parent)
3028
+ document_advance_version(document);
3029
+ doc_ptr = doc_ptr_of(document);
3030
+ } else {
3031
+ doc_ptr = NIL_P(document)
3032
+ ? NULL
3033
+ : (void *)(uintptr_t)NUM2ULL(document);
3034
+ }
3035
+ elem = create_elem_core(doc_ptr, parent,
3036
+ StringValueCStr(name), attrs);
3014
3037
  return elem ? ULL2NUM((uintptr_t)elem) : Qnil;
3015
3038
  }
3016
3039
 
@@ -3025,15 +3048,29 @@ static VALUE nf_create_element_with_attrs_wrapped(
3025
3048
  VALUE self, VALUE document, VALUE parent_addr, VALUE name,
3026
3049
  VALUE attrs)
3027
3050
  {
3028
- void *doc_ptr = doc_ptr_of(document);
3029
- void *parent = NIL_P(parent_addr)
3030
- ? NULL
3031
- : (void *)(uintptr_t)NUM2ULL(parent_addr);
3032
- void *elem = create_elem_core(doc_ptr, parent,
3033
- StringValueCStr(name), attrs);
3051
+ void *doc_ptr;
3052
+ void *parent;
3053
+ void *elem;
3034
3054
  VALUE ck, key, node, cache;
3035
3055
 
3036
3056
  (void)self;
3057
+ parent = NIL_P(parent_addr)
3058
+ ? NULL
3059
+ : (void *)(uintptr_t)NUM2ULL(parent_addr);
3060
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
3061
+ rb_raise(c_use_after_free_error,
3062
+ "owning document has been freed");
3063
+ /* #539: attached construction bumps the version — the same
3064
+ * discipline as append_binding_child (bump before the call; a
3065
+ * failed mutation merely discards memos) — so version-stamped
3066
+ * memos (C content, walk-reads) retire across the splice.
3067
+ * Free-floating creates (parent_addr 0) mutate no tree and
3068
+ * stay bump-free, like the detached create faces. */
3069
+ if (parent)
3070
+ document_advance_version(document);
3071
+ doc_ptr = doc_ptr_of(document);
3072
+ elem = create_elem_core(doc_ptr, parent,
3073
+ StringValueCStr(name), attrs);
3037
3074
  if (!elem) return Qnil;
3038
3075
  resolve_binding_classes();
3039
3076
  ck = doc_klass_for_kind(document, 0 /* element */);
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.380.2"
4
+ VERSION = "1.9.381.0"
5
5
  end
Binary file
Binary file
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.380
4
+ - libleptris v1.9.381
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,47 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.381] - 2026-10-22
4
+
5
+ The XSLT transform engine is no longer slower than libxslt on the
6
+ workload users actually run. The identity transform
7
+ (`match="@*|node()"` + `xsl:copy`) was quadratic: 13.0 s where
8
+ xsltproc takes 65 ms on a 20k-book catalog. Three independent causes
9
+ went in PR #1839, and every measured CLI row now beats xsltproc
10
+ end-to-end:
11
+
12
+ | stylesheet (20k-book catalog) | xsltproc 1.1.35 | 1.9.380 | 1.9.381 |
13
+ | --- | --- | --- | --- |
14
+ | identity `@*|node()` | 64.5 ms | 13,002 ms | **56.9 ms** |
15
+ | `copy-of //book` | 37.2 ms | 58.8 ms | **25.3 ms** |
16
+ | dispatch (apply-templates) | 63.8 ms | 23.5 ms | **23.6 ms** |
17
+ | 1,000-template sheet | 293.8 ms | 18.7 ms | **17.6 ms** |
18
+
19
+ ### Fixed
20
+
21
+ - apply xsi:type at every depth in whole-instance validation: an
22
+ instance document may pin a derived type on any element via
23
+ `xsi:type`; validation previously honored it only at the root, so
24
+ deeper particles validated against the declared type (#1838)
25
+
26
+ ### Performance
27
+
28
+ - **xslt: identity transforms and copy-of were quadratic (#1839).**
29
+ Three fixes: (1) the pattern fast path never compiled node-type
30
+ tests — `node(`/`text(`/`processing-instruction(` parsed as NCName
31
+ and failed on the paren, sending every match through the quadratic
32
+ ancestor ladder (and `processing-instruction(` compared 21 of its
33
+ 23 prefix characters); (2) the document-order rank table hashed
34
+ pointers by low bits, which degenerates on fixed-stride pool
35
+ allocations (872 ms to build for a 500k-node transform); (3) the
36
+ append-tail pair pointed at the deepest node a body built, so wide
37
+ parents re-walked their whole child chain on every append after an
38
+ interleaved child. Full suite 2103/2103, libxslt-205 output parity
39
+ included.
40
+ - dom: lane-18 append/create micro-levers from a fresh profile
41
+ (#1836)
42
+
43
+
44
+
3
45
  ## [1.9.380] - 2026-10-22
4
46
 
5
47
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.380
6
+ VERSION 1.9.381
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -106,7 +106,11 @@ Harness `wt/460-harness/build_bench.rb`; A/B pairs alternate within
106
106
  ONE window (drift ±20%). Rejected within noise: fast_attach,
107
107
  native-cache deletion, doc_klass memo, identity-probe deletion,
108
108
  add_child alias. Residual: wrapper allocation (owner-level node
109
- layout).
109
+ layout). **Allocation census 2026-10-22: AT FLOOR** — moxml
110
+ 0.06 minor GC/build vs nokogiri 0.39 (6.5x fewer), 3420 objects vs
111
+ 9907; every named suspect eliminated by the levers; the non-floor
112
+ remainder is 17 objects/build (~0.1% CPU). No lever remains — the
113
+ gap is the wrapper-count/identity-contract decision.
110
114
  - [ ] per-element object budget: one Ruby object per created node
111
115
  (drop the shell entirely post-create-priming; the mint cache
112
116
  insert policy; the context registration)
@@ -138,7 +142,19 @@ three interleaved-median runs (`wt/460-harness/pipeline_bench.rb`,
138
142
  build, serialize medians, fail on >5% regression)
139
143
 
140
144
  ### B5 remaining red rows
141
- - [ ] pugixml lane-18 append/attr-parse (2.0x/5.9x; hash prefilter)
145
+ - [x] pugixml lane-18 append/attr-parse — **re-dated 2026-10-22: the
146
+ 2.0x/5.9x rows no longer exist.** The named levers shipped in
147
+ the prior 22-round campaign (dup-check hash PR #1413, FNV-fused
148
+ lookup PR #1425, append parity PR #1533). Attr-lookup
149
+ ~parity (0.99x; caveat: in-process rigs let leptris's teardown
150
+ depress the adjacent pugixml row — self-warmed rigs are the
151
+ honest numbers). Append: **1.44x on self-warmed rigs; 1.37x
152
+ after PR leptris#1836** (4 behavior-identical levers: childless-
153
+ proof ancestor-walk skip, fused type chains, bounded name scan,
154
+ create_pooled double-copy fix; −3.6%, 9/10 pairs). Residual
155
+ ~1.37x parse gap = conformance features pugixml doesn't do
156
+ (dup detection, error positions, NUL writes) — the measured
157
+ floor; owner decision in #1436.
142
158
  - [ ] zero-copy text lever (#1218) — adopt if it wins any row
143
159
 
144
160
  ## Gate C — Safety: trust is auditable
@@ -58,7 +58,7 @@ from stale build dirs, so the harness protocol is part of the gate.
58
58
  | attr listing | ≥ all | 99x — ahead |
59
59
  | build | ≥ all | 0.87x — ahead |
60
60
  | walk+read (moxml) | ≥ 0.95x nokogiri | **done 2026-10-19** — ~195ns name / ~193ns content, 0.90–0.95x |
61
- | append / attr-parse (pugixml lane 18) | ≥ parity | **RED 2.0x / 5.9x** — hash-prefilter lever named |
61
+ | append / attr-parse (pugixml lane 18) | ≥ parity | lookup ~parity (rig-bias caveat); **append 1.37x after PR leptris#1836** (was 1.44x self-warmed) — residual = conformance floor (#1436) |
62
62
  | DOM mutation rows | ≥ parity | RED — node-layout redesign, owner decision pending |
63
63
  | zero-copy text (#1218 lever) | adopt if it wins any row | lever named, unmeasured |
64
64
 
@@ -59,6 +59,7 @@ LeptrisElement leptris_element_create_with_view(
59
59
  memset(elem, 0, sizeof(struct leptris_element));
60
60
  elem->base.type = LEPTRIS_NODE_TYPE_ELEMENT;
61
61
  char* nm = leptris_sv_to_cstr_pooled(&name_view, pool);
62
+ if (!nm) return NULL; /* pool OOM — do not store a NULL name */
62
63
  elem_set_name(elem, nm);
63
64
  elem->name_hash = leptris_name_hash_compute(nm);
64
65
  elem->name_len = (name_view.length > 254)
@@ -107,13 +108,16 @@ LeptrisElement leptris_element_create_zero_copy(LeptrisMemoryPool* pool) {
107
108
  LeptrisElement leptris_element_create_pooled(const char* name, LeptrisMemoryPool* pool) {
108
109
  if (!name || !pool) return NULL;
109
110
 
110
- /* Pool-owned copy; lives for the document's lifetime. */
111
- char* name_copy = leptris_pool_strdup(pool, name);
112
- if (!name_copy) return NULL;
113
-
114
- /* Hand ownership to create_with_view via the inplace path so the
115
- * name_view points into pool-owned storage, not the caller's buffer. */
116
- return leptris_element_create_pooled_inplace(name_copy, pool);
111
+ /* Lane 18: ONE strlen + ONE pooled copy. The old route strdup'ed
112
+ * the name into the pool and then handed that copy to the inplace
113
+ * path, whose with_view call copies the SAME bytes into the pool
114
+ * again via sv_to_cstr_pooled — the strdup result was orphaned
115
+ * (never stored on the element), so every fallback-path create
116
+ * paid a dead strlen + pool alloc + memcpy. Building the view
117
+ * over the caller's buffer lands on the identical final name
118
+ * pointer (same pooled copy, same interning behavior). */
119
+ return leptris_element_create_with_view(leptris_sv_from_cstr(name),
120
+ pool);
117
121
  }
118
122
 
119
123
  /* Free element - pool allocated, so this is a no-op */
@@ -771,22 +775,16 @@ void leptris_element_append_child_hint_doc(LeptrisElement elem,
771
775
  LeptrisNode* hint) {
772
776
  if (!elem || !child) return;
773
777
 
774
- /* SAFETY: Verify node type field is valid before accessing it
775
- * Small values like 0x4 or 0x1 in the type field suggest the pointer is corrupted */
776
- if (child->type < LEPTRIS_NODE_TYPE_ELEMENT ||
777
- (child->type > LEPTRIS_NODE_TYPE_DOCTYPE &&
778
- child->type != LEPTRIS_NODE_TYPE_ENTITY_REF)) {
779
- /* Invalid node type - corrupted pointer */
780
- return;
781
- }
782
-
783
- if (child->type != LEPTRIS_NODE_TYPE_ELEMENT &&
784
- child->type != LEPTRIS_NODE_TYPE_TEXT &&
785
- child->type != LEPTRIS_NODE_TYPE_CDATA &&
786
- child->type != LEPTRIS_NODE_TYPE_COMMENT &&
787
- child->type != LEPTRIS_NODE_TYPE_PI &&
788
- child->type != LEPTRIS_NODE_TYPE_ENTITY_REF) {
789
- return; /* Only allow these node types as children */
778
+ /* Type gate, fused (lane 18): the appendable set is
779
+ * {ELEMENT..PI} ∪ {ENTITY_REF}; the old two-chain form (a range
780
+ * guard followed by a six-way membership walk) admitted exactly
781
+ * the same set in ~8 compare/branch pairs. One load + two
782
+ * compares decide it. */
783
+ {
784
+ unsigned t = (unsigned)child->type;
785
+ if (!(t <= (unsigned)LEPTRIS_NODE_TYPE_PI ||
786
+ t == (unsigned)LEPTRIS_NODE_TYPE_ENTITY_REF))
787
+ return; /* corrupted or non-appendable node type */
790
788
  }
791
789
 
792
790
  /* Issue #217: if child is already attached to a parent, unlink
@@ -909,22 +907,16 @@ void leptris_element_append_child_hint_doc(LeptrisElement elem,
909
907
  void leptris_element_prepend_child_internal(LeptrisElement elem, LeptrisNode* child) {
910
908
  if (!elem || !child) return;
911
909
 
912
- /* SAFETY: Verify node type field is valid before accessing it
913
- * Small values like 0x4 or 0x1 in the type field suggest the pointer is corrupted */
914
- if (child->type < LEPTRIS_NODE_TYPE_ELEMENT ||
915
- (child->type > LEPTRIS_NODE_TYPE_DOCTYPE &&
916
- child->type != LEPTRIS_NODE_TYPE_ENTITY_REF)) {
917
- /* Invalid node type - corrupted pointer */
918
- return;
919
- }
920
-
921
- if (child->type != LEPTRIS_NODE_TYPE_ELEMENT &&
922
- child->type != LEPTRIS_NODE_TYPE_TEXT &&
923
- child->type != LEPTRIS_NODE_TYPE_CDATA &&
924
- child->type != LEPTRIS_NODE_TYPE_COMMENT &&
925
- child->type != LEPTRIS_NODE_TYPE_PI &&
926
- child->type != LEPTRIS_NODE_TYPE_ENTITY_REF) {
927
- return; /* Only allow these node types as children */
910
+ /* Type gate, fused (lane 18): the appendable set is
911
+ * {ELEMENT..PI} ∪ {ENTITY_REF}; the old two-chain form (a range
912
+ * guard followed by a six-way membership walk) admitted exactly
913
+ * the same set in ~8 compare/branch pairs. One load + two
914
+ * compares decide it. */
915
+ {
916
+ unsigned t = (unsigned)child->type;
917
+ if (!(t <= (unsigned)LEPTRIS_NODE_TYPE_PI ||
918
+ t == (unsigned)LEPTRIS_NODE_TYPE_ENTITY_REF))
919
+ return; /* corrupted or non-appendable node type */
928
920
  }
929
921
 
930
922
  /* Issue #217: if child is already attached to a parent, unlink
@@ -183,7 +183,12 @@ LeptrisElement leptris_element_create(LeptrisDocument doc, const char* name) {
183
183
  * Same init contract as leptris_element_create_with_view
184
184
  * (memset-zero + type/name/name_hash/name_len). Falls back
185
185
  * to the plain pool path when a block can't be allocated. */
186
- size_t name_len = strlen(name);
186
+ /* Lane 18: element names are almost always tiny — a bounded
187
+ * byte scan resolves the length without the strlen call;
188
+ * anything 8+ bytes pays the same strlen as before. */
189
+ size_t name_len = 0;
190
+ while (name_len < 8 && name[name_len]) name_len++;
191
+ if (name_len == 8 && name[8]) name_len = strlen(name);
187
192
  elem = mut_elem_carve(doc);
188
193
  if (elem) {
189
194
  char* name_copy = mut_name_carve(doc, name, name_len);
@@ -469,6 +474,11 @@ LeptrisStatus leptris_element_add_child_addr_installed(
469
474
  LeptrisStatus leptris_element_append_child(LeptrisElement parent, LeptrisElement child) {
470
475
  if (!parent || !child) return LEPTRIS_ERROR_NULL_ARG;
471
476
 
477
+ /* Single document resolution per mutation (TODO 195c): the
478
+ * root walk + map lookup was paid three times per append
479
+ * (twice here, once inside the internal for the tail cache). */
480
+ struct leptris_document* doc = leptris_element_get_document(parent);
481
+
472
482
  /* #1636: appending a node into its own descendant subtree
473
483
  * builds a parent cycle — every tree walker (serializer
474
484
  * first) then recurses without bound and the process dies
@@ -477,9 +487,24 @@ LeptrisStatus leptris_element_append_child(LeptrisElement parent, LeptrisElement
477
487
  * ancestor edge: walk child's parent chain for parent. The
478
488
  * same-document fast path below never sees foreign copies as
479
489
  * ancestors, so the walk runs on the caller's node — O(depth),
480
- * depth bounded by the parse cap. */
481
- {
482
- if (parent == child) return LEPTRIS_ERROR_INVALID_ARG;
490
+ * depth bounded by the parse cap.
491
+ *
492
+ * Lane 18: the freshness probe below runs FIRST. A child is
493
+ * provably childless — and therefore cannot be an ancestor of
494
+ * parent (nor equal any node that has descendants) — only when
495
+ * BOTH hold: it was carved from the current mut block (recent
496
+ * create) AND its child list is still empty. The extent alone is
497
+ * NOT enough: a block-extent element may have been given
498
+ * children since its create, and appending an ancestor under its
499
+ * own descendant must keep rejecting. parent == child stays
500
+ * unconditional and the walk still sees the ORIGINAL child
501
+ * before any #1528 adoption, so accept/reject decisions are
502
+ * identical to the unconditioned guard. */
503
+ int child_fresh = node_in_current_mut_block(doc, child) &&
504
+ (leptris_elem_first_child(child) == NULL);
505
+
506
+ if (parent == child) return LEPTRIS_ERROR_INVALID_ARG;
507
+ if (!child_fresh) {
483
508
  LeptrisElement p = leptris_element_parent(parent);
484
509
  int guard = 0;
485
510
  while (p && guard++ < 8192) {
@@ -488,11 +513,6 @@ LeptrisStatus leptris_element_append_child(LeptrisElement parent, LeptrisElement
488
513
  }
489
514
  }
490
515
 
491
- /* Single document resolution per mutation (TODO 195c): the
492
- * root walk + map lookup was paid three times per append
493
- * (twice here, once inside the internal for the tail cache). */
494
- struct leptris_document* doc = leptris_element_get_document(parent);
495
-
496
516
  /* #1528: a foreign document's node is adopted by copy, never
497
517
  * spliced raw. The freshly-created-then-appended shape proves
498
518
  * same-doc in two hot loads: a child inside the CURRENT mut
@@ -500,7 +520,7 @@ LeptrisStatus leptris_element_append_child(LeptrisElement parent, LeptrisElement
500
520
  * THIS document's block by the create call just executed — no
501
521
  * foreign allocation can live inside it. Everything else takes
502
522
  * the full check (Lane 18 append bar). */
503
- if (!(node_in_current_mut_block(doc, child))) {
523
+ if (!child_fresh) {
504
524
  child = (LeptrisElement)adopt_for_splice((LeptrisNode*)child,
505
525
  doc);
506
526
  if (!child) return LEPTRIS_ERROR_MEMORY;
@@ -508,6 +508,21 @@ static void doc_rank_table_free(DocOrderRankTable* t) {
508
508
  free(t->ranks);
509
509
  }
510
510
 
511
+ /* Mix the pointer before masking. Pool nodes are allocated at a
512
+ * fixed stride, so the old (ptr >> 4) & mask sent every key to the
513
+ * same residue class: with a power-of-two capacity and 64/96-byte
514
+ * strides, only a fraction of the slots were ever reachable and
515
+ * linear probing degenerated. A 500k-node identity transform spent
516
+ * 872 ms in the rank build; mixing makes the same build O(1) per
517
+ * insert. */
518
+ static size_t doc_rank_slot(const void* key, size_t mask) {
519
+ uint64_t x = (uint64_t)(uintptr_t)key;
520
+ x ^= x >> 33;
521
+ x *= 0xff51afd7ed558ccdULL;
522
+ x ^= x >> 33;
523
+ return (size_t)x & mask;
524
+ }
525
+
511
526
  /* Grow at 70% load so probe chains stay short. */
512
527
  static int doc_rank_table_grow(DocOrderRankTable* t) {
513
528
  size_t new_cap = (t->mask + 1) << 1;
@@ -521,7 +536,7 @@ static int doc_rank_table_grow(DocOrderRankTable* t) {
521
536
  size_t new_mask = new_cap - 1;
522
537
  for (size_t i = 0; i <= t->mask; i++) {
523
538
  if (!t->keys[i]) continue;
524
- size_t j = (size_t)(((uintptr_t)t->keys[i] >> 4) & new_mask);
539
+ size_t j = doc_rank_slot(t->keys[i], new_mask);
525
540
  while (new_keys[j]) j = (j + 1) & new_mask;
526
541
  new_keys[j] = t->keys[i];
527
542
  new_ranks[j] = t->ranks[i];
@@ -538,7 +553,7 @@ static int doc_rank_table_put(DocOrderRankTable* t, void* key, int64_t rank) {
538
553
  if ((t->count + 1) * 10 >= (t->mask + 1) * 7) {
539
554
  if (!doc_rank_table_grow(t)) return 0;
540
555
  }
541
- size_t i = (size_t)(((uintptr_t)key >> 4) & t->mask);
556
+ size_t i = doc_rank_slot(key, t->mask);
542
557
  while (t->keys[i]) {
543
558
  if (t->keys[i] == key) return 1; /* first visit wins */
544
559
  i = (i + 1) & t->mask;
@@ -550,7 +565,7 @@ static int doc_rank_table_put(DocOrderRankTable* t, void* key, int64_t rank) {
550
565
  }
551
566
 
552
567
  static int64_t doc_rank_table_get(const DocOrderRankTable* t, void* key) {
553
- size_t i = (size_t)(((uintptr_t)key >> 4) & t->mask);
568
+ size_t i = doc_rank_slot(key, t->mask);
554
569
  while (t->keys[i]) {
555
570
  if (t->keys[i] == key) return t->ranks[i];
556
571
  i = (i + 1) & t->mask;
@@ -423,8 +423,25 @@ static void validate_children(struct xsd_validator* v,
423
423
  }
424
424
  }
425
425
  }
426
- if (ptype)
426
+ /* leptris-ruby#535 (XSD 1.0 2.6.2): xsi:type applies at every
427
+ * assessed depth -- a child typed by the parent particle is
428
+ * substituted exactly like a globally declared one (which
429
+ * validate_element above already handles). */
430
+ const char* cxsi = leptris_element_attribute_ns(
431
+ ce, "http://www.w3.org/2001/XMLSchema-instance", "type");
432
+ if (cxsi && cxsi[0] && ptype) {
433
+ char xuri[512], xlocal[256];
434
+ xsd_resolve_qname_inst(ce, cxsi, xuri, sizeof(xuri),
435
+ xlocal, sizeof(xlocal));
436
+ if (!xlocal[0]) {
437
+ verrf_at(v, ce, "element", cl,
438
+ "xsi:type prefix is not bound in the instance");
439
+ } else {
440
+ validate_local_uri(v, s, ce, cl, xlocal, xuri, cxsi);
441
+ }
442
+ } else if (ptype) {
427
443
  validate_local(v, s, ce, cl, ptype);
444
+ }
428
445
  }
429
446
  }
430
447
 
@@ -1011,6 +1011,15 @@ static int op_result_elem(XsltExec* ex, const XsltInstr* in,
1011
1011
  }
1012
1012
  }
1013
1013
  ex->pending_parent = parent;
1014
+ /* The content ran only inside e (append-only, strictly nested),
1015
+ * so e is `parent`'s newest child — hand it back as the next
1016
+ * append's tail hint (the single-entry pair otherwise points at
1017
+ * the deepest node the body built, and each append to a wide
1018
+ * parent then walks its whole child chain, #682). */
1019
+ if (parent) {
1020
+ ex->last_append_parent = parent;
1021
+ ex->last_append_child = (LeptrisNodeRef)e;
1022
+ }
1014
1023
  return rc;
1015
1024
  }
1016
1025
 
@@ -2691,6 +2700,16 @@ static int op_copy(XsltExec* ex, const XsltInstr* in, LeptrisElement node) {
2691
2700
  ex->pending_parent = e;
2692
2701
  int rc = xslt_exec_instrs(ex, in->child, node);
2693
2702
  ex->pending_parent = saved;
2703
+ /* Result-tree construction is append-only and strictly nested,
2704
+ * so `e` is `saved`'s newest child when the body returns — hand
2705
+ * it back as the next append's tail hint. The single-entry pair
2706
+ * otherwise points at the deepest node the body built, and each
2707
+ * append to a wide parent then walks that parent's whole child
2708
+ * chain (quadratic identity transforms, #682). */
2709
+ if (saved) {
2710
+ ex->last_append_parent = saved;
2711
+ ex->last_append_child = (LeptrisNodeRef)e;
2712
+ }
2694
2713
  return rc;
2695
2714
  }
2696
2715
 
@@ -2763,21 +2782,27 @@ static int copy_node_deep(XsltExec* ex, LeptrisElement node,
2763
2782
  LeptrisNodeRef cc = (LeptrisNodeRef)leptris_cdata_create(
2764
2783
  t, t ? strlen(t) : 0,
2765
2784
  ((struct leptris_document*)ex->result)->pool);
2766
- if (cc) leptris_element_append_child_internal(e, (LeptrisNode*)cc);
2785
+ if (cc) out_append_child(ex, e, (void*)cc);
2767
2786
  } else if (ty == LEPTRIS_NODE_TYPE_COMMENT) {
2768
2787
  LeptrisNodeRef cm = leptris_comment_node_create(
2769
2788
  ex->result, leptris_comment_node_get_content(c));
2770
- if (cm) leptris_element_append_child_internal(e, (LeptrisNode*)cm);
2789
+ if (cm) out_append_child(ex, e, (void*)cm);
2771
2790
  } else if (ty == LEPTRIS_NODE_TYPE_PI) {
2772
2791
  LeptrisNodeRef pi = leptris_pi_node_create(
2773
2792
  ex->result, leptris_pi_node_get_target(c),
2774
2793
  leptris_pi_node_get_data(c));
2775
- if (pi) leptris_element_append_child_internal(e, (LeptrisNode*)pi);
2794
+ if (pi) out_append_child(ex, e, (void*)pi);
2776
2795
  } else if (ty == LEPTRIS_NODE_TYPE_TEXT) {
2777
2796
  const char* t = leptris_text_get_content((LeptrisTextNode*)c);
2778
2797
  if (t && t[0]) out_append_text(ex, e, t);
2779
2798
  }
2780
2799
  }
2800
+ /* The caller's newest child is e — hand it back as the tail
2801
+ * hint so sibling copies append O(1) (see op_copy). */
2802
+ if (parent) {
2803
+ ex->last_append_parent = parent;
2804
+ ex->last_append_child = (LeptrisNodeRef)e;
2805
+ }
2781
2806
  return 0;
2782
2807
  }
2783
2808
 
@@ -3515,6 +3540,10 @@ static int builtin_no_match(XsltExec* ex, const char* mode,
3515
3540
  &(XsltInstr){ .kind = XSLT_INSTR_APPLY_TEMPLATES, .name = mode },
3516
3541
  node);
3517
3542
  ex->pending_parent = saved_parent;
3543
+ if (saved_parent && e) {
3544
+ ex->last_append_parent = saved_parent;
3545
+ ex->last_append_child = (LeptrisNodeRef)e;
3546
+ }
3518
3547
  return rc;
3519
3548
  }
3520
3549
 
@@ -3876,6 +3905,16 @@ static int op_element(XsltExec* ex, const XsltInstr* in,
3876
3905
  ex->pending_parent = e;
3877
3906
  int rc = xslt_exec_instrs(ex, in->child, node);
3878
3907
  ex->pending_parent = saved;
3908
+ /* Result-tree construction is append-only and strictly nested,
3909
+ * so `e` is `saved`'s newest child when the body returns — hand
3910
+ * it back as the next append's tail hint. The single-entry pair
3911
+ * otherwise points at the deepest node the body built, and each
3912
+ * append to a wide parent then walks that parent's whole child
3913
+ * chain (quadratic identity transforms, #682). */
3914
+ if (saved) {
3915
+ ex->last_append_parent = saved;
3916
+ ex->last_append_child = (LeptrisNodeRef)e;
3917
+ }
3879
3918
  return rc;
3880
3919
  }
3881
3920
 
@@ -59,6 +59,22 @@ static int parse_pat_step(const char** pp, const char* end,
59
59
  if (*p == '@') { s->is_attr = 1; p++; }
60
60
  if (p < end && *p == '*') {
61
61
  p++;
62
+ /* Node-type tests FIRST: "node(" also starts with an NCName, so
63
+ * the name branch below would swallow the keyword and then fail
64
+ * on its '(' — leaving steps_valid=0 and sending every match of
65
+ * node()/text()/comment()/processing-instruction() into the
66
+ * ancestor ladder (O(width) per node: the identity transform's
67
+ * @*|node() went quadratic). The bare names "node"/"text" still
68
+ * parse as name tests: the keyword check requires the '('. */
69
+ } else if (p + 5 <= end && strncmp(p, "node(", 5) == 0) {
70
+ s->kind = 1; p += 5;
71
+ } else if (p + 5 <= end && strncmp(p, "text(", 5) == 0) {
72
+ s->kind = 2; p += 5;
73
+ } else if (p + 8 <= end && strncmp(p, "comment(", 8) == 0) {
74
+ s->kind = 3; p += 8;
75
+ } else if (p + 23 <= end &&
76
+ strncmp(p, "processing-instruction(", 23) == 0) {
77
+ s->kind = 4; p += 23;
62
78
  } else if (p < end && ncname_char(*p) && *p != '-') {
63
79
  const char* st = p;
64
80
  while (p < end && ncname_char(*p)) p++;
@@ -72,15 +88,6 @@ static int parse_pat_step(const char** pp, const char* end,
72
88
  if (!s->name) return 0;
73
89
  memcpy(s->name, st, n);
74
90
  s->name[n] = 0;
75
- } else if (p + 5 <= end && strncmp(p, "node(", 5) == 0) {
76
- s->kind = 1; p += 5;
77
- } else if (p + 5 <= end && strncmp(p, "text(", 5) == 0) {
78
- s->kind = 2; p += 5;
79
- } else if (p + 8 <= end && strncmp(p, "comment(", 8) == 0) {
80
- s->kind = 3; p += 8;
81
- } else if (p + 21 <= end &&
82
- strncmp(p, "processing-instruction(", 23 - 2) == 0) {
83
- s->kind = 4; p += 21;
84
91
  } else {
85
92
  return 0;
86
93
  }
@@ -2966,6 +2966,48 @@ TEST(DomCycleGuard, AncestorAppendRejected) {
2966
2966
  leptris_document_free(d);
2967
2967
  }
2968
2968
 
2969
+ /* Lane 18: the append path skips the #1636 ancestor walk when the
2970
+ * child is provably childless (fresh mut-block carve AND empty
2971
+ * child list). An element still sitting inside the mut-block
2972
+ * extent is NOT automatically childless — it may have been given
2973
+ * children since its create — so the walk must still run for it.
2974
+ * Regression pin for the first cut, which keyed the skip on the
2975
+ * extent alone and ACCEPTED root-under-b for mutation-built
2976
+ * trees (parse-built trees never enter the mut block, which is
2977
+ * why the parse-only specs above could not see it). */
2978
+ TEST(DomCycleGuard, AncestorAppendRejectedInMutBlockExtent) {
2979
+ LeptrisDocument d = leptris_document_create();
2980
+ ASSERT_NE(d, nullptr);
2981
+ LeptrisElement root = leptris_element_create(d, "root");
2982
+ ASSERT_NE(root, nullptr);
2983
+ ASSERT_EQ(leptris_document_set_root(d, root), LEPTRIS_OK);
2984
+ LeptrisElement a = leptris_element_create(d, "a");
2985
+ LeptrisElement b = leptris_element_create(d, "b");
2986
+ ASSERT_NE(a, nullptr);
2987
+ ASSERT_NE(b, nullptr);
2988
+ ASSERT_EQ(leptris_element_append_child(root, a), LEPTRIS_OK);
2989
+ ASSERT_EQ(leptris_element_append_child(a, b), LEPTRIS_OK);
2990
+
2991
+ /* all three still sit in the current mut-block extent here —
2992
+ * the guard must not trust the extent as childlessness */
2993
+ EXPECT_EQ(leptris_element_append_child(b, root),
2994
+ LEPTRIS_ERROR_INVALID_ARG);
2995
+ EXPECT_EQ(leptris_element_append_child(a, a),
2996
+ LEPTRIS_ERROR_INVALID_ARG);
2997
+ EXPECT_EQ(leptris_element_prepend_child(b, root),
2998
+ LEPTRIS_ERROR_INVALID_ARG);
2999
+
3000
+ /* fresh create->append (the fast-path shape) still links */
3001
+ LeptrisElement c = leptris_element_create(d, "c");
3002
+ ASSERT_NE(c, nullptr);
3003
+ EXPECT_EQ(leptris_element_append_child(b, c), LEPTRIS_OK);
3004
+ char* out = leptris_document_serialize(d, nullptr);
3005
+ ASSERT_NE(out, nullptr);
3006
+ EXPECT_NE(strstr(out, "<b><c/></b>"), nullptr);
3007
+ leptris_free_string(out);
3008
+ leptris_document_free(d);
3009
+ }
3010
+
2969
3011
  TEST(DomCycleGuard, InsertIntoOwnSubtreeRejected) {
2970
3012
  LeptrisStatus st = LEPTRIS_OK;
2971
3013
  LeptrisDocument d =
@@ -2855,4 +2855,94 @@ TEST(XsdConformanceClauses, XsiTypeUnknownTypeIsLoud) {
2855
2855
  leptris_xsd_free(s);
2856
2856
  }
2857
2857
 
2858
+ TEST(XsdConformanceClauses, XsiTypeAtDepthGlobalChild) {
2859
+ const char schema[] =
2860
+ "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\""
2861
+ " targetNamespace=\"urn:t\" xmlns=\"urn:t\""
2862
+ " elementFormDefault=\"qualified\">"
2863
+ " <xs:complexType name=\"Base\">"
2864
+ " <xs:sequence><xs:element name=\"a\" type=\"xs:string\"/></xs:sequence>"
2865
+ " </xs:complexType>"
2866
+ " <xs:complexType name=\"Derived\">"
2867
+ " <xs:complexContent>"
2868
+ " <xs:extension base=\"Base\">"
2869
+ " <xs:sequence><xs:element name=\"b\" type=\"xs:string\"/></xs:sequence>"
2870
+ " </xs:extension>"
2871
+ " </xs:complexContent>"
2872
+ " </xs:complexType>"
2873
+ " <xs:element name=\"doc\" type=\"Base\"/>"
2874
+ " <xs:element name=\"wrap\">"
2875
+ " <xs:complexType>"
2876
+ " <xs:sequence><xs:element ref=\"doc\"/></xs:sequence>"
2877
+ " </xs:complexType>"
2878
+ " </xs:element>"
2879
+ "</xs:schema>";
2880
+ LeptrisXsdSchema s = compile_string(schema);
2881
+ ASSERT_NE(s, (LeptrisXsdSchema)0);
2882
+ /* leptris-ruby#535 depth case, global-declaration child: the
2883
+ * child walk re-enters validate_element, which reads xsi:type.
2884
+ * The prefix resolves through the ROOT's declaration -- in
2885
+ * scope, not just the bearing element's own. */
2886
+ EXPECT_EQ(validate_compiled(
2887
+ s,
2888
+ "<wrap xmlns=\"urn:t\" xmlns:i=\"urn:t\""
2889
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
2890
+ "<doc xsi:type=\"i:Derived\"><a>x</a><b>y</b></doc></wrap>"),
2891
+ 1)
2892
+ << "xsi:type must be honored on a global child below the root";
2893
+ /* the declared type governs without xsi:type: b is out of Base. */
2894
+ EXPECT_EQ(validate_compiled(
2895
+ s, "<wrap xmlns=\"urn:t\"><doc><a>x</a><b>y</b></doc></wrap>"),
2896
+ 0);
2897
+ leptris_xsd_free(s);
2898
+ }
2899
+
2900
+ TEST(XsdConformanceClauses, XsiTypeAtDepthParticleTypedChild) {
2901
+ const char schema[] =
2902
+ "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\""
2903
+ " targetNamespace=\"urn:t\" xmlns=\"urn:t\""
2904
+ " elementFormDefault=\"qualified\">"
2905
+ " <xs:complexType name=\"Base\">"
2906
+ " <xs:sequence><xs:element name=\"a\" type=\"xs:string\"/></xs:sequence>"
2907
+ " </xs:complexType>"
2908
+ " <xs:complexType name=\"Derived\">"
2909
+ " <xs:complexContent>"
2910
+ " <xs:extension base=\"Base\">"
2911
+ " <xs:sequence><xs:element name=\"b\" type=\"xs:string\"/></xs:sequence>"
2912
+ " </xs:extension>"
2913
+ " </xs:complexContent>"
2914
+ " </xs:complexType>"
2915
+ " <xs:element name=\"wrap\">"
2916
+ " <xs:complexType>"
2917
+ " <xs:sequence><xs:element name=\"doc\" type=\"Base\"/></xs:sequence>"
2918
+ " </xs:complexType>"
2919
+ " </xs:element>"
2920
+ "</xs:schema>";
2921
+ LeptrisXsdSchema s = compile_string(schema);
2922
+ ASSERT_NE(s, (LeptrisXsdSchema)0);
2923
+ /* leptris-ruby#535 depth case, local particle: no global `doc`
2924
+ * exists, so the child is typed by wrap's particle -- xsi:type
2925
+ * must still substitute Derived's model at that depth. */
2926
+ EXPECT_EQ(validate_compiled(
2927
+ s,
2928
+ "<wrap xmlns=\"urn:t\" xmlns:i=\"urn:t\""
2929
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
2930
+ "<doc xsi:type=\"i:Derived\"><a>x</a><b>y</b></doc></wrap>"),
2931
+ 1)
2932
+ << "xsi:type must substitute on a particle-typed child too";
2933
+ /* the named type's own constraint applies: Derived needs b. */
2934
+ EXPECT_EQ(validate_compiled(
2935
+ s,
2936
+ "<wrap xmlns=\"urn:t\" xmlns:i=\"urn:t\""
2937
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
2938
+ "<doc xsi:type=\"i:Derived\"><a>x</a></doc></wrap>"),
2939
+ 0);
2940
+ /* without xsi:type the declared particle type governs: b is out
2941
+ * of Base's model. */
2942
+ EXPECT_EQ(validate_compiled(
2943
+ s, "<wrap xmlns=\"urn:t\"><doc><a>x</a><b>y</b></doc></wrap>"),
2944
+ 0);
2945
+ leptris_xsd_free(s);
2946
+ }
2947
+
2858
2948
  } // namespace
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json",
3
3
  "name": "leptris",
4
- "version": "1.9.380",
4
+ "version": "1.9.381",
5
5
  "description": "High-performance XML parser and XPath 1.0 engine in C (libleptris)",
6
6
  "homepage": "https://github.com/leptris/leptris",
7
7
  "license": "MIT",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.380.2
4
+ version: 1.9.381.0
5
5
  platform: arm-linux
6
6
  authors:
7
7
  - Ribose Inc.