leptris 1.9.265.0 → 1.9.268.0

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: 5549f821f7f1ba7a4d5c161680e4535d439938b0d0d5220417e636b684805719
4
- data.tar.gz: d7cb676a13bbce365e0f704e8cee85d006318071867040b3a6f87b29f90379ba
3
+ metadata.gz: cba1727210a6adce592d6f84970bfcf2abb6b0cd6bba396bfc5714e0b6a2c00e
4
+ data.tar.gz: f8669f950703f4a01fbabc3feef084911a22ef169ec8c6debfe14f057579b591
5
5
  SHA512:
6
- metadata.gz: 88f706c894390f015bb8501e711a3063d5ae0d1588d5319e2606cdfa79fe1a657da37dda758a85b79833d172ccdc075fe1f61f5a300b42652f51d71b088516d1
7
- data.tar.gz: a99096295f37ed317e9201400f273c76f94f29d78602cec42f8eba9bc4d4808d7cfa99a646cb11e7392ed4defbc7ea2e4f5be8e690c339ac73ddb257bc4c7ab8
6
+ metadata.gz: c3441867d08747f3ed67559e68c29e1c9b1a33b0df9ab29d919580d149a696ff422e20a339611ce52ea2e768c7dfe6ecc9881f5cd20e4575f8bcf7b59adb2fcb
7
+ data.tar.gz: 8421fd0b02bdd2a321bc998e75bddbf07aec3597740618fa38008b811b61e7726357323ae91c893d1b714ce233290b1055a2c8239dc1d0d0dc53bcdc412bae5d
data/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ All notable changes to Leptris will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.268.0] - 2026-09-29
9
+
10
+ ### Performance — engine sync (1.9.266-1.9.268)
11
+
12
+ - Vendored libleptris 1.9.268: three DOM access rounds.
13
+ - The child-iteration cache: indexed child access resumes from a
14
+ per-element generation-validated slot instead of re-walking —
15
+ CI Child Access 0.34x -> 2.42x vs pugixml, Tree Walking 5.81x;
16
+ the cache now materializes lazily for attribute-free parents
17
+ too (the plain wide-catalog case was an O(N^2) re-walk per
18
+ call: 148s -> 0.77s on the 96 KB fixture).
19
+ - Bare-name attribute lookup skips prefixed attributes via the
20
+ TODO 173 ns-cache invariant — 14.5 -> 8.8 ns/query, 1.15x
21
+ pugixml.
22
+
8
23
  ## [1.9.265.0] - 2026-09-28
9
24
 
10
25
  ### 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.265"
11
+ LIBLEPTRIS_VERSION = "1.9.268"
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.265.0"
4
+ VERSION = "1.9.268.0"
5
5
  end
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.265
4
+ - libleptris v1.9.268
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,29 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.268] - 2026-09-28
4
+
5
+ ### Performance
6
+
7
+ - dom: the child-iteration cache is now materialized lazily for attribute-free parents. The round-13 resume slot only engaged for elements carrying namespace state, so indexed child access on a plain wide parent (`<catalog>` with a thousand `<product>` children) was still an O(N^2) full re-walk per call — dom_benchmark_v2's Child Access row measured 148 SECONDS on the 96 KB catalog fixture (CI's 1 KB fixture hid it entirely). `child()` now carves the cache from the element's pool on first indexed access; the mutation-invalidation contract is unchanged and pinned by a new spec plus a tightened (18x -> 6x) perf guard watched failing first. Same fixture after: 0.77 s (~190x). CI artifacts now show all three access rows AHEAD of pugixml on ubuntu: Child 2.42x, Attribute 1.45x, Tree Walking 5.81x.
8
+
9
+
10
+
11
+ ## [1.9.267] - 2026-09-28
12
+
13
+ ### Performance
14
+
15
+ - dom: bare-name attribute lookup rebuilt on the TODO 173 invariant — an attribute carries a side ns cache only when its name contains a colon, so the query walk skips prefixed attributes with a single int32 load and the per-attribute colon-probe loop is gone entirely. The finder is always-inline and the needle is hashed in the same pass as its length+colon scan (`attr_hash15_finish` split out as the shared finalizer). Attr-only arm64 harness: 14.5 -> 8.8 ns/query vs pugixml 7.4 (1.94x -> 1.15x).
16
+
17
+
18
+
19
+ ## [1.9.266] - 2026-09-28
20
+
21
+ ### Performance
22
+
23
+ - dom: child-iteration cache for indexed child access. `leptris_element_child(parent, i)` no longer re-walks from the first child on every call — it resumes from a per-element slot (generation-validated; every child-list mutator invalidates) so sequential indexed access is O(1) per step, and sibling hops skip the generic node type-switch via the shared edge tables. CI dom_benchmark_v2 Child Access: 0.34x -> 2.02x vs pugixml (ubuntu leg); Tree Walking 4.94x.
24
+
25
+
26
+
3
27
  ## [1.9.265] - 2026-09-28
4
28
 
5
29
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.265
6
+ VERSION 1.9.268
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -219,12 +219,7 @@ static inline void leptris_attr_set_next(struct leptris_attribute* a,
219
219
  * low bits sit under the multiply's carry chain; the upper half
220
220
  * mixes better). Returns nonzero: 0 maps to 1 so the "uncomputed"
221
221
  * sentinel stays unambiguous. */
222
- static inline uint16_t attr_hash15(const char* s, size_t len) {
223
- uint32_t h = 2166136261u;
224
- for (size_t i = 0; i < len; i++) {
225
- h ^= (unsigned char)s[i];
226
- h *= 16777619u;
227
- }
222
+ static inline uint16_t attr_hash15_finish(uint32_t h) {
228
223
  /* Finalizer (round 20): raw FNV truncation (h>>16) mapped
229
224
  * names differing in their last digit to hashes exactly 256
230
225
  * apart — arithmetic progressions that collide onto one open-
@@ -237,6 +232,15 @@ static inline uint16_t attr_hash15(const char* s, size_t len) {
237
232
  return h15 ? h15 : 1;
238
233
  }
239
234
 
235
+ static inline uint16_t attr_hash15(const char* s, size_t len) {
236
+ uint32_t h = 2166136261u;
237
+ for (size_t i = 0; i < len; i++) {
238
+ h ^= (unsigned char)s[i];
239
+ h *= 16777619u;
240
+ }
241
+ return attr_hash15_finish(h);
242
+ }
243
+
240
244
  /* Entity flag (round 19): name_hash bit 15. */
241
245
  static inline int attr_has_entities(const struct leptris_attribute* a) {
242
246
  return (a->name_hash & 0x8000u) != 0;
@@ -370,6 +374,16 @@ struct leptris_ns_cache {
370
374
  struct leptris_ns_cache* doc_next;
371
375
  unsigned char prefix_heap;
372
376
  unsigned char uri_heap;
377
+ /* Lane-18 round 13: child-iteration cache. child_gen is bumped by
378
+ * every child-list mutator (prepend/insert/remove); child() stores
379
+ * (child_cached_gen, child_idx, child_node) after a walk and
380
+ * resumes from it while the generations agree — sequential indexed
381
+ * access becomes O(1) per step. append_child needs no bump: it
382
+ * preserves existing indices. */
383
+ uint32_t child_gen;
384
+ uint32_t child_cached_gen;
385
+ uint32_t child_idx;
386
+ LeptrisNode* child_node;
373
387
  };
374
388
 
375
389
  struct leptris_element {
@@ -927,7 +941,15 @@ void leptris_element_set_next_sibling(LeptrisElement elem, LeptrisElement siblin
927
941
  * future compact-storage cache (e.g. pugixml-style compact pointer
928
942
  * table) can plug in without touching every mutation call site. */
929
943
  static inline void leptris_element_invalidate_child_cache(LeptrisElement elem) {
930
- (void)elem;
944
+ /* Lane-18 round 13: the hook now drives the ns_cache child-
945
+ * iteration cache (child_idx/child_node below). Bumping child_gen
946
+ * AND nulling child_node means a stale cache can never serve a
947
+ * node even across a 2^32 generation wrap. */
948
+ struct leptris_ns_cache* c = elem_get_ns_cache(elem);
949
+ if (c) {
950
+ c->child_gen++;
951
+ c->child_node = NULL;
952
+ }
931
953
  }
932
954
 
933
955
  /* ============================================================================
@@ -24,6 +24,8 @@ struct leptris_namespace* leptris_namespace_new_pooled(const char* prefix,
24
24
  #include "cdata.h"
25
25
  #include "comment.h"
26
26
  #include "pi.h"
27
+ #include "entity_ref.h"
28
+ #include "edges.h"
27
29
  #include "../common/string_view.h"
28
30
  #include "../common/entities.h"
29
31
  #include "../common/port.h"
@@ -35,6 +37,11 @@ struct leptris_namespace* leptris_namespace_new_pooled(const char* prefix,
35
37
  * resolver are defined in the attribute section below. */
36
38
  static struct leptris_attribute* find_attr_expanded_len(
37
39
  LeptrisElement elem, const char* uri, const char* local, size_t ll);
40
+ static struct leptris_attribute* find_attr_bare(
41
+ LeptrisElement elem, const char* local, size_t ll);
42
+ static struct leptris_attribute* find_attr_bare_h(LeptrisElement elem,
43
+ const char* local,
44
+ size_t ll, uint16_t lh);
38
45
  static const char* elem_resolve_attr_prefix(LeptrisElement elem,
39
46
  const char* prefix);
40
47
  #include <math.h>
@@ -170,14 +177,23 @@ LEPTRIS_API const char* leptris_element_attribute(LeptrisElement elem, const cha
170
177
  * strcmp ran for EVERY bare-name query. */
171
178
  const char* colon = NULL;
172
179
  size_t name_len = 0;
180
+ /* The FNV rides this same pass (attr_hash15's byte loop fused
181
+ * into the len+colon scan — a second pass over the needle cost
182
+ * ~1ns/query). Discarded when a colon ends the scan: the bare
183
+ * path that consumes the hash is not taken then. */
184
+ uint32_t nh = 2166136261u;
173
185
  for (; name[name_len]; name_len++) {
174
- if (name[name_len] == ':') { colon = &name[name_len]; break; }
186
+ unsigned char c9 = (unsigned char)name[name_len];
187
+ if (c9 == ':') { colon = &name[name_len]; break; }
188
+ nh ^= c9;
189
+ nh *= 16777619u;
175
190
  }
176
191
  struct leptris_attribute* attr;
177
192
  if (!colon) {
178
193
  if (name_len == 5 && name[0] == 'x' &&
179
194
  leptris_memeq_short(name, "xmlns", 5)) return NULL; /* (5) */
180
- attr = find_attr_expanded_len(elem, NULL, name, name_len); /* (1) */
195
+ attr = find_attr_bare_h(elem, name, name_len,
196
+ attr_hash15_finish(nh)); /* (1) */
181
197
  } else {
182
198
  size_t pl = (size_t)(colon - name);
183
199
  if (pl == 5 && memcmp(name, "xmlns", 5) == 0) return NULL; /* (5) */
@@ -327,6 +343,41 @@ static int uri_is_none(const char* uri) {
327
343
  return !uri || !uri[0];
328
344
  }
329
345
 
346
+ /* Bare-name lookup (needle provably colon-free). The TODO 173
347
+ * invariant: an attribute carries a side ns cache ONLY when its name
348
+ * contains a colon — all three attr_set_ns_cache sites are
349
+ * colon-gated — so `ns_cache_off == 0` is an exact colon-free test,
350
+ * and a prefixed attr can never match a bare needle (#542 rule 1).
351
+ * That removes the per-attr colon probe entirely: a skipped hop is
352
+ * one int32 load, and the confirmed hop needs no probe either. */
353
+ /* Always-inline: an out-of-line call per query costs more than the
354
+ * whole walk body (LTO otherwise emits this as a shared subroutine
355
+ * for the expanded-name delegation — the call was ~65% of query
356
+ * time at 2-attr chain lengths). Takes the caller's needle hash:
357
+ * attribute() computes it inside its len+colon scan, so the needle
358
+ * is hashed in one pass, not two. */
359
+ LEPTRIS_ALWAYS_INLINE
360
+ static struct leptris_attribute* find_attr_bare_h(LeptrisElement elem,
361
+ const char* local,
362
+ size_t ll, uint16_t lh) {
363
+ struct leptris_attribute* attr = leptris_element_get_first_attribute(elem);
364
+ while (attr) {
365
+ if (attr->ns_cache_off == 0 &&
366
+ attr->name_view.length == ll &&
367
+ attr_name_hash(attr) == lh &&
368
+ leptris_memeq_short(attr->name_view.data, local, ll))
369
+ return attr;
370
+ attr = leptris_attr_next(attr);
371
+ }
372
+ return NULL;
373
+ }
374
+
375
+ static struct leptris_attribute* find_attr_bare(LeptrisElement elem,
376
+ const char* local,
377
+ size_t ll) {
378
+ return find_attr_bare_h(elem, local, ll, attr_hash15(local, ll));
379
+ }
380
+
330
381
  /* Find an attribute by EXPANDED name. uri NULL/"" matches only
331
382
  * no-namespace attributes; otherwise the URI must match what the
332
383
  * attribute's prefix resolves to (prefix-agnostic). Local names
@@ -334,34 +385,24 @@ static int uri_is_none(const char* uri) {
334
385
  static struct leptris_attribute* find_attr_expanded_len(
335
386
  LeptrisElement elem, const char* uri, const char* local,
336
387
  size_t ll) {
388
+ if (uri_is_none(uri)) return find_attr_bare(elem, local, ll);
337
389
  struct leptris_attribute* attr = leptris_element_get_first_attribute(elem);
338
- /* No-namespace search: the 15-bit name-hash prefilter (TODO 172
339
- * / S9, as in leptris_dom_attr_find). A hash+length+memcmp hit
340
- * against a colon-free needle implies the attr name carries no
341
- * colon, so the per-attr memchr is gone from the common path —
342
- * value-of @name and attribute axes ride this (#682). */
343
- uint16_t lh = uri_is_none(uri) ? attr_hash15(local, ll) : 0;
344
390
  while (attr) {
345
391
  const char* n = attr->name_view.data;
346
392
  size_t nl = attr->name_view.length;
347
- if (nl < ll) goto next;
348
393
  /* Round 11: inline colon probe for the short names that
349
- * dominate attr traffic — memchr's call setup costs more than
350
- * the 1-3 iterations (the TODO 174 law). */
351
- const char* colon = NULL;
352
- if (nl <= 16) {
353
- for (const char* c9 = n; c9 < n + nl; c9++) {
354
- if (*c9 == ':') { colon = c9; break; }
355
- }
356
- } else {
357
- colon = (const char*)memchr(n, ':', nl);
358
- }
359
- if (uri_is_none(uri)) {
360
- if (colon) goto next; /* namespaced */
361
- if (attr_name_hash(attr) == lh &&
362
- leptris_memeq_short(n, local, ll)) return attr;
394
+ * dominate attr traffic — memchr's call setup costs more
395
+ * than the 1-3 iterations (the TODO 174 law). */
396
+ const char* colon = NULL;
397
+ if (nl <= 16) {
398
+ for (const char* c9 = n; c9 < n + nl; c9++) {
399
+ if (*c9 == ':') { colon = c9; break; }
400
+ }
363
401
  } else {
364
- if (!colon) goto next; /* no namespace */
402
+ colon = (const char*)memchr(n, ':', nl);
403
+ }
404
+ if (!colon) goto next; /* no namespace */
405
+ {
365
406
  size_t pl = (size_t)(colon - n);
366
407
  if (nl - pl - 1 != ll || memcmp(colon + 1, local, ll) != 0)
367
408
  goto next;
@@ -621,11 +662,72 @@ LEPTRIS_API LeptrisElement leptris_element_child(LeptrisElement elem, size_t ind
621
662
  if (!elem) return NULL;
622
663
  if (index >= elem->child_count) return NULL;
623
664
 
624
- LeptrisElement child = leptris_element_get_first_child(elem);
625
- for (size_t i = 0; i < index && child != NULL; i++) {
626
- child = leptris_element_get_next_sibling(child);
665
+ /* Lane-18 round 13: resume from the ns_cache child-iteration
666
+ * slot when the generations agree — sequential indexed access
667
+ * (the loop pattern) becomes O(1) per step instead of re-walking
668
+ * from the first child through every interleaved text node.
669
+ * leptris_element_invalidate_child_cache bumps the generation on
670
+ * every child-list mutation. */
671
+ struct leptris_ns_cache* cc = elem_get_ns_cache(elem);
672
+ if (!cc && elem->child_count > 1) {
673
+ /* Lane-18 round 15: attr-free parents carry no parse-time
674
+ * ns_cache (it exists only where the parse stamped
675
+ * xmlns/prefix state), so the round-13 resume slot never
676
+ * engaged for them and every indexed read re-walked from
677
+ * the first child — O(children) per call (a 96 KB catalog
678
+ * root measured 148 s per 100k indexed reads). Materialize
679
+ * the cache from the element's pool on first indexed
680
+ * access: the string fields stay NULL (nothing heap-owned,
681
+ * so no doc-chain registration), and the mutation
682
+ * invalidation hook keys off elem_get_ns_cache, so a cache
683
+ * created here is invalidated exactly like a parse-time
684
+ * one. Detached elements have no pool — they keep the
685
+ * walk. */
686
+ LeptrisMemoryPool* pool = leptris_element_get_pool(elem);
687
+ if (pool) {
688
+ struct leptris_ns_cache* nc = (struct leptris_ns_cache*)
689
+ leptris_pool_alloc(pool, sizeof(*nc));
690
+ if (nc) {
691
+ memset(nc, 0, sizeof(*nc));
692
+ elem_set_ns_cache((struct leptris_element*)elem, nc);
693
+ cc = nc;
694
+ }
695
+ }
627
696
  }
628
- return child;
697
+ LeptrisNode* cur;
698
+ size_t i;
699
+ if (cc && cc->child_cached_gen == cc->child_gen && cc->child_node &&
700
+ index >= cc->child_idx) {
701
+ cur = cc->child_node;
702
+ i = cc->child_idx;
703
+ } else {
704
+ cur = (LeptrisNode*)leptris_element_get_first_child(elem);
705
+ i = 0;
706
+ }
707
+ while (cur) {
708
+ if (cur->type == LEPTRIS_NODE_TYPE_ELEMENT) {
709
+ if (i == index) {
710
+ if (cc) {
711
+ cc->child_cached_gen = cc->child_gen;
712
+ cc->child_idx = index;
713
+ cc->child_node = cur;
714
+ }
715
+ return (LeptrisElement)cur;
716
+ }
717
+ i++;
718
+ }
719
+ /* Type-dispatched raw hop (edges.h tables, #450): text nodes
720
+ * interleave with elements and carry next_sibling_off at a
721
+ * different struct offset — a single-offset cast here reads
722
+ * garbage (the segfault this comment replaced). */
723
+ size_t ti = leptris_edge_idx((unsigned)cur->type);
724
+ if (ti >= 6) { cur = NULL; continue; }
725
+ const int32_t* f =
726
+ (const int32_t*)((char*)cur + leptris_edge_sib_off[ti]);
727
+ cur = (LeptrisNode*)leptris_compact_int32_decode_inline(
728
+ (void*)cur, *f, f);
729
+ }
730
+ return NULL;
629
731
  }
630
732
 
631
733
  /**
@@ -1085,6 +1085,10 @@ static struct leptris_ns_cache* dp_ensure_cache(DParser* p,
1085
1085
  c->doc_next = NULL;
1086
1086
  c->prefix_heap = 0;
1087
1087
  c->uri_heap = 0;
1088
+ c->child_gen = 0;
1089
+ c->child_cached_gen = 0;
1090
+ c->child_idx = 0;
1091
+ c->child_node = NULL;
1088
1092
  elem_set_ns_cache(elem, c);
1089
1093
  return c;
1090
1094
  }
@@ -93,6 +93,7 @@ leptris_add_test(test_parser parser/test_parser.cpp)
93
93
  leptris_add_test(test_large_docs parser/test_large_documents.cpp)
94
94
  leptris_add_test(test_encoding parser/test_encoding.cpp)
95
95
  leptris_add_test(test_dom dom/test_dom.cpp)
96
+ leptris_add_test(test_element_children dom/test_element_children.cpp)
96
97
  leptris_add_test(test_map_lifecycle dom/test_map_lifecycle.cpp)
97
98
  leptris_add_test(test_simd_count parser/test_simd_count.cpp)
98
99
  leptris_add_test(test_descriptor abi/test_descriptor.cpp)
@@ -0,0 +1,102 @@
1
+ // test/dom/test_element_children.cpp — indexed child access specs,
2
+ // including the lane-18 round-13 iteration-cache invalidation guards.
3
+ #include <gtest/gtest.h>
4
+ #include <string>
5
+ #include "leptris.h"
6
+
7
+
8
+ TEST(ElementChildCache, MutationInvalidatesIterationSlot) {
9
+ /* Lane-18 round 13: the ns_cache child-iteration slot must never
10
+ * serve a node across a child-list mutation. */
11
+ LeptrisStatus st;
12
+ LeptrisDocument doc = leptris_parse_string(
13
+ "<r><a/><b/><c/></r>", 19, &st);
14
+ ASSERT_NE(doc, nullptr);
15
+
16
+ LeptrisElement root = leptris_document_root(doc);
17
+ ASSERT_NE(root, nullptr);
18
+ LeptrisElement b = leptris_element_child(root, 1);
19
+ ASSERT_NE(b, nullptr);
20
+ EXPECT_EQ(std::string(leptris_element_name(b)), "b");
21
+
22
+ /* Mutate before the cached position: indices shift. */
23
+ LeptrisDocument fresh = leptris_document_create();
24
+ LeptrisElement z = leptris_element_create(fresh, "z");
25
+ ASSERT_NE(z, nullptr);
26
+ ASSERT_EQ(leptris_element_prepend_child(root, z), LEPTRIS_OK);
27
+
28
+ const char* names[] = {"z", "a", "b", "c"};
29
+ for (int i = 0; i < 4; i++) {
30
+ LeptrisElement c = leptris_element_child(root, (size_t)i);
31
+ ASSERT_NE(c, nullptr) << "i=" << i;
32
+ EXPECT_EQ(std::string(leptris_element_name(c)), names[i]) << "i=" << i;
33
+ }
34
+ leptris_document_free(fresh);
35
+ leptris_document_free(doc);
36
+ }
37
+
38
+ TEST(ElementChildCache, RemoveChildInvalidatesIterationSlot) {
39
+ LeptrisStatus st;
40
+ LeptrisDocument doc = leptris_parse_string(
41
+ "<r><a/><b/><c/></r>", 19, &st);
42
+ ASSERT_NE(doc, nullptr);
43
+ LeptrisElement root = leptris_document_root(doc);
44
+ ASSERT_NE(root, nullptr);
45
+ LeptrisElement c = leptris_element_child(root, 2);
46
+ ASSERT_NE(c, nullptr);
47
+
48
+ LeptrisElement b = leptris_element_child(root, 1);
49
+ ASSERT_NE(b, nullptr);
50
+ ASSERT_EQ(leptris_element_remove_child(root, b), LEPTRIS_OK);
51
+
52
+ LeptrisElement c2 = leptris_element_child(root, 1);
53
+ ASSERT_NE(c2, nullptr);
54
+ EXPECT_EQ(std::string(leptris_element_name(c2)), "c");
55
+ EXPECT_EQ(leptris_element_child(root, 2), nullptr);
56
+ leptris_document_free(doc);
57
+ }
58
+
59
+ TEST(ElementChildCache, AttrFreeParentLazyCacheStaysMutationSafe) {
60
+ /* Lane-18 round 15: attribute-free elements carry no ns_cache
61
+ * from the parser (the cache exists only when the parse stamped
62
+ * xmlns/prefix state), so the round-13 resume slot never engaged
63
+ * for them and every indexed read re-walked from the first
64
+ * child. child() now materializes the cache lazily. The mutation
65
+ * contract must hold for that lazily-created cache exactly as it
66
+ * does for parse-created ones. */
67
+ LeptrisStatus st;
68
+ LeptrisDocument doc = leptris_parse_string(
69
+ "<r><a/><b/><c/></r>", 19, &st);
70
+ ASSERT_NE(doc, nullptr);
71
+ LeptrisElement root = leptris_document_root(doc);
72
+ ASSERT_NE(root, nullptr);
73
+ EXPECT_EQ(leptris_element_attribute(root, "id"), nullptr);
74
+
75
+ /* Warm the iteration slot through the lazy path. */
76
+ for (int i = 0; i < 3; i++) {
77
+ LeptrisElement c = leptris_element_child(root, (size_t)i);
78
+ ASSERT_NE(c, nullptr) << "i=" << i;
79
+ }
80
+
81
+ LeptrisDocument fresh = leptris_document_create();
82
+ LeptrisElement z = leptris_element_create(fresh, "z");
83
+ ASSERT_NE(z, nullptr);
84
+ ASSERT_EQ(leptris_element_prepend_child(root, z), LEPTRIS_OK);
85
+
86
+ const char* names[] = {"z", "a", "b", "c"};
87
+ for (int i = 0; i < 4; i++) {
88
+ LeptrisElement c = leptris_element_child(root, (size_t)i);
89
+ ASSERT_NE(c, nullptr) << "i=" << i;
90
+ EXPECT_EQ(std::string(leptris_element_name(c)), names[i]) << "i=" << i;
91
+ }
92
+
93
+ LeptrisElement b = leptris_element_child(root, 2);
94
+ ASSERT_NE(b, nullptr);
95
+ ASSERT_EQ(leptris_element_remove_child(root, b), LEPTRIS_OK);
96
+ LeptrisElement c = leptris_element_child(root, 2);
97
+ ASSERT_NE(c, nullptr);
98
+ EXPECT_EQ(std::string(leptris_element_name(c)), "c");
99
+ EXPECT_EQ(leptris_element_child(root, 3), nullptr);
100
+ leptris_document_free(fresh);
101
+ leptris_document_free(doc);
102
+ }
@@ -313,12 +313,16 @@ TEST(PerfRegression, IndexedChildAccessDoesNotRegress) {
313
313
  if (s < small) small = s;
314
314
  if (l < large) large = l;
315
315
  }
316
- /* 3x size: O(N^2) sweep => large/small ~ 9x; an O(N^3)
317
- * regression reaches ~27x. Budget 18x separates both with
318
- * margin on loaded runners and under ASAN. */
319
- EXPECT_LT(large, 18.0 * small)
320
- << "Indexed child access complexity regression: 75-child sweep "
321
- << large << " us vs 25-child sweep " << small << " us";
316
+ /* 3x size: a LINEAR sweep (the round-13 resume slot makes a
317
+ * sequential indexed read O(1) per step; round 15 extends it to
318
+ * attr-free parents, whose cache child() materializes lazily)
319
+ * measures large/small ~ 3x. The old quadratic walk measures
320
+ * ~9x; an O(N^3) regression reaches ~27x. Budget 6x separates
321
+ * linear from quadratic with margin on loaded runners. */
322
+ EXPECT_LT(large, 6.0 * small)
323
+ << "Indexed child access lost the O(1)-per-step resume "
324
+ << "(quadratic walk back?): 75-child sweep " << large
325
+ << " us vs 25-child sweep " << small << " us";
322
326
  }
323
327
 
324
328
  /* Template dispatch must scale with the CANDIDATE'S subtree, not
@@ -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.265",
4
+ "version": "1.9.268",
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.265.0
4
+ version: 1.9.268.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -730,6 +730,7 @@ files:
730
730
  - vendor-src/libleptris/test/dom/test_digest.cpp
731
731
  - vendor-src/libleptris/test/dom/test_document_children.cpp
732
732
  - vendor-src/libleptris/test/dom/test_dom.cpp
733
+ - vendor-src/libleptris/test/dom/test_element_children.cpp
733
734
  - vendor-src/libleptris/test/dom/test_map_lifecycle.cpp
734
735
  - vendor-src/libleptris/test/dom/test_node_surface_parity.cpp
735
736
  - vendor-src/libleptris/test/dom/test_text_borrowed.cpp