leptris 1.9.210.1-arm-linux → 1.9.216.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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/Rakefile +1 -1
  4. data/ext/leptris/native/native.c +22 -7
  5. data/lib/leptris/version.rb +1 -1
  6. data/lib/leptris/xml/descriptor.rb +51 -13
  7. data/lib/leptris/xml/ffi.rb +49 -2
  8. data/lib/leptris/xml/native.so +0 -0
  9. data/lib/leptris/xml/plan_value.rb +192 -24
  10. data/lib/libleptris.so +0 -0
  11. data/vendor-src/README.md +1 -1
  12. data/vendor-src/libleptris/CHANGELOG.md +118 -0
  13. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  14. data/vendor-src/libleptris/TODO.max-perf/00-INDEX.md +4 -0
  15. data/vendor-src/libleptris/TODO.max-perf/interleaved-parse.md +67 -0
  16. data/vendor-src/libleptris/benchmarks/CMakeLists.txt +13 -0
  17. data/vendor-src/libleptris/benchmarks/parse/bench_parse_ab.cpp +83 -0
  18. data/vendor-src/libleptris/src/include/leptris/descriptor.h +58 -0
  19. data/vendor-src/libleptris/src/include/leptris.h +30 -0
  20. data/vendor-src/libleptris/src/leptris/descriptor/plan.c +447 -27
  21. data/vendor-src/libleptris/src/leptris/dom/element_query.c +28 -0
  22. data/vendor-src/libleptris/src/leptris/dom/root_doc_map.c +44 -8
  23. data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +658 -0
  24. data/vendor-src/libleptris/src/leptris/leptris_internal.h +12 -0
  25. data/vendor-src/libleptris/src/leptris/xpath/evaluator.c +11 -4
  26. data/vendor-src/libleptris/src/leptris/xpath/evaluator_internal.h +3 -0
  27. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +790 -53
  28. data/vendor-src/libleptris/src/leptris/xpath/evaluator_path.c +69 -3
  29. data/vendor-src/libleptris/src/leptris/xpath/evaluator_types.c +48 -0
  30. data/vendor-src/libleptris/src/leptris/xpath/functions.c +20 -3
  31. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +502 -29
  32. data/vendor-src/libleptris/src/leptris/xpath/parser.c +341 -21
  33. data/vendor-src/libleptris/src/leptris/xpath/vm.c +3 -1
  34. data/vendor-src/libleptris/src/leptris/xpath/xpath_internal.h +8 -1
  35. data/vendor-src/libleptris/src/leptris/xpath/xpath_public.c +11 -3
  36. data/vendor-src/libleptris/src/leptris/xpath/xpath_variables.c +9 -0
  37. data/vendor-src/libleptris/src/leptris/xquery/xquery.c +1048 -138
  38. data/vendor-src/libleptris/src/leptris/xslt/xslt_exec.c +7 -3
  39. data/vendor-src/libleptris/src/leptris/xslt/xslt_internal.h +9 -0
  40. data/vendor-src/libleptris/test/CMakeLists.txt +1 -0
  41. data/vendor-src/libleptris/test/abi/test_descriptor.cpp +294 -2
  42. data/vendor-src/libleptris/test/dom/test_dom.cpp +87 -0
  43. data/vendor-src/libleptris/test/parser/test_interleaved.cpp +223 -0
  44. data/vendor-src/libleptris/test/xpath/test_xpath.cpp +4 -1
  45. data/vendor-src/libleptris/test/xpath/test_xslt.cpp +1 -1
  46. data/vendor-src/libleptris/test/xquery/qt3/README.md +12 -0
  47. data/vendor-src/libleptris/test/xquery/qt3/docs/works-mod.xml +76 -0
  48. data/vendor-src/libleptris/test/xquery/qt3/fn/day-from-date.xml +249 -0
  49. data/vendor-src/libleptris/test/xquery/qt3/fn/day-from-dateTime.xml +249 -0
  50. data/vendor-src/libleptris/test/xquery/qt3/fn/function-arity.xml +280 -0
  51. data/vendor-src/libleptris/test/xquery/qt3/fn/function-name.xml +324 -0
  52. data/vendor-src/libleptris/test/xquery/qt3/fn/hours-from-dateTime.xml +249 -0
  53. data/vendor-src/libleptris/test/xquery/qt3/fn/hours-from-time.xml +249 -0
  54. data/vendor-src/libleptris/test/xquery/qt3/fn/implicit-timezone.xml +249 -0
  55. data/vendor-src/libleptris/test/xquery/qt3/fn/minutes-from-dateTime.xml +250 -0
  56. data/vendor-src/libleptris/test/xquery/qt3/fn/minutes-from-time.xml +249 -0
  57. data/vendor-src/libleptris/test/xquery/qt3/fn/month-from-date.xml +249 -0
  58. data/vendor-src/libleptris/test/xquery/qt3/fn/month-from-dateTime.xml +249 -0
  59. data/vendor-src/libleptris/test/xquery/qt3/fn/parse-ietf-date.xml +1268 -0
  60. data/vendor-src/libleptris/test/xquery/qt3/fn/seconds-from-dateTime.xml +250 -0
  61. data/vendor-src/libleptris/test/xquery/qt3/fn/seconds-from-time.xml +249 -0
  62. data/vendor-src/libleptris/test/xquery/qt3/fn/timezone-from-date.xml +333 -0
  63. data/vendor-src/libleptris/test/xquery/qt3/fn/timezone-from-dateTime.xml +250 -0
  64. data/vendor-src/libleptris/test/xquery/qt3/fn/timezone-from-time.xml +249 -0
  65. data/vendor-src/libleptris/test/xquery/qt3/fn/year-from-date.xml +253 -0
  66. data/vendor-src/libleptris/test/xquery/qt3/fn/year-from-dateTime.xml +249 -0
  67. data/vendor-src/libleptris/test/xquery/qt3/op/add-dayTimeDuration-to-date.xml +467 -0
  68. data/vendor-src/libleptris/test/xquery/qt3/op/add-dayTimeDuration-to-dateTime.xml +436 -0
  69. data/vendor-src/libleptris/test/xquery/qt3/op/add-dayTimeDuration-to-time.xml +432 -0
  70. data/vendor-src/libleptris/test/xquery/qt3/op/add-dayTimeDurations.xml +850 -0
  71. data/vendor-src/libleptris/test/xquery/qt3/op/add-yearMonthDuration-to-date.xml +441 -0
  72. data/vendor-src/libleptris/test/xquery/qt3/op/add-yearMonthDuration-to-dateTime.xml +305 -0
  73. data/vendor-src/libleptris/test/xquery/qt3/op/add-yearMonthDurations.xml +440 -0
  74. data/vendor-src/libleptris/test/xquery/qt3/op/dayTimeDuration-greater-than.xml +859 -0
  75. data/vendor-src/libleptris/test/xquery/qt3/op/dayTimeDuration-less-than.xml +685 -0
  76. data/vendor-src/libleptris/test/xquery/qt3/op/divide-dayTimeDuration-by-dayTimeDuration.xml +410 -0
  77. data/vendor-src/libleptris/test/xquery/qt3/op/divide-dayTimeDuration.xml +807 -0
  78. data/vendor-src/libleptris/test/xquery/qt3/op/divide-yearMonthDuration-by-yearMonthDuration.xml +377 -0
  79. data/vendor-src/libleptris/test/xquery/qt3/op/divide-yearMonthDuration.xml +499 -0
  80. data/vendor-src/libleptris/test/xquery/qt3/op/duration-equal.xml +1477 -0
  81. data/vendor-src/libleptris/test/xquery/qt3/op/multiply-dayTimeDuration.xml +697 -0
  82. data/vendor-src/libleptris/test/xquery/qt3/op/multiply-yearMonthDuration.xml +685 -0
  83. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-dayTimeDuration-from-date.xml +377 -0
  84. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-dayTimeDuration-from-dateTime.xml +376 -0
  85. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-dayTimeDuration-from-time.xml +441 -0
  86. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-dayTimeDurations.xml +971 -0
  87. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-yearMonthDuration-from-date.xml +426 -0
  88. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-yearMonthDuration-from-dateTime.xml +377 -0
  89. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-yearMonthDurations.xml +531 -0
  90. data/vendor-src/libleptris/test/xquery/qt3/op/yearMonthDuration-greater-than.xml +542 -0
  91. data/vendor-src/libleptris/test/xquery/qt3/op/yearMonthDuration-less-than.xml +542 -0
  92. data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause/books.xml +26 -0
  93. data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause/products.xml +33 -0
  94. data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause/sales-records.xml +48 -0
  95. data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause/stores.xml +19 -0
  96. data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause.xml +749 -0
  97. data/vendor-src/libleptris/test/xquery/qt3/prod/IfExpr.xml +479 -0
  98. data/vendor-src/libleptris/test/xquery/qt3/prod/OrderByClause/orderData.xml +114 -0
  99. data/vendor-src/libleptris/test/xquery/qt3/prod/OrderByClause.xml +2491 -0
  100. data/vendor-src/libleptris/test/xquery/qt3/prod/SwitchExpr.xml +425 -0
  101. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +400 -6
  102. data/vendor-src/libleptris/vcpkg.json +1 -1
  103. metadata +59 -2
@@ -1,5 +1,123 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.216] - 2026-09-21
4
+
5
+ ### Fixed
6
+
7
+ - **#1242** — generation-checked root→document memo. The TLS
8
+ last-hit memo was keyed on the root's address alone; the iterparse
9
+ yield path primed it with a subtree the consumer frees, and the
10
+ allocator recycled that address for the NEXT document's root —
11
+ the stale pair resolved all-NULL attribute reads and
12
+ `set_root`'s "element belongs to a different document" false
13
+ positive. Every map mutation now bumps a generation counter; a
14
+ memo hit requires the prime-time generation to match, so
15
+ invalidation is deterministic instead of allocator luck. The
16
+ transform steady state the #682 lever targets still hits (one
17
+ pointer compare); TDD-covered by
18
+ `DomBasics.RootDocMemoDiesOnAnyMapMutationAfterPrime`.
19
+
20
+
21
+
22
+ ## [1.9.215] - 2026-09-21
23
+
24
+ ### Added — XQuery order-by (QT3 OrderByClause 138/205)
25
+
26
+ - Expression-level `order by` on FLWOR: string-typed keys compare by
27
+ codepoint (`collation`-shaped strmode mask), repeated `order by`
28
+ clauses chain as sequential stable sorts keyed by clause index.
29
+ - libxml2 number-spelling parity stays pinned for XPath 1.0/XSLT;
30
+ XQuery gets its own channel (integral plain ≤1e18, plain
31
+ fractions ≥1e-18, INF/-INF) selected at eval entry.
32
+ - `/a/b/string()` fn-step rehang + `//` twin root-match fast path
33
+ (#692); base-uri declaration support.
34
+
35
+ ### Fixed
36
+
37
+ - ASAN chain from the orderby gate: deep-copy owned head nodesets
38
+ in `evaluate_location_path`; `set_nodeset` frees a prior nodeset
39
+ instead of leaking; tuple rebind clones attr/ns; variable lookup
40
+ deep-copies owned attributes/namespaces.
41
+
42
+
43
+
44
+ ## [1.9.214] - 2026-09-21
45
+
46
+ ### Added — QT3 lane-15 core: switch / group-by / if (lever 6 stage-2)
47
+
48
+ Two batches close the lane-15 expression families:
49
+
50
+ - **fn-items (#1268)**: `fn:function-name` / `fn:function-arity` gated on the vendored sets; `fn:function-name` returns the lexical QName verbatim (no `fn:` prefixing).
51
+ - **lane-15 core (#1270)**: `prod/SwitchExpr` (16), `prod/GroupByClause` (20, works-mod + docvar externals) and `prod/IfExpr` (24) — every adopted case agrees; full suite 1640/1640.
52
+
53
+ Engine fixes the corpus drove:
54
+
55
+ - **group by**: bare `$k` grouping variables, comma-separated multi-spec grouping on the key tuple, optional `as SequenceType`/`collation` specs, WHERE-after-GROUP-BY filtering the grouped tuples.
56
+ - **ctor-enclosed FLWOR** (`<out>{ for … group by … }</out>`): the ctor is hoisted and wraps the result sequence once.
57
+ - **node lifetime**: attribute/namespace nodes in FLWOR tuples and LET bindings are cloned and tuple-owned (group-by keys dangled after unbind and read empty); external-variable selects evaluate on the query context so `parse-xml()` documents stay anchored.
58
+ - **constructors**: nested direct ctors inside enclosed expressions splice recursively; `text{(a,b,c)}` joins atomics with spaces; 0-arity `string()` is kind-aware for attribute/text contexts; fn steps (`E/string()`) map per member after a filter expr; the `!` map no longer frees its left nodeset before mapping.
59
+ - **`prefix:*` vs multiply**: the NCNAME+STAR token pair is disambiguated from the source text (`k * 2` mis-parsed as `k:*`).
60
+ - multi-case `switch` (`case A case B return R`); `xs:float` registered; two `%lld` 32-bit-long print fixes (xsl:number, month-shift seconds).
61
+
62
+
63
+
64
+ ## [1.9.213] - 2026-09-21
65
+
66
+ ### Added — the op:date and yearMonth families (#1182, lever 6)
67
+
68
+ - **op-date arithmetic (#1265)**: date/time/dateTime +/- dayTimeDuration — 6 QT3 sets, 128/128. Shared lexical shifter (`leptris_dt_shift`), time arithmetic wrapping the day, ISO date-shaped string comparisons. Fixed two corpus-caught bugs: zoned times misclassified as dateTimes, and dateTime results printing the input's seconds instead of the shifted instant's.
69
+ - **yearMonthDuration family (#1266)**: 11 QT3 sets, 264/264 — months arithmetic (PnYnM canonical, llrint tie rule), month-end clamping for date/dateTime shifts (leap-aware), negative-year printing. The lt/gt sets rode the existing months comparison hook untouched.
70
+
71
+ Both batches survived the 32-bit-long portability classes (MSVC/ILP32): int64-range instant math and every %lld argument now use long long — the CI legs caught three occurrences of the width/specifier mismatch family.
72
+
73
+ ## [1.9.212] - 2026-09-21
74
+
75
+ ### Added — the QT3 date/time family (lever 6 stage-2, #1182)
76
+
77
+ Three corpus-driven batches land the date/time surface with the
78
+ W3C QT3 suite as the gate:
79
+
80
+ - **Extractors (#1261)**: 12 vendored test-sets (300/300) —
81
+ year/month/day-from-date{,Time}, hours/minutes/seconds-from-
82
+ {time,dateTime} — plus five language fixes: function-call
83
+ operands for eq/ne/lt/le/gt/ge (unprefixed and fn:-prefixed),
84
+ the `idiv` operator, unary `+`, the timezone scan in date
85
+ parsing (zoned values were treated as naive), and the
86
+ empty-sequence/fractional-seconds extractor contracts.
87
+ - **Timezone + RFC 5322 (#1262)**: 5 sets (90/90) —
88
+ timezone-from-* accessors and fn:parse-ietf-date (both lexical
89
+ orders, obsolete US zones, verbatim fractions). The QT3 harness
90
+ grew inline param environments and external-declaration
91
+ prepending.
92
+ - **op:dayTimeDuration value model (#1263)**: 8 sets (287/287) —
93
+ duration arithmetic (+, -, *, div), comparisons, min/max over
94
+ duration sequences, canonical formatting, FODT0002 range errors
95
+ (including a fixed fortified-runtime abort on giant values), and
96
+ adjust-* attaching the implicit timezone to naive values.
97
+
98
+ ## [1.9.211] - 2026-09-20
99
+
100
+ ### Performance
101
+
102
+ - **Interleaved parse lane, slices 1-3a (#1258).** The first
103
+ pugixml-style interleaved rewrite landed behind
104
+ `LEPTRIS_INTERLEAVED=1` (opt-in; every input outside the covered
105
+ surface bails to the classic parser before any state). The lane
106
+ streams `IlRec`/`IlAttr` records with buffer-relative views, then
107
+ replays the classic public stores in one linear post-pass:
108
+ sentinel-discipline scans (no per-byte bounds tests; fused 48-byte
109
+ quote/'&'/whitespace probes with SIMD tails), upfront stream
110
+ capacity from the input size, and element/attribute names as
111
+ NUL'd views into the parse scratch (no per-name copies). Parity is
112
+ gated by a dedicated spec plus running the FULL test suite with
113
+ the lane forced on: 1633/1633 identical. `LEPTRIS_IL_STATS=1`
114
+ prints an engagement canary for diagnosing whether the lane ran.
115
+ - **XSLT append-tail memo (#1259).** `out_append_child` keeps a
116
+ validated single-slot (parent, last-child) hint and feeds
117
+ `leptris_element_append_child_tail` — result-tree placement no
118
+ longer re-walks sibling chains (the hottest dispatch-heavy
119
+ profile leaf, ~17% of samples).
120
+
3
121
  ## [1.9.210] - 2026-09-20
4
122
 
5
123
  ### Added
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.210
6
+ VERSION 1.9.216
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -24,3 +24,7 @@ Next portability gate: s390x big-endian (#1194, PR #1202).
24
24
  The node-layout fork (row 4 successor, owner green-lit 2026-09-20):
25
25
  TODO.max-perf/node-layout-fork.md — 5 slices, addresses #1222 +
26
26
  #1179 residual + #1238 parse side.
27
+
28
+ The interleaved-parse rewrite (owner-directed 2026-09-20):
29
+ TODO.max-perf/interleaved-parse.md — the #1222 step-change; slices 0-4,
30
+ each CI-benchmark-gated.
@@ -0,0 +1,67 @@
1
+ # The interleaved-parse rewrite (the pugixml-class lane)
2
+
3
+ Status: PLAN (2026-09-20, owner directive). Supersedes the fork's
4
+ remaining slices for the XML parse path. Target: close the #1222
5
+ large-document gap (pugixml 2-5.9x ahead on CI) at the SOURCE.
6
+
7
+ ## Why the current lane can't get there
8
+
9
+ The no-LTO dissection (2026-09-20) shows direct_parse is at its
10
+ representation optimum: fused 48B probe + SIMD value scan, packed
11
+ attr tail, O(1) wiring, offset edges, root-only registration. The
12
+ remaining per-item cost is the representation itself: per-attr
13
+ 40B struct stores inline in the parse loop, the scratch
14
+ NUL-terminate/restore dance, per-element ns/line bookkeeping, and
15
+ the O(K) dup-detection walk.
16
+
17
+ ## The pugixml model (what we're adopting)
18
+
19
+ One immutable input (already ours, #1125) + ONE node region + ONE
20
+ stream region. The parse loop ONLY appends to streams; nothing is
21
+ finalized inline:
22
+
23
+ 1. **SOA attr streams** — per element, attrs land in parallel
24
+ arrays: name_off[4B] / value_off[4B] / len8[len pairs] into the
25
+ string stream. ~12-16B per attr, sequential stores, no structs
26
+ built during parse.
27
+ 2. **Length-based views everywhere** — names and values are
28
+ (offset, len) into the pristine buffer. NO NUL-termination, NO
29
+ scratch mutation, NO restore pass (the current lane's biggest
30
+ hidden cost).
31
+ 3. **Element records as pure data** — a 32-48B record per element
32
+ (name view, first_attr index, parent/first/next indices, line)
33
+ bump-carved from the node region (doc_block, slice 1 landed).
34
+ 4. **Deferred passes** — dup detection, ns stamping, hash
35
+ sentinel: all become ONE linear post-pass sweep over the
36
+ streams (cache-sequential), building the public
37
+ leptris_attribute structs. The public API, DOM semantics, and
38
+ every consumer see IDENTICAL trees.
39
+
40
+ ## Slices (one PR each, CI-benchmark-gated)
41
+
42
+ 0. **A/B harness**: bench_parse_ab in benchmarks/ — the attr/text
43
+ shapes at 1.5/5/13MB, leptris vs pugixml vs libxml2, run by the
44
+ CI Benchmark legs on QUIET runners (the local Mac under load
45
+ 343 produced 46-85 MB/s phantom numbers — measurements gate on
46
+ CI only). PR with this plan.
47
+ 1. **The dp_il lane skeleton** (flag off): streams + records +
48
+ post-pass building the public tree for well-formed, DTD-free
49
+ input. Gate: parity specs byte-identical to the current lane;
50
+ CI A/B numbers recorded.
51
+ 2. **Tokenizer port**: the SIMD scan helpers onto the stream
52
+ appends. Gate: attr shape ≤1.5x pugixml, text ≤1.25x on CI.
53
+ 3. **Feature coverage**: DTD/entity/namespace/CDATA/PI routes
54
+ through the lane (fall back to the classic lane per-feature,
55
+ never wrong). Gate: full ctest + html5lib-style parity for the
56
+ XML suite (W3C + libxml2 corpus green).
57
+ 4. **Default flip**: LEPTRIS_PARSE_INTERLEAVED=1 default, classic
58
+ lane behind an env escape hatch. Gate: all 24 CI legs + a
59
+ release cut with the CI A/B table in the notes.
60
+
61
+ ## Invariants (non-negotiable)
62
+
63
+ - Public API/ABI unchanged; every existing spec passes unmodified.
64
+ - The classic lane stays until slice 4 ships AND one full release
65
+ runs clean.
66
+ - Zero-copy views remain primary; the pristine buffer is never
67
+ mutated by the interleaved lane.
@@ -232,6 +232,19 @@ leptris_add_benchmark(bench_xslt_subtree_copy xslt/bench_subtree_copy.c utils.c)
232
232
  # ----------------------------------------------------------------------------
233
233
  leptris_add_benchmark(bench_iterparse sax/bench_iterparse.c)
234
234
 
235
+ # Interleaved-parse gate (#1222): leptris vs pugixml on identical
236
+ # fixtures, run by the CI Benchmark legs (quiet runners).
237
+ if(PUGIXML_AVAILABLE)
238
+ leptris_add_benchmark(bench_parse_ab CXX parse/bench_parse_ab.cpp)
239
+ target_compile_definitions(bench_parse_ab PRIVATE HAVE_PUGIXML=1)
240
+ if(PUGIXML_TARGET)
241
+ target_link_libraries(bench_parse_ab PRIVATE ${PUGIXML_TARGET})
242
+ else()
243
+ target_include_directories(bench_parse_ab PRIVATE ${PUGIXML_INCLUDE_DIR})
244
+ target_link_libraries(bench_parse_ab PRIVATE ${PUGIXML_LIBRARY})
245
+ endif()
246
+ endif()
247
+
235
248
  # ----------------------------------------------------------------------------
236
249
  # SAX benchmarks (leptris vs libxml2). TODO 101.
237
250
  # ----------------------------------------------------------------------------
@@ -0,0 +1,83 @@
1
+ /* bench_parse_ab.cpp — the interleaved-parse lane's gate (#1222).
2
+ *
3
+ * Same fixtures, sizes, and shapes for leptris and pugixml,
4
+ * best-of-5 each. The CI Benchmark legs run this on quiet
5
+ * runners; the interleaved lane's slices are gated on THESE
6
+ * numbers, never a loaded dev machine (2026-09-20: a Mac at load
7
+ * 340 measured a 472 MB/s build at 46-85).
8
+ */
9
+ #include <stdio.h>
10
+ #include <stdlib.h>
11
+ #include <string.h>
12
+ #include <time.h>
13
+ #include "leptris.h"
14
+ #ifdef HAVE_PUGIXML
15
+ #include "pugixml.hpp"
16
+ using namespace pugi;
17
+ #endif
18
+
19
+ static double now_us(void) {
20
+ struct timespec ts;
21
+ clock_gettime(CLOCK_MONOTONIC, &ts);
22
+ return ts.tv_sec * 1e6 + ts.tv_nsec / 1e3;
23
+ }
24
+
25
+ static size_t gen(char* out, size_t target, int text_shape) {
26
+ size_t len = 0;
27
+ len += (size_t)sprintf(out + len, "<root>");
28
+ int i = 0;
29
+ if (text_shape) {
30
+ while (len < target)
31
+ len += (size_t)sprintf(out + len,
32
+ "<p>The quick brown fox jumps over the lazy dog %d.</p>", i++);
33
+ } else {
34
+ while (len < target)
35
+ len += (size_t)sprintf(out + len,
36
+ "<item id=\"%d\" class=\"c%d\" state=\"on\" ref=\"%d\">v %d</item>",
37
+ i, i % 7, i * 3, i++);
38
+ }
39
+ len += (size_t)sprintf(out + len, "</root>");
40
+ return len;
41
+ }
42
+
43
+ int main(void) {
44
+ const size_t sizes[3] = {1500 * 1024, 5000 * 1024, 13000 * 1024};
45
+ const char* names[2] = {"attr", "text"};
46
+ printf("%-6s %9s %12s %12s\n", "shape", "bytes", "leptris", "pugixml");
47
+ for (int s = 0; s < 2; s++) {
48
+ for (size_t si = 0; si < 3; si++) {
49
+ char* doc = (char*)malloc(sizes[si] + 2048);
50
+ if (!doc) return 1;
51
+ size_t len = gen(doc, sizes[si], s);
52
+ double best = 1e18;
53
+ for (int r = 0; r < 5; r++) {
54
+ double a = now_us();
55
+ LeptrisStatus st = LEPTRIS_OK;
56
+ LeptrisDocument d = leptris_parse_string(doc, len, &st);
57
+ double b = now_us();
58
+ if (!d) { printf("PARSE FAILED\n"); return 1; }
59
+ leptris_document_free(d);
60
+ if (b - a < best) best = b - a;
61
+ }
62
+ double mbps = len / 1048576.0 / (best / 1e6);
63
+ #ifdef HAVE_PUGIXML
64
+ double pbest = 1e18;
65
+ for (int r = 0; r < 5; r++) {
66
+ double a = now_us();
67
+ xml_document xdoc;
68
+ xml_parse_result res = xdoc.load_buffer(doc, len);
69
+ double b = now_us();
70
+ if (!res) { printf("PUGIXML FAILED: %s\n", res.description()); return 1; }
71
+ if (b - a < pbest) pbest = b - a;
72
+ }
73
+ double pmbps = len / 1048576.0 / (pbest / 1e6);
74
+ printf("%-6s %9zu %10.1f %10.1f MB/s (pugixml %.2fx)\n",
75
+ names[s], len, mbps, pmbps, pmbps / mbps);
76
+ #else
77
+ printf("%-6s %9zu %10.1f %10s\n", names[s], len, mbps, "-");
78
+ #endif
79
+ free(doc);
80
+ }
81
+ }
82
+ return 0;
83
+ }
@@ -62,6 +62,13 @@ typedef enum {
62
62
  #define LEPTRIS_PLAN_FLAG_ORDERED 0x2u /* matches within a row keep document order (default) */
63
63
  #define LEPTRIS_PLAN_FLAG_CDATA 0x4u /* CDATA sections count as text runs */
64
64
  #define LEPTRIS_PLAN_FLAG_NS_LENIENT 0x8u /* #754: bind out-of-namespace children anyway */
65
+ /* #1273: when set, the walk ALSO emits unmatched sibling non-element
66
+ * nodes (text runs / comments / PIs) as SCALAR values with
67
+ * `position` and `node_kind` populated, so ordered/mixed-content
68
+ * hosts can rebuild `element_order` without re-parsing the source.
69
+ * Document-order identity already covers element children; the
70
+ * spine closes the gap for runs between matched elements. */
71
+ #define LEPTRIS_PLAN_FLAG_EMIT_ORDER_SPINE 0x10u
65
72
 
66
73
  /* Namespace matching form for binding an element. */
67
74
  typedef enum {
@@ -70,10 +77,24 @@ typedef enum {
70
77
  LEPTRIS_PLAN_NS_ANY = 2
71
78
  } LeptrisPlanNsForm;
72
79
 
80
+ typedef struct {
81
+ const char* wire_name; /* XML attribute name as it appears on the wire */
82
+ const char* expected_value; /* string-equal match (non-nil); host-owned */
83
+ } leptris_attr_predicate;
84
+
73
85
  typedef struct {
74
86
  const char* wire_name; /* XML attribute name as it appears on the wire */
75
87
  uint8_t kind; /* LeptrisPlanKind: SCALAR | COLLECTION | CALLBACK */
76
88
  uint8_t type_tag; /* host-defined; echoed back verbatim */
89
+ /* #1272: filter rows to require every (attr_name, expected_value)
90
+ * pair (AND across pairs). Multiple rows with the same wire_name
91
+ * and different predicates partition the match space
92
+ * (exclusive — first matching row wins per occurrence, no
93
+ * double-capture). Trailing additive field; `predicate_count = 0`
94
+ * preserves historical behavior. Strings deep-copied at build. */
95
+ uint16_t predicate_count;
96
+ uint16_t pad_pred;
97
+ const leptris_attr_predicate* predicates;
77
98
  } leptris_attr_plan;
78
99
 
79
100
  typedef struct {
@@ -91,6 +112,11 @@ typedef struct {
91
112
  uint8_t ns_form; /* LeptrisPlanNsForm */
92
113
  uint8_t pad0;
93
114
  const char* ns_uri; /* LEPTRIS_PLAN_NS_EXACT only */
115
+ /* #1272: element-side predicates. Same semantics as attr_predicate
116
+ * (AND across pairs, exclusive same-name rows). */
117
+ uint16_t predicate_count;
118
+ uint16_t pad_pred;
119
+ const leptris_attr_predicate* predicates;
94
120
  } leptris_child_plan;
95
121
 
96
122
  typedef struct {
@@ -168,6 +194,14 @@ LEPTRIS_API const char* leptris_plan_value_string(const LeptrisPlanResult v);
168
194
  LEPTRIS_API size_t leptris_plan_value_length(const LeptrisPlanResult v);
169
195
  /* CALLBACK: document byte offset of the source node (0 unknown). */
170
196
  LEPTRIS_API size_t leptris_plan_value_position(const LeptrisPlanResult v);
197
+ /* #1273: LEPTRIS_NODE_TYPE_* of the value's source node, or 0 for
198
+ * ELEMENT/COLLECTION wrappers and synthesized text. */
199
+ LEPTRIS_API uint8_t leptris_plan_value_node_kind(const LeptrisPlanResult v);
200
+ /* #1273: dense sibling rank inside the producing element (0 if
201
+ * unranked). Useful when byte offsets are 0 (mutated docs) or
202
+ * when hosts sort by document order without byte-offset helpers. */
203
+ LEPTRIS_API uint32_t leptris_plan_value_order_index(
204
+ const LeptrisPlanResult v);
171
205
  /* ELEMENT: child-value count. COLLECTION: item count. */
172
206
  LEPTRIS_API size_t leptris_plan_value_count(const LeptrisPlanResult v);
173
207
  /* ELEMENT child value / COLLECTION item at i; NULL out of range. */
@@ -177,6 +211,30 @@ LEPTRIS_API LeptrisPlanResult leptris_plan_value_at(const LeptrisPlanResult v,
177
211
  LEPTRIS_API const char* leptris_plan_value_attribute(const LeptrisPlanResult v,
178
212
  const char* wire_name);
179
213
 
214
+ /* #1269a in-pass type execution (#1269a): for SCALAR/RAW values
215
+ * whose producing row carried type_tag in {1=int, 2=float, 3=bool},
216
+ * the walk parses the string once and stores typed values. The str
217
+ * field is always populated for backward compatibility. Accessors
218
+ * return 0 on success (matches type) and non-zero on parse failure
219
+ * (host may fall back to `string`). NULL in or non-numeric value is
220
+ * a failure. */
221
+ LEPTRIS_API int leptris_plan_value_int(const LeptrisPlanResult v,
222
+ int64_t* out);
223
+ LEPTRIS_API int leptris_plan_value_float(const LeptrisPlanResult v,
224
+ double* out);
225
+ /* `true` / `1` (case-insensitive) -> 1, `false` / `0` -> 0, anything
226
+ * else leaves *out untouched and returns non-zero. */
227
+ LEPTRIS_API int leptris_plan_value_bool(const LeptrisPlanResult v, int* out);
228
+
229
+ /* #1269b fused parse+walk entry. v1 = parse_string + walk(root) +
230
+ * free(doc) — byte-parity with Descriptor#materialize. Returns the
231
+ * same result type as leptris_plan_walk; status reports
232
+ * LEPTRIS_ERROR_PARSE on syntax error. NULL doc / plan / status
233
+ * handled. The caller still owns plan lifetime. */
234
+ LEPTRIS_API LeptrisPlanResult leptris_plan_materialize(
235
+ const char* source, size_t source_len,
236
+ LeptrisPlan plan, LeptrisStatus* status);
237
+
180
238
  #ifdef __cplusplus
181
239
  }
182
240
  #endif
@@ -1846,6 +1846,36 @@ LEPTRIS_API const char* leptris_attribute_get_name(LeptrisAttribute attr);
1846
1846
  */
1847
1847
  LEPTRIS_API const char* leptris_attribute_get_value(LeptrisElement elem, LeptrisAttribute attr);
1848
1848
 
1849
+ /**
1850
+ * Bulk attribute read face (#1254): one C pass per element returns
1851
+ * names, values, AND attribute handles in parallel — the shape
1852
+ * bindings need to replace the 4-calls-per-attribute chain
1853
+ * (first / get_name / get_value / next) with a single dispatch.
1854
+ *
1855
+ * Names and values are the element's canonical attribute list in
1856
+ * source order (xmlns declarations excluded — they live in the
1857
+ * separate namespace channel, matching
1858
+ * leptris_element_first_attribute). Handles let mutation-capable
1859
+ * consumers keep native attribute references; pass NULL for any
1860
+ * column to skip it.
1861
+ *
1862
+ * @param elem Element
1863
+ * @param out_names Output array of name strings, or NULL to skip
1864
+ * @param out_values Output array of value strings (parallel), or
1865
+ * NULL to skip
1866
+ * @param out_attrs Output array of attribute handles (parallel), or
1867
+ * NULL to skip
1868
+ * @param max_count Capacity of the non-NULL output arrays
1869
+ * @return The total attribute count (min(total, max_count) copied)
1870
+ *
1871
+ * Memory: Strings and handles are owned by the element/document.
1872
+ * Do not free separately. Handles stay valid until the attribute
1873
+ * is removed or the document is freed.
1874
+ */
1875
+ LEPTRIS_API size_t leptris_element_attribute_pairs(
1876
+ LeptrisElement elem, const char** out_names, const char** out_values,
1877
+ LeptrisAttribute* out_attrs, size_t max_count);
1878
+
1849
1879
  /**
1850
1880
  * Get number of child elements
1851
1881
  *