leptris 1.9.210.0 → 1.9.213.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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.adoc +49 -11
  4. data/Rakefile +1 -1
  5. data/TODO.restructure/17-perf-drift-record.md +15 -0
  6. data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
  7. data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
  8. data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
  9. data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
  10. data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
  11. data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
  12. data/lib/leptris/vendor/s390x-linux-musl/libleptris.so +0 -0
  13. data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
  14. data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
  15. data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
  16. data/lib/leptris/version.rb +1 -1
  17. data/lib/leptris/xml/descriptor.rb +59 -2
  18. data/lib/leptris/xml/plan_value.rb +53 -2
  19. data/vendor-src/README.md +1 -1
  20. data/vendor-src/libleptris/CHANGELOG.md +57 -0
  21. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  22. data/vendor-src/libleptris/TODO.max-perf/00-INDEX.md +4 -0
  23. data/vendor-src/libleptris/TODO.max-perf/interleaved-parse.md +67 -0
  24. data/vendor-src/libleptris/benchmarks/CMakeLists.txt +13 -0
  25. data/vendor-src/libleptris/benchmarks/parse/bench_parse_ab.cpp +83 -0
  26. data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +658 -0
  27. data/vendor-src/libleptris/src/leptris/leptris_internal.h +1 -0
  28. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +368 -1
  29. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +466 -24
  30. data/vendor-src/libleptris/src/leptris/xpath/parser.c +69 -7
  31. data/vendor-src/libleptris/src/leptris/xpath/vm.c +3 -1
  32. data/vendor-src/libleptris/src/leptris/xpath/xpath_internal.h +8 -1
  33. data/vendor-src/libleptris/src/leptris/xslt/xslt_exec.c +5 -2
  34. data/vendor-src/libleptris/src/leptris/xslt/xslt_internal.h +9 -0
  35. data/vendor-src/libleptris/test/CMakeLists.txt +1 -0
  36. data/vendor-src/libleptris/test/parser/test_interleaved.cpp +223 -0
  37. data/vendor-src/libleptris/test/xpath/test_xpath.cpp +4 -1
  38. data/vendor-src/libleptris/test/xquery/qt3/README.md +12 -0
  39. data/vendor-src/libleptris/test/xquery/qt3/fn/day-from-date.xml +249 -0
  40. data/vendor-src/libleptris/test/xquery/qt3/fn/day-from-dateTime.xml +249 -0
  41. data/vendor-src/libleptris/test/xquery/qt3/fn/hours-from-dateTime.xml +249 -0
  42. data/vendor-src/libleptris/test/xquery/qt3/fn/hours-from-time.xml +249 -0
  43. data/vendor-src/libleptris/test/xquery/qt3/fn/implicit-timezone.xml +249 -0
  44. data/vendor-src/libleptris/test/xquery/qt3/fn/minutes-from-dateTime.xml +250 -0
  45. data/vendor-src/libleptris/test/xquery/qt3/fn/minutes-from-time.xml +249 -0
  46. data/vendor-src/libleptris/test/xquery/qt3/fn/month-from-date.xml +249 -0
  47. data/vendor-src/libleptris/test/xquery/qt3/fn/month-from-dateTime.xml +249 -0
  48. data/vendor-src/libleptris/test/xquery/qt3/fn/parse-ietf-date.xml +1268 -0
  49. data/vendor-src/libleptris/test/xquery/qt3/fn/seconds-from-dateTime.xml +250 -0
  50. data/vendor-src/libleptris/test/xquery/qt3/fn/seconds-from-time.xml +249 -0
  51. data/vendor-src/libleptris/test/xquery/qt3/fn/timezone-from-date.xml +333 -0
  52. data/vendor-src/libleptris/test/xquery/qt3/fn/timezone-from-dateTime.xml +250 -0
  53. data/vendor-src/libleptris/test/xquery/qt3/fn/timezone-from-time.xml +249 -0
  54. data/vendor-src/libleptris/test/xquery/qt3/fn/year-from-date.xml +253 -0
  55. data/vendor-src/libleptris/test/xquery/qt3/fn/year-from-dateTime.xml +249 -0
  56. data/vendor-src/libleptris/test/xquery/qt3/op/add-dayTimeDuration-to-date.xml +467 -0
  57. data/vendor-src/libleptris/test/xquery/qt3/op/add-dayTimeDuration-to-dateTime.xml +436 -0
  58. data/vendor-src/libleptris/test/xquery/qt3/op/add-dayTimeDuration-to-time.xml +432 -0
  59. data/vendor-src/libleptris/test/xquery/qt3/op/add-dayTimeDurations.xml +850 -0
  60. data/vendor-src/libleptris/test/xquery/qt3/op/add-yearMonthDuration-to-date.xml +441 -0
  61. data/vendor-src/libleptris/test/xquery/qt3/op/add-yearMonthDuration-to-dateTime.xml +305 -0
  62. data/vendor-src/libleptris/test/xquery/qt3/op/add-yearMonthDurations.xml +440 -0
  63. data/vendor-src/libleptris/test/xquery/qt3/op/dayTimeDuration-greater-than.xml +859 -0
  64. data/vendor-src/libleptris/test/xquery/qt3/op/dayTimeDuration-less-than.xml +685 -0
  65. data/vendor-src/libleptris/test/xquery/qt3/op/divide-dayTimeDuration-by-dayTimeDuration.xml +410 -0
  66. data/vendor-src/libleptris/test/xquery/qt3/op/divide-dayTimeDuration.xml +807 -0
  67. data/vendor-src/libleptris/test/xquery/qt3/op/divide-yearMonthDuration-by-yearMonthDuration.xml +377 -0
  68. data/vendor-src/libleptris/test/xquery/qt3/op/divide-yearMonthDuration.xml +499 -0
  69. data/vendor-src/libleptris/test/xquery/qt3/op/duration-equal.xml +1477 -0
  70. data/vendor-src/libleptris/test/xquery/qt3/op/multiply-dayTimeDuration.xml +697 -0
  71. data/vendor-src/libleptris/test/xquery/qt3/op/multiply-yearMonthDuration.xml +685 -0
  72. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-dayTimeDuration-from-date.xml +377 -0
  73. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-dayTimeDuration-from-dateTime.xml +376 -0
  74. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-dayTimeDuration-from-time.xml +441 -0
  75. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-dayTimeDurations.xml +971 -0
  76. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-yearMonthDuration-from-date.xml +426 -0
  77. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-yearMonthDuration-from-dateTime.xml +377 -0
  78. data/vendor-src/libleptris/test/xquery/qt3/op/subtract-yearMonthDurations.xml +531 -0
  79. data/vendor-src/libleptris/test/xquery/qt3/op/yearMonthDuration-greater-than.xml +542 -0
  80. data/vendor-src/libleptris/test/xquery/qt3/op/yearMonthDuration-less-than.xml +542 -0
  81. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +128 -3
  82. data/vendor-src/libleptris/vcpkg.json +1 -1
  83. metadata +47 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6a8b73b57e537bd42a2f62759cd8b36fb842ed36c5046894453ae4b6c1540ca0
4
- data.tar.gz: b089afc26fe2c7d8e8396a25a88c00baaa947c732896cf3aa70097c3b88b4302
3
+ metadata.gz: 708246fd0d69af25b4257d67e0c22a62a818c5aa7e523eb914628c011b6065e3
4
+ data.tar.gz: 386676e6428cbb3aa09b9bb490516e438465570302ac0024a0b13ebafc545d4b
5
5
  SHA512:
6
- metadata.gz: 1661a5db0ca010813f02bb90a24537fd92c5849cb797775ec7bb98c5217144bbdf95b8eca4ae3ec0a42a18f7cbebc57fd13b33500b1e402c69f903de4be85bff
7
- data.tar.gz: 6724e22c525398f6ecead93260b6716188242a5f3722bc29fc6810bb00d89f66e8fc2a512729a7915b3be001b0a7c9b3dbbc2250623a22b528a819cc3f7faa49
6
+ metadata.gz: 19826be07b804dca192cef9777f88f804b077c87a44cc0b24940509ae1db6787391c9e1db63acfad547fbeef910c03fc79ac345e064cb6b32a7ecedfdd9ec500
7
+ data.tar.gz: b24ae844a86ac307c39164b096cf5ba5fbb5a11cf981d9c1ca5573fdf9b6a0f14968074c73e47e1d9c3ba7143a57fa2e00ba616575750dbacf5c00be70ca3fef
data/CHANGELOG.md CHANGED
@@ -5,6 +5,46 @@ 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.213.0] - 2026-09-21
9
+
10
+ ### Engine sync — 1.9.211–1.9.213 (perf + the QT3 date/time family)
11
+
12
+ - 1.9.211: the **interleaved parse lane** landed behind
13
+ `LEPTRIS_INTERLEAVED=1` (engine-side opt-in; sentinel-discipline
14
+ scans, fused 48-byte SIMD probes, names as zero-copy views —
15
+ 1633/1633 parity). The XSLT append-tail memo (single-slot
16
+ validated last-child hint) also rides.
17
+ - 1.9.212/1.9.213: the **QT3 date/time function family** for
18
+ XPath/XSLT (extractors, timezone + `fn:parse-ietf-date`,
19
+ `op:date` arithmetic, the yearMonthDuration family — 17 test
20
+ sets, 782/782 engine-side) plus int64/`long long` portability
21
+ fixes for MSVC/ILP32.
22
+ - No binding-facing surface changes: audit holds at 335/335,
23
+ suite 760/0.
24
+
25
+ ## [Unreleased]
26
+
27
+ ### Added — Descriptor typed scalars + the fused materialize entry (#230's consumer contract)
28
+
29
+ - **Typed scalars**: attribute and child plan rows accept
30
+ `type: :string | :integer | :float | :boolean`. The tag travels
31
+ through the plan ABI (host-defined, echoed verbatim) and
32
+ `PlanValue#to_ruby` returns the cast value — Integer / Float /
33
+ true / false — on element scalars AND collected attributes, so
34
+ per-value cast probes disappear from the consumer's loop
35
+ (lutaml-model's `cast_element_present`/`apply_value_map` class
36
+ of cost). Lenient on unparseable input (raw String wins);
37
+ `#string_value` stays the raw escape hatch regardless of tag.
38
+ - **`Descriptor#materialize(source)`** — the fused loop, pointed
39
+ at the parse: source bytes → typed rows in ONE call (parse,
40
+ plan walk, document free; the standalone PlanValue tree is all
41
+ that surfaces). Byte-parity with parse + `#walk(root)`.
42
+ - Mirrors the yeptris schema-ABI shape (leptris/yeptris#238:
43
+ SCALAR + type_tag STR|INT|FLOAT|BOOL, whole-document results)
44
+ so a framework compiles one descriptor vocabulary and runs it
45
+ on every engine. Native in-pass int/float parsing (no string
46
+ round-trip) is the recorded engine-side headroom.
47
+
8
48
  ## [1.9.210.0] - 2026-09-20
9
49
 
10
50
  ### Performance — engine sync (1.9.210)
data/README.adoc CHANGED
@@ -67,8 +67,11 @@ vendors precompiled `libleptris` + `libutf8proc` binaries for the
67
67
  common engine platforms under `lib/leptris/vendor/`:
68
68
 
69
69
  - `arm64-darwin`, `x86_64-darwin`
70
- - `x86_64-linux`, `aarch64-linux` (glibc; the musl variants are
71
- tried as fallback for Alpine hosts)
70
+ - `x86_64-linux`, `aarch64-linux` (glibc) and `x86_64-linux-musl`,
71
+ `aarch64-linux-musl` (tried as fallback for Alpine hosts)
72
+ - `arm-linux`, `arm-linux-musl` (32-bit ARM EABI5)
73
+ - `ppc64le-linux`, `s390x-linux`, `s390x-linux-musl` (POWER8+ LE
74
+ and IBM Z, qemu-built)
72
75
 
73
76
  `gem install leptris` just works on both engines — the FFI layer
74
77
  selects the matching vendored binary at require time (the binding
@@ -127,7 +130,10 @@ Leptris::XML::FFI.leptris_last_error # => "..."
127
130
  `Leptris::XML.parse_html(html)` parses tolerant HTML4/5 into a
128
131
  standard Document — the same nodes, pool, serializer, and
129
132
  XPath/XSLT/XQuery machinery as XML (libleptris 1.9.75, the last
130
- Nokogiri capability gap):
133
+ Nokogiri capability gap). The `Leptris::HTML`, `Leptris::HTML4`,
134
+ and `Leptris::HTML5` module facades provide the
135
+ Nokogiri-shaped entry points over the same engine
136
+ (`Leptris::HTML5.parse(html)`, `Leptris::HTML(html)`):
131
137
 
132
138
  [source,ruby]
133
139
  ----
@@ -345,18 +351,45 @@ puts doc.to_xml
345
351
  `Element#replace(node)` / `#swap(node)` :: replace in parent.
346
352
  `Element#wrap(node_or_markup)` :: wrap this element in a new one.
347
353
  `Node#unlink` :: detach from the tree.
354
+ `Element#attribute_pairs` :: bulk read-only attribute listing — `[[name, value], ...]` in document order, duplicates included; one C crossing on the native layer (libleptris 1.9.210 era).
355
+
356
+ Document diffing (`Leptris::XML.diff(a, b)`) yields an op list with
357
+ `#ops`, per-kind counts via `#summary`
358
+ (`{update_attr: 1, insert: 1}`, `{}` when identical), and `#to_json`
359
+ for the op list — the `leptris diff --summary/--json` CLI modes'
360
+ library face.
348
361
 
349
362
  === Building from scratch (no parse)
350
363
 
364
+ `Document.create` starts an empty document; build up from there
365
+ (document-level prolog parts included — PIs, comments, the XML
366
+ declaration, and the DOCTYPE are all first-class):
367
+
351
368
  [source,ruby]
352
369
  ----
353
- # Create an empty Document by parsing a sentinel and replacing the root,
354
- # or build incrementally on a one-element seed.
355
- doc = Leptris::XML.parse("<root/>")
356
- doc.root.name = "catalog"
357
- # ... then create_element / add_child as above.
370
+ doc = Leptris::XML::Document.create
371
+ book = doc.create_element("book")
372
+ book[:id] = "b3"
373
+ book << doc.create_element("title")
374
+ doc.root = doc.create_element("catalog")
375
+ doc.root << book
376
+ doc.add_comment("built programmatically") # epilog comment
377
+ doc.set_doctype("catalog", system_id: "catalog.dtd")
378
+
379
+ puts doc.to_xml
358
380
  ----
359
381
 
382
+ [horizontal]
383
+ `Document.create` :: empty document (no root yet).
384
+ `Document#root=` :: attach the root element (bottom-up construction).
385
+ `Document#set_doctype(name, public_id:, system_id:)` :: programmatic DOCTYPE.
386
+ `Document#doctype` (alias `#internal_subset`) :: the `DocType` reader (name/root_name/public_id/system_id/internal_subset).
387
+ `Document#clear_declaration` :: un-set the XML declaration (serialize as if the input had none; idempotent).
388
+ `Document#remove_doctype` :: un-set the DOCTYPE (true when removed; the DocType stays readable until `#free`).
389
+ `Document#add_pi(target, data)` / `#remove_pi(target_or_index)` :: document-level processing instructions.
390
+ `Document#add_comment(str)` :: epilog comment.
391
+ `Document#create_entity_reference(name)` :: `&name;` node, serializes verbatim once attached.
392
+
360
393
  == Namespaces
361
394
 
362
395
  [horizontal]
@@ -711,11 +744,16 @@ Notable differences:
711
744
  past Document disposal will see the error; silence-replace-UAF patterns
712
745
  from Nokogiri do not apply.
713
746
  * No `Nokogiri::CSS` parser. HTML parsing is supported
714
- (`Leptris::XML.parse_html`, libleptris 1.9.75); Nokogiri's
747
+ (`Leptris::HTML` / `Leptris::HTML4` / `Leptris::HTML5` facades
748
+ over `Leptris::XML.parse_html`, libleptris 1.9.75); Nokogiri's
715
749
  HTML-specific node subclasses have no equivalent.
716
- * No RelaxNG / DTD validation, no schema caching (XSLT 1.0–3.0
750
+ * RelaxNG and DTD validation ARE supported (`Leptris::XML::RelaxNG`,
751
+ `Leptris::XML::DTD` — parse once, validate many, structured
752
+ errors). No W3C XML Schema and no schema caching (XSLT 1.0–3.0
717
753
  and an XQuery 1.0 core are supported — see above).
718
- * No built-in JRuby / TruffleRuby support — only CRuby via `ffi`.
754
+ * TruffleRuby and JRuby ARE supported out of the box (#160): the
755
+ `ruby`-platform variant vendors precompiled binaries and the
756
+ binding is FFI-based (no MRI C extension required to load).
719
757
 
720
758
  == Performance
721
759
 
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.210"
11
+ LIBLEPTRIS_VERSION = "1.9.213"
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
@@ -10,3 +10,18 @@ Status: DONE
10
10
  parse 10–12×, serialize 2.9–3.2×, inner_html 4–5×, attr reads
11
11
  6–9×, text 9–11×, element_children walks at parity. Will
12
12
  re-record on the next clean window (load < 15).
13
+
14
+ ## Re-recorded 2026-09-21 — clean window (load ~12, 1.9.210.0, 84k-element/1.3MB catalog, interleaved best-of-5, nokogiri 1.19.4)
15
+
16
+ - [x] Standing harness (MEDIUM fixture): parse medium **11.1x** /
17
+ small 1.88x; xpath 2.4-20x (predicate 15-20x, complex
18
+ 11-14x); traverse 1.5-1.7x; serialize 2.25-3.13x
19
+ - [x] Warm `children` walks **2.06-2.08x** (was 1.86x at 1.9.152)
20
+ - [x] Cold parse+first-touch walks **0.58-0.74x (avg ~0.66x)** —
21
+ the loss nearly HALVED from 0.34x; the engine's
22
+ document-block/arena work (1.9.210) shows here
23
+ - [x] Attr cold row (fresh wrappers) 1.18x
24
+ - [ ] **Open front: cold TEXT row 0.66x** — fresh text-node
25
+ materialization (`element_children.each(&:content)` cold)
26
+ loses where warm text aggregation was 6.18x. Next perf
27
+ round's target.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.210.0"
4
+ VERSION = "1.9.213.0"
5
5
  end
@@ -28,7 +28,33 @@ require "ffi"
28
28
  # Namespace binding per plan: +ns:+ is :none (default), :any, or
29
29
  # { exact: "urn:..." }; +flags: [:mixed_content, :ordered, :cdata,
30
30
  # :ns_lenient].
31
+ #
32
+ # == Typed scalars (#230's fused-consumer contract, mirroring the
33
+ # yeptris schema ABI)
34
+ #
35
+ # Attribute and child rows accept +type: :string (default),
36
+ # :integer, :float, or :boolean+. The tag travels through the plan
37
+ # ABI (host-defined, echoed verbatim) and PlanValue#to_ruby returns
38
+ # the cast value — Integer / Float / true / false — so per-value
39
+ # cast probes disappear from the consumer. Unparseable values fall
40
+ # back to the raw String (lenient, first-wins house style);
41
+ # #string_value always returns the raw String regardless of tag.
42
+ #
43
+ # descriptor = Leptris::XML::Descriptor.build(
44
+ # name: "catalog",
45
+ # children: [
46
+ # { name: "price", kind: :scalar, type: :float },
47
+ # { name: "in_stock", kind: :scalar, type: :boolean },
48
+ # ])
49
+ # descriptor.materialize(xml_source).to_ruby # typed, one call
31
50
  class Leptris::XML::Descriptor
51
+ TYPE_TAGS = {
52
+ string: 0,
53
+ integer: 1,
54
+ float: 2,
55
+ boolean: 3,
56
+ }.freeze
57
+ private_constant :TYPE_TAGS
32
58
  class Handle < ::FFI::AutoPointer
33
59
  def self.release(ptr)
34
60
  Leptris::XML::FFI.leptris_plan_free(ptr)
@@ -136,7 +162,7 @@ class Leptris::XML::Descriptor
136
162
  ap = Leptris::XML::FFI::AttrPlan.new(attr_memory[j])
137
163
  ap[:wire_name] = anchor_string(anchors, row.fetch(:name))
138
164
  ap[:kind] = kind_code(row[:kind] || :scalar)
139
- ap[:type_tag] = row[:type_tag] || 0
165
+ ap[:type_tag] = type_tag_code(row)
140
166
  end
141
167
  ep[:attribute_count] = attrs.size
142
168
  ep[:attribute_plans] = attr_memory
@@ -148,7 +174,7 @@ class Leptris::XML::Descriptor
148
174
  cp = Leptris::XML::FFI::ChildPlan.new(child_memory[j])
149
175
  cp[:wire_name] = anchor_string(anchors, row.fetch(:name))
150
176
  cp[:kind] = kind_code(row[:kind] || :scalar)
151
- cp[:type_tag] = row[:type_tag] || 0
177
+ cp[:type_tag] = type_tag_code(row)
152
178
  cp[:child_plan_index] = row[:child_plan_index] || -1
153
179
  # Rule-level ns form (libleptris 1.9.178, #1115): siblings
154
180
  # under one parent can require different URIs when set;
@@ -189,6 +215,20 @@ class Leptris::XML::Descriptor
189
215
  end
190
216
  private_class_method :kind_code
191
217
 
218
+ # +type: :string/:integer/:float/:boolean+ (or a raw numeric
219
+ # type_tag passthrough for hosts with their own vocabulary).
220
+ # Public: PlanValue's attribute casts share the vocabulary.
221
+ def self.type_tag_code(row)
222
+ if row.key?(:type)
223
+ TYPE_TAGS.fetch(row[:type]) do
224
+ raise ArgumentError,
225
+ "type must be one of #{TYPE_TAGS.keys.inspect}, got #{row[:type].inspect}"
226
+ end
227
+ else
228
+ row[:type_tag] || 0
229
+ end
230
+ end
231
+
192
232
  def initialize(handle, plans, root_index)
193
233
  @handle = handle
194
234
  @plans = plans
@@ -211,4 +251,21 @@ class Leptris::XML::Descriptor
211
251
  Leptris::XML::PlanValue.new(ResultHandle.new(raw),
212
252
  owner: true, plans: @plans, plan: @plans[0])
213
253
  end
254
+
255
+ # The fused loop (#230, pointed at the parse): source bytes →
256
+ # typed rows in ONE call — parse, walk the compiled plan, free
257
+ # the document; the returned PlanValue tree is standalone and
258
+ # the Document never surfaces. Byte-for-byte parity with
259
+ # #walk on the same source's root element.
260
+ def materialize(source)
261
+ document = Leptris::XML::Document.parse(source)
262
+ begin
263
+ root = document.root
264
+ raise Leptris::XML::Error,
265
+ "materialize: document has no root element" if root.nil?
266
+ walk(root)
267
+ ensure
268
+ document.free
269
+ end
270
+ end
214
271
  end
@@ -94,12 +94,40 @@ class Leptris::XML::PlanValue
94
94
  when :callback
95
95
  { value: string_value, position: position, type_tag: type_tag }
96
96
  else
97
- string_value
97
+ typed_string_value
98
98
  end
99
99
  end
100
100
 
101
101
  private
102
102
 
103
+ # SCALAR honoring the plan row's type tag (#230's typed-scalars
104
+ # contract): 1=Integer, 2=Float, 3=boolean. Lenient on
105
+ # unparseable input — the raw String wins (first-wins house
106
+ # style; strict validation belongs to the consumer's callback
107
+ # rows). #string_value stays the raw escape.
108
+ def typed_string_value
109
+ case type_tag
110
+ when 1
111
+ s = string_value
112
+ Integer(s, 10) rescue s
113
+ when 2
114
+ s = string_value
115
+ begin
116
+ Float(s)
117
+ rescue ArgumentError, TypeError
118
+ s
119
+ end
120
+ when 3
121
+ case string_value
122
+ when "true", "1" then true
123
+ when "false", "0" then false
124
+ else string_value
125
+ end
126
+ else
127
+ string_value
128
+ end
129
+ end
130
+
103
131
  # The element plan that a child row with this wire_name recurses
104
132
  # into (nil for non-nested rows — their values carry no plan).
105
133
  def child_row_plan(child_wire_name)
@@ -114,8 +142,31 @@ class Leptris::XML::PlanValue
114
142
  result = {}
115
143
  ((@plan && @plan[:attributes]) || []).each do |row|
116
144
  v = attribute(row[:name])
117
- result[row[:name]] = v unless v.nil?
145
+ result[row[:name]] = cast_attribute(v, row) unless v.nil?
118
146
  end
119
147
  result
120
148
  end
149
+
150
+ # Attribute values come back as plain Strings from the walk;
151
+ # apply the row's type tag with the same lenient semantics as
152
+ # typed_string_value.
153
+ def cast_attribute(value, row)
154
+ tag = Leptris::XML::Descriptor.type_tag_code(row)
155
+ case tag
156
+ when 1 then Integer(value, 10) rescue value
157
+ when 2
158
+ begin
159
+ Float(value)
160
+ rescue ArgumentError, TypeError
161
+ value
162
+ end
163
+ when 3
164
+ case value
165
+ when "true", "1" then true
166
+ when "false", "0" then false
167
+ else value
168
+ end
169
+ else value
170
+ end
171
+ end
121
172
  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.210
4
+ - libleptris v1.9.213
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,62 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.213] - 2026-09-21
4
+
5
+ ### Added — the op:date and yearMonth families (#1182, lever 6)
6
+
7
+ - **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.
8
+ - **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.
9
+
10
+ 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.
11
+
12
+ ## [1.9.212] - 2026-09-21
13
+
14
+ ### Added — the QT3 date/time family (lever 6 stage-2, #1182)
15
+
16
+ Three corpus-driven batches land the date/time surface with the
17
+ W3C QT3 suite as the gate:
18
+
19
+ - **Extractors (#1261)**: 12 vendored test-sets (300/300) —
20
+ year/month/day-from-date{,Time}, hours/minutes/seconds-from-
21
+ {time,dateTime} — plus five language fixes: function-call
22
+ operands for eq/ne/lt/le/gt/ge (unprefixed and fn:-prefixed),
23
+ the `idiv` operator, unary `+`, the timezone scan in date
24
+ parsing (zoned values were treated as naive), and the
25
+ empty-sequence/fractional-seconds extractor contracts.
26
+ - **Timezone + RFC 5322 (#1262)**: 5 sets (90/90) —
27
+ timezone-from-* accessors and fn:parse-ietf-date (both lexical
28
+ orders, obsolete US zones, verbatim fractions). The QT3 harness
29
+ grew inline param environments and external-declaration
30
+ prepending.
31
+ - **op:dayTimeDuration value model (#1263)**: 8 sets (287/287) —
32
+ duration arithmetic (+, -, *, div), comparisons, min/max over
33
+ duration sequences, canonical formatting, FODT0002 range errors
34
+ (including a fixed fortified-runtime abort on giant values), and
35
+ adjust-* attaching the implicit timezone to naive values.
36
+
37
+ ## [1.9.211] - 2026-09-20
38
+
39
+ ### Performance
40
+
41
+ - **Interleaved parse lane, slices 1-3a (#1258).** The first
42
+ pugixml-style interleaved rewrite landed behind
43
+ `LEPTRIS_INTERLEAVED=1` (opt-in; every input outside the covered
44
+ surface bails to the classic parser before any state). The lane
45
+ streams `IlRec`/`IlAttr` records with buffer-relative views, then
46
+ replays the classic public stores in one linear post-pass:
47
+ sentinel-discipline scans (no per-byte bounds tests; fused 48-byte
48
+ quote/'&'/whitespace probes with SIMD tails), upfront stream
49
+ capacity from the input size, and element/attribute names as
50
+ NUL'd views into the parse scratch (no per-name copies). Parity is
51
+ gated by a dedicated spec plus running the FULL test suite with
52
+ the lane forced on: 1633/1633 identical. `LEPTRIS_IL_STATS=1`
53
+ prints an engagement canary for diagnosing whether the lane ran.
54
+ - **XSLT append-tail memo (#1259).** `out_append_child` keeps a
55
+ validated single-slot (parent, last-child) hint and feeds
56
+ `leptris_element_append_child_tail` — result-tree placement no
57
+ longer re-walks sibling chains (the hottest dispatch-heavy
58
+ profile leaf, ~17% of samples).
59
+
3
60
  ## [1.9.210] - 2026-09-20
4
61
 
5
62
  ### 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.213
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
+ }