leptris 1.9.210.0-aarch64-linux → 1.9.213.0-aarch64-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 +4 -4
- data/CHANGELOG.md +40 -0
- data/README.adoc +49 -11
- data/Rakefile +1 -1
- data/TODO.restructure/17-perf-drift-record.md +15 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/descriptor.rb +59 -2
- data/lib/leptris/xml/plan_value.rb +53 -2
- data/lib/libleptris.so +0 -0
- data/vendor-src/README.md +1 -1
- data/vendor-src/libleptris/CHANGELOG.md +57 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/TODO.max-perf/00-INDEX.md +4 -0
- data/vendor-src/libleptris/TODO.max-perf/interleaved-parse.md +67 -0
- data/vendor-src/libleptris/benchmarks/CMakeLists.txt +13 -0
- data/vendor-src/libleptris/benchmarks/parse/bench_parse_ab.cpp +83 -0
- data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +658 -0
- data/vendor-src/libleptris/src/leptris/leptris_internal.h +1 -0
- data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +368 -1
- data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +466 -24
- data/vendor-src/libleptris/src/leptris/xpath/parser.c +69 -7
- data/vendor-src/libleptris/src/leptris/xpath/vm.c +3 -1
- data/vendor-src/libleptris/src/leptris/xpath/xpath_internal.h +8 -1
- data/vendor-src/libleptris/src/leptris/xslt/xslt_exec.c +5 -2
- data/vendor-src/libleptris/src/leptris/xslt/xslt_internal.h +9 -0
- data/vendor-src/libleptris/test/CMakeLists.txt +1 -0
- data/vendor-src/libleptris/test/parser/test_interleaved.cpp +223 -0
- data/vendor-src/libleptris/test/xpath/test_xpath.cpp +4 -1
- data/vendor-src/libleptris/test/xquery/qt3/README.md +12 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/day-from-date.xml +249 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/day-from-dateTime.xml +249 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/hours-from-dateTime.xml +249 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/hours-from-time.xml +249 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/implicit-timezone.xml +249 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/minutes-from-dateTime.xml +250 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/minutes-from-time.xml +249 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/month-from-date.xml +249 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/month-from-dateTime.xml +249 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/parse-ietf-date.xml +1268 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/seconds-from-dateTime.xml +250 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/seconds-from-time.xml +249 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/timezone-from-date.xml +333 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/timezone-from-dateTime.xml +250 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/timezone-from-time.xml +249 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/year-from-date.xml +253 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/year-from-dateTime.xml +249 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/add-dayTimeDuration-to-date.xml +467 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/add-dayTimeDuration-to-dateTime.xml +436 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/add-dayTimeDuration-to-time.xml +432 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/add-dayTimeDurations.xml +850 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/add-yearMonthDuration-to-date.xml +441 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/add-yearMonthDuration-to-dateTime.xml +305 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/add-yearMonthDurations.xml +440 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/dayTimeDuration-greater-than.xml +859 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/dayTimeDuration-less-than.xml +685 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/divide-dayTimeDuration-by-dayTimeDuration.xml +410 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/divide-dayTimeDuration.xml +807 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/divide-yearMonthDuration-by-yearMonthDuration.xml +377 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/divide-yearMonthDuration.xml +499 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/duration-equal.xml +1477 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/multiply-dayTimeDuration.xml +697 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/multiply-yearMonthDuration.xml +685 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/subtract-dayTimeDuration-from-date.xml +377 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/subtract-dayTimeDuration-from-dateTime.xml +376 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/subtract-dayTimeDuration-from-time.xml +441 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/subtract-dayTimeDurations.xml +971 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/subtract-yearMonthDuration-from-date.xml +426 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/subtract-yearMonthDuration-from-dateTime.xml +377 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/subtract-yearMonthDurations.xml +531 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/yearMonthDuration-greater-than.xml +542 -0
- data/vendor-src/libleptris/test/xquery/qt3/op/yearMonthDuration-less-than.xml +542 -0
- data/vendor-src/libleptris/test/xquery/test_qt3.cpp +128 -3
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +47 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b840899442b8d26da4e97f213ea68ddbe4158ec52d9ddcbd423172231b8eda25
|
|
4
|
+
data.tar.gz: 0c2a4b6fe6b2d9b8fb2fe0114d4a9d1ab6c11d25d34a463a929f48bda71fb330
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4252440c757a98e2dae8483f959e8df94c047cffc8d43699e741ad3206e49ecacdb0ac2cb8b18d329e8faac43811a557dca864664f92a7c415743e9e68032c8f
|
|
7
|
+
data.tar.gz: 18f7d22d36e6a16ec1ff654d625c4a33efb7161a8d7ccac5c701183091c4ac38d3c2139a93b923f8cd8148f899ef00bf0f63403005103d6eb695141b2d8f4125
|
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
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
doc.
|
|
357
|
-
|
|
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::
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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.
|
data/lib/leptris/version.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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/lib/libleptris.so
CHANGED
|
Binary file
|
data/vendor-src/README.md
CHANGED
|
@@ -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
|
|
@@ -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
|
+
}
|