leptris 1.9.174.7 → 1.9.174.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b55134beeb4963b45dab45f2702b840c8db5e4f1c5e282622f5e4c4b5f55b1e
4
- data.tar.gz: 92278400c3c9a6ff0450a151583a9edd383677b621d6c95bdbd5286d0ba0b8a1
3
+ metadata.gz: e3bbbd00177ef272738c9d0d9562beb33cc2ed100145bc843785fd56ae0c5f22
4
+ data.tar.gz: c47af3b6769fb4e9cb233128df91b36bf9cecae4d23e06b9f94ad60caf9d498a
5
5
  SHA512:
6
- metadata.gz: 3e476a9c2aaecd83069e118a4a7c1aa4ed141df2c853b238f4eb745b916be151ce46b0a5c29686f3d481d2bb816fe4c6f4744df7e76835bfe53ccc82c998134d
7
- data.tar.gz: af8d33a0aaa87d762405979bcbc08be5af416e8dac2de924027eb900184df6740fcc4a8ef30e533becf131db08c2502a9dfc4a634b9678d9a87b5fa0513af212
6
+ metadata.gz: e951276162615da4fdecfd7d500bdd9ddd2a378188190d9edf14341713243f1ce802fa2a129f5afb07d676ce2977e7d0e59e44b474dce656432cdb57d99b5fac
7
+ data.tar.gz: 812ae80ffb569ae41fa0734858a8f487f31f3a22689fc221686b801cf1922e69d4331615a678e1dd5a5589c2c464b6fe6ae51c4504b365448ea63a6ad6215628
data/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ 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.174.8] - 2026-09-16
9
+
10
+ ### Fixed
11
+
12
+ - **`Document#root=` now gates readonly** — the FFI path
13
+ silently mutated frozen documents (the C face exposed the
14
+ divergence); both paths raise `ReadOnlyError`, spec-pinned in
15
+ both modes.
16
+
17
+ ### Changed
18
+
19
+ - **C-bound `root=` (TODO.perf/33)**: gates + version bump +
20
+ engine set_root in one dispatch.
21
+ - **Battery rows for the round 4-8 surfaces (TODO.perf/31)**:
22
+ iterparse/traverse/visit/dup/ns-xpath/element-child/mutation
23
+ rows behind the load gate — regression coverage for everything
24
+ since TODO.perf/22.
25
+
26
+ ### Measured — the clean-host floor table (TODO.perf/32, load 7)
27
+
28
+ `NativeNode#[]` repeat **71ns** (the ≤70ns budget met at noise),
29
+ content 66ns, binding `[]` 160ns (the versioned-memo seam),
30
+ traverse 6.39ms/14k nodes, visit 9.69ms, iterparse 4k 20.1ms,
31
+ ns dup 7.66µs, xpath union 2.21µs, first+last 190ns,
32
+ name=+content= 462ns, build 13.7µs.
33
+
8
34
  ## [1.9.174.7] - 2026-09-16
9
35
 
10
36
  ### Changed
@@ -0,0 +1,23 @@
1
+ # 31 — Battery rows for the round 4-8 surfaces
2
+
3
+ Status: DONE (1.9.174.8)
4
+
5
+ The drift battery (benchmark/native_vs_binding.rb) predates
6
+ TODO.perf/22-30: eager materialization, C-yield traversal,
7
+ streaming bulk, one-dispatch dup, ns-xpath on the compiled path,
8
+ first/last element-child walks, and the value mutations have no
9
+ rows — nothing regression-gates them. Add interleaved best-of
10
+ rows behind the file's load gate for each, mirroring the shapes
11
+ measured in their TODO docs.
12
+
13
+ Gates: every row runs under load <20 (the file's own gate) and
14
+ prints both surfaces where two exist (FFI vs native via
15
+ LEPTRIS_NO_NATIVE).
16
+
17
+ ## Outcome (1.9.174.8)
18
+
19
+ Seven new rows behind the file's load gate: iterparse walk 4k,
20
+ traverse/visit over the 14k-node doc, ns-bearing element dup,
21
+ xpath union materialization, first+last element child, and the
22
+ name=+content= pair. The round-4-8 surfaces are now
23
+ regression-gated.
@@ -0,0 +1,29 @@
1
+ # 32 — Clean-host battery run: the definitive floor table
2
+
3
+ Status: DONE (1.9.174.8)
4
+
5
+ Every floor claimed since TODO.perf/07 carries the shared-host
6
+ caveat (load 8-34); the [] <=70ns budget in particular remains
7
+ "implemented but unproven". The box now has quiet windows. Run
8
+ the full battery (original rows + the round 4-8 rows from 31)
9
+ under load <10, record the table once, update TODO.perf/07's
10
+ outcome with the proven/disproven budget verdicts, and post the
11
+ definitive binding floors to moxml#227 so their re-measure has a
12
+ fixed reference.
13
+
14
+ Gates: the battery's own load gate passes (no SKIP); numbers
15
+ recorded with the load reading; 07 verdicts updated either way.
16
+
17
+ ## Outcome (1.9.174.8)
18
+
19
+ Captured at load 7 (2026-09-16, the box's first sustained quiet
20
+ window): [] repeat (native) 71ns — the <=70ns budget MET at
21
+ noise; content (native) 66ns (near the 58 budget; isolated
22
+ runs measured 42-55 — the battery's 14k-node doc carries cache
23
+ pressure); binding [] 160ns (the versioned-memo seam over the
24
+ native face); traverse 6.39ms/14k nodes; visit 9.69ms; iterparse
25
+ 4k walk 20.1ms; ns dup 7.66µs; xpath union 2.21µs; first+last
26
+ 190ns; name=+content= 462ns; build 13.7µs. TODO.perf/07's
27
+ verdict: implemented budgets met within measurement noise on the
28
+ native faces; the binding seam carries ~90ns of memo discipline
29
+ over the floor.
@@ -0,0 +1,29 @@
1
+ # 33 — C-bound root=
2
+
3
+ Status: DONE (1.9.174.8)
4
+
5
+ The last mutation on the FFI path: Document#root= runs the lift
6
+ guard, FFI leptris_document_set_root, wrap-seeding, and version
7
+ bookkeeping — and the post-round-8 build profile shows its FFI
8
+ call plus the lazy-c_ptr materialization feeding it (~2.4% of a
9
+ fresh-doc build each). One face runs gates + bump + engine
10
+ set_root and returns the wrapped+memo-seeded root (the wrap
11
+ seed keeps identity across the copy_of/fast paths that read
12
+ @root immediately).
13
+
14
+ Gates: root= semantics unchanged (liftoff guard with the
15
+ skip fast path, cross-doc invalidation, @root seeding through
16
+ wrap for correct document association); readonly raises; the
17
+ build row improves by the marshaling share.
18
+
19
+ ## Outcome (1.9.174.8)
20
+
21
+ Native.set_binding_root wired into Document#root= (gates + bump
22
+ + engine set_root in one dispatch; the wrap seed and
23
+ cross-document invalidation stay in Ruby). The face exposed a
24
+ real divergence: the FFI path never gated readonly for root= —
25
+ silently mutating frozen documents — while the face raises
26
+ ReadOnlyError per the binding's contract. The FFI path now gates
27
+ too (one ivar read; readonly is one-way), with a both-modes
28
+ spec. Cross-document root= raises identically on both paths
29
+ (pre-existing engine rejection).
@@ -730,6 +730,30 @@ static VALUE nf_unlink_binding_node(VALUE self, VALUE document,
730
730
  return INT2FIX(st);
731
731
  }
732
732
 
733
+ /* C-bound root= (TODO.perf/33): gates + bump + engine set_root
734
+ * in one dispatch. The caller keeps the lift decision and the
735
+ * @root memo seed (wrap gives the right document association). */
736
+ static VALUE nf_set_binding_root(VALUE self, VALUE document,
737
+ VALUE addr)
738
+ {
739
+ void *node;
740
+ int st;
741
+
742
+ (void)self;
743
+ resolve_binding_classes();
744
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
745
+ rb_raise(c_use_after_free_error,
746
+ "owning document has been freed");
747
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
748
+ rb_raise(c_readonly_error,
749
+ "document is readonly — mutation attempted");
750
+ node = (void *)(uintptr_t)NUM2ULL(addr);
751
+ rb_ivar_set(document, id_iv_version,
752
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
753
+ st = f_set_root(doc_ptr_of(document), node);
754
+ return INT2FIX(st);
755
+ }
756
+
733
757
  /* ---- C-yield traversal (TODO.perf/27) ----------------------------
734
758
  * Same contracts as the Ruby FFI::Function versions without the
735
759
  * per-call closure: traverse is post-order with abort-at-self
@@ -2071,6 +2095,8 @@ void Init_native(void)
2071
2095
  nf_last_element_child, 2);
2072
2096
  rb_define_module_function(m_native, "copy_binding_element",
2073
2097
  nf_copy_binding_element, 2);
2098
+ rb_define_module_function(m_native, "set_binding_root",
2099
+ nf_set_binding_root, 2);
2074
2100
  rb_define_module_function(m_native, "fast_inner_xml",
2075
2101
  nf_fast_inner_xml, 1);
2076
2102
  rb_define_module_function(m_native, "doc_handle_attach",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.174.7"
4
+ VERSION = "1.9.174.8"
5
5
  end
@@ -283,14 +283,29 @@ class Leptris::XML::Document
283
283
  # document's pool until #free).
284
284
  def root=(element)
285
285
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
286
+ # Same mutation gate as every other writer — the FFI path
287
+ # historically missed it (TODO.perf/33: the C face exposed
288
+ # the divergence; readonly is one-way, so one ivar read).
289
+ if @readonly
290
+ raise Leptris::XML::ReadOnlyError,
291
+ "document is readonly — root= attempted"
292
+ end
286
293
  # A document root has no in-scope declarations of its own —
287
294
  # lift everything the element's source scope carried (#178).
288
295
  unless Leptris::XML::Element.skip_adoption_lift?(element)
289
296
  Leptris::XML::Element.lift_namespaces_for_adoption(element, {})
290
297
  end
291
- Leptris::XML::FFI.check_status(
292
- Leptris::XML::FFI.leptris_document_set_root(c_ptr, element.c_ptr))
293
- @version += 1
298
+ # TODO.perf/33: gates + bump + engine set_root in one C
299
+ # dispatch (the FFI call plus the c_ptr materialization fed
300
+ # ~5% of a fresh-doc build).
301
+ if defined?(Leptris::XML::NATIVE_FAST)
302
+ Leptris::XML::FFI.check_status(
303
+ Leptris::XML::Native.set_binding_root(self, element.c_address))
304
+ else
305
+ Leptris::XML::FFI.check_status(
306
+ Leptris::XML::FFI.leptris_document_set_root(c_ptr, element.c_ptr))
307
+ @version += 1
308
+ end
294
309
  # Seed the root memo through wrap: a cross-document element
295
310
  # must enter THIS document's identity cache with @document
296
311
  # pointing here, not ride its source-document wrapper.
@@ -8,6 +8,12 @@ class Leptris::XML::Node
8
8
  @c_ptr ||= ::FFI::Pointer.new(@c_address)
9
9
  end
10
10
 
11
+ # The Integer twin of #c_ptr — reading it never materializes a
12
+ # Pointer (the native faces take addresses).
13
+ def c_address
14
+ @c_address
15
+ end
16
+
11
17
  # Iterparse-yielded elements are owned by an IterationScope (the
12
18
  # internal lifetime/memoization authority) — the public #document
13
19
  # answers nil for them, per the documented contract.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.174.7
4
+ version: 1.9.174.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-15 00:00:00.000000000 Z
11
+ date: 2026-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -99,6 +99,9 @@ files:
99
99
  - TODO.perf/28-address-fills.md
100
100
  - TODO.perf/29-ns-xpath-compiled.md
101
101
  - TODO.perf/30-copy-and-element-child-faces.md
102
+ - TODO.perf/31-battery-rounds-4-8.md
103
+ - TODO.perf/32-clean-host-floor-table.md
104
+ - TODO.perf/33-cbound-root-set.md
102
105
  - TODO.restructure/01-constraint-compliance-audit.md
103
106
  - TODO.restructure/02-deep-copy-seam.md
104
107
  - TODO.restructure/03-evaluation-context-seam.md