leptris 1.9.174.7 → 1.9.174.9

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: 73130c8bf549007d80bf610c22bc831df382f5fe972316835a6dc806885929c5
4
+ data.tar.gz: 3b6bfb23c72a2ed3532eb64886c9f36603975b5d601aa693f6b3ba821bb19de2
5
5
  SHA512:
6
- metadata.gz: 3e476a9c2aaecd83069e118a4a7c1aa4ed141df2c853b238f4eb745b916be151ce46b0a5c29686f3d481d2bb816fe4c6f4744df7e76835bfe53ccc82c998134d
7
- data.tar.gz: af8d33a0aaa87d762405979bcbc08be5af416e8dac2de924027eb900184df6740fcc4a8ef30e533becf131db08c2502a9dfc4a634b9678d9a87b5fa0513af212
6
+ metadata.gz: 621d0f62f7869c1a4b0c63212dce8694f51cc1914a3f115551b7e91c1f5b2bc0af9a33dcb2baa4d462fc5a12f72225a228dee59f7e4a7f70ed5173e0afe6932b
7
+ data.tar.gz: 2a389319493ace9979ea863853b75c56149f73dcc5e204c362bd8bdbaacd335d94a58bd9bb3d4ed83f6c83df3331b53549bb462fa19980fb996a8c2798c3fbdc
data/CHANGELOG.md CHANGED
@@ -5,6 +5,49 @@ 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.9] - 2026-09-16
9
+
10
+ ### Changed
11
+
12
+ - **Fragment fast lane (TODO.perf/34)**: `add_child(String)` runs
13
+ one C dispatch for the whole markup add — fragment parse +
14
+ every child append + a single readonly gate and version bump
15
+ (the move-during-iteration hazard handled: appending detaches,
16
+ so the walk captures next before each move). The fragment parse
17
+ face returns an address (no status MemoryPointer, no Pointer);
18
+ `DocumentFragment#children` rides the bulk face; parse failures
19
+ fall back to the legacy path for the exact error.
20
+ - **Document.parse default path as one C dispatch (TODO.perf/35)**:
21
+ engine parse + ivar-seeded wrapper + lifetime handle in one
22
+ call — small parses measured 15.6µs → 6.8µs (~2.3x, under host
23
+ load ~100; the shape is self-contained).
24
+
25
+ ## [1.9.174.8] - 2026-09-16
26
+
27
+ ### Fixed
28
+
29
+ - **`Document#root=` now gates readonly** — the FFI path
30
+ silently mutated frozen documents (the C face exposed the
31
+ divergence); both paths raise `ReadOnlyError`, spec-pinned in
32
+ both modes.
33
+
34
+ ### Changed
35
+
36
+ - **C-bound `root=` (TODO.perf/33)**: gates + version bump +
37
+ engine set_root in one dispatch.
38
+ - **Battery rows for the round 4-8 surfaces (TODO.perf/31)**:
39
+ iterparse/traverse/visit/dup/ns-xpath/element-child/mutation
40
+ rows behind the load gate — regression coverage for everything
41
+ since TODO.perf/22.
42
+
43
+ ### Measured — the clean-host floor table (TODO.perf/32, load 7)
44
+
45
+ `NativeNode#[]` repeat **71ns** (the ≤70ns budget met at noise),
46
+ content 66ns, binding `[]` 160ns (the versioned-memo seam),
47
+ traverse 6.39ms/14k nodes, visit 9.69ms, iterparse 4k 20.1ms,
48
+ ns dup 7.66µs, xpath union 2.21µs, first+last 190ns,
49
+ name=+content= 462ns, build 13.7µs.
50
+
8
51
  ## [1.9.174.7] - 2026-09-16
9
52
 
10
53
  ### 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).
@@ -0,0 +1,41 @@
1
+ # 34 — Fragment fast lane: C parse, bulk children, one-shot markup append
2
+
3
+ Status: DONE (1.9.174.9)
4
+
5
+ The add_child(String) path — moxml's markup-builder shape — pays
6
+ three layers per call: parse_fragment_with_status allocates a
7
+ MemoryPointer for the status out-param, DocumentFragment#children
8
+ runs the FFI fetch+wrap loop (never moved to the bulk faces), and
9
+ each child appends through its own dispatch.
10
+
11
+ Bind the lane: a C parse face returns the fragment's address
12
+ (Integer — no Pointer, no MemoryPointer); the fragment stores
13
+ @c_address with a lazy c_ptr; children rides the bulk face
14
+ (fragment nodes live in the document pool — the document's cache
15
+ and version discipline apply); and a one-shot append face parses
16
+ the markup, walks the fragment's children, and appends every one
17
+ with a single readonly gate + version bump. Parse failures return
18
+ a marker; the Ruby fallback re-runs the old path for the exact
19
+ error message (the rare path, cheap to re-fail).
20
+
21
+ Gates: add_child(String) semantics identical (multiple top-level
22
+ nodes, text, PIs, namespaces lifted per the Node path); fragment
23
+ children identical through the bulk face; suite both modes.
24
+
25
+ ## Outcome (1.9.174.9)
26
+
27
+ Three layers landed: the fragment parse face answers the
28
+ fragment ADDRESS (no status MemoryPointer, no Pointer mint —
29
+ failures fall back to the FFI pair for the exact error);
30
+ DocumentFragment stores @c_address with a lazy c_ptr and
31
+ children() rides the bulk face (fragment nodes live in the
32
+ document pool — the document's cache and version discipline
33
+ apply); and Element#add_child(String) runs ONE dispatch for the
34
+ whole markup add (parse + per-child append + single gate/bump —
35
+ with the move-during-iteration hazard caught by the suite:
36
+ appending detaches the child, so the walk captures next BEFORE
37
+ each move). -1 re-runs the legacy path for parse errors;
38
+ -1000-st routes through check_status. The markup row needs a
39
+ quiet-host battery number (this box sat at load 100+ today);
40
+ small-parse measured 6.8µs native vs 15.6µs FFI (~2.3x) under
41
+ load 100.
@@ -0,0 +1,24 @@
1
+ # 35 — Document.parse default path as one C dispatch
2
+
3
+ Status: DONE (1.9.174.9)
4
+
5
+ The default parse path (no options, no recover — the adapter
6
+ default) is already allocation-free at the Ruby level (#187),
7
+ but still pays FFI marshaling, the wrap frames, the Freed and
8
+ Pointer constructions, and the handle attach across three Ruby
9
+ layers. One C face runs leptris_parse_string + the full document
10
+ wrapper (ivar-seeded) + lifetime handle and returns it; failure
11
+ raises the same ParseError shape via last_error read in C.
12
+
13
+ Gates: parse defaults identical (errors, readonly flag, root
14
+ access); the options/recover paths untouched; suite both modes.
15
+
16
+ ## Outcome (1.9.174.9)
17
+
18
+ Native.parse_binding_document: leptris_parse_string + the full
19
+ ivar-seeded wrapper + lifetime handle in one dispatch (shared
20
+ build_binding_document helper); Document.parse's default path
21
+ (no options, no recover) routes through it, raising the same
22
+ ParseError shape via last_error on failure. Options/recover
23
+ paths untouched. Measured (load ~100, self-contained shape):
24
+ small parse 15.6µs -> 6.8µs (~2.3x).
@@ -94,6 +94,9 @@ typedef void (*visit_fn)(void *, visit_cb_fn, void *);
94
94
  typedef void (*free_str_fn)(void *);
95
95
  typedef const char *(*node_xpath_fn)(void *);
96
96
  typedef void *(*elem_copy_fn)(void *, void *);
97
+ typedef void *(*parse_str_fn)(const char *, size_t, void *);
98
+ typedef void *(*parse_frag_fn)(const char *, size_t, void *, int *);
99
+ typedef const char *(*last_err_fn)(void);
97
100
  static set_str_fn f_elem_set_name, f_elem_set_text, f_text_set_content;
98
101
  static node_unlink_fn f_node_unlink;
99
102
  static traverse_fn f_node_traverse;
@@ -101,6 +104,9 @@ static visit_fn f_node_visit;
101
104
  static free_str_fn f_free_str;
102
105
  static node_xpath_fn f_node_xpath;
103
106
  static elem_copy_fn f_elem_copy;
107
+ static parse_str_fn f_parse_str;
108
+ static parse_frag_fn f_parse_frag;
109
+ static last_err_fn f_last_err;
104
110
  static set_root_fn f_set_root;
105
111
  static doc_free_fn f_doc_free;
106
112
 
@@ -221,6 +227,9 @@ static void resolve_symbols(const char *lib_path)
221
227
  f_free_str = (free_str_fn)lib_sym(h, "leptris_free_string");
222
228
  f_node_xpath = (node_xpath_fn)lib_sym(h, "leptris_node_get_xpath");
223
229
  f_elem_copy = (elem_copy_fn)lib_sym(h, "leptris_element_copy");
230
+ f_parse_str = (parse_str_fn)lib_sym(h, "leptris_parse_string");
231
+ f_parse_frag = (parse_frag_fn)lib_sym(h, "leptris_parse_fragment");
232
+ f_last_err = (last_err_fn)lib_sym(h, "leptris_last_error");
224
233
  f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
225
234
  f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
226
235
  if (!f_elem_name || !f_text_content || !f_attr ||
@@ -233,7 +242,8 @@ static void resolve_symbols(const char *lib_path)
233
242
  !f_pi_data || !f_elem_set_name || !f_elem_set_text ||
234
243
  !f_text_set_content || !f_node_unlink ||
235
244
  !f_node_traverse || !f_node_visit || !f_free_str ||
236
- !f_node_xpath || !f_elem_copy ||
245
+ !f_node_xpath || !f_elem_copy || !f_parse_str ||
246
+ !f_parse_frag || !f_last_err ||
237
247
  !f_doc_free ||
238
248
  !f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
239
249
  !f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
@@ -730,6 +740,30 @@ static VALUE nf_unlink_binding_node(VALUE self, VALUE document,
730
740
  return INT2FIX(st);
731
741
  }
732
742
 
743
+ /* C-bound root= (TODO.perf/33): gates + bump + engine set_root
744
+ * in one dispatch. The caller keeps the lift decision and the
745
+ * @root memo seed (wrap gives the right document association). */
746
+ static VALUE nf_set_binding_root(VALUE self, VALUE document,
747
+ VALUE addr)
748
+ {
749
+ void *node;
750
+ int st;
751
+
752
+ (void)self;
753
+ resolve_binding_classes();
754
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
755
+ rb_raise(c_use_after_free_error,
756
+ "owning document has been freed");
757
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
758
+ rb_raise(c_readonly_error,
759
+ "document is readonly — mutation attempted");
760
+ node = (void *)(uintptr_t)NUM2ULL(addr);
761
+ rb_ivar_set(document, id_iv_version,
762
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
763
+ st = f_set_root(doc_ptr_of(document), node);
764
+ return INT2FIX(st);
765
+ }
766
+
733
767
  /* ---- C-yield traversal (TODO.perf/27) ----------------------------
734
768
  * Same contracts as the Ruby FFI::Function versions without the
735
769
  * per-call closure: traverse is post-order with abort-at-self
@@ -1850,6 +1884,102 @@ static VALUE nf_copy_binding_element(VALUE self, VALUE document,
1850
1884
  return new_doc;
1851
1885
  }
1852
1886
 
1887
+ /* Shared document-wrapper construction from a C document
1888
+ * pointer: ivar-seeded wrapper + lifetime handle. Returns the
1889
+ * wrapper. (TODO.perf/35 — the create face inlines the same
1890
+ * steps.) */
1891
+ static VALUE build_binding_document(void *doc)
1892
+ {
1893
+ VALUE new_doc, doc_addr, handle, freed;
1894
+ struct doc_handle *h;
1895
+
1896
+ doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
1897
+ new_doc = rb_obj_alloc(c_b_document);
1898
+ rb_iv_set(new_doc, "@c_ptr",
1899
+ rb_funcall(c_ffi_pointer, id_ptr_new, 1, doc_addr));
1900
+ rb_iv_set(new_doc, "@c_address", doc_addr);
1901
+ freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
1902
+ rb_iv_set(new_doc, "@freed", freed);
1903
+ rb_iv_set(new_doc, "@readonly", Qfalse);
1904
+ rb_iv_set(new_doc, "@version", INT2FIX(0));
1905
+ handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
1906
+ &dh_type, h);
1907
+ h->doc = doc;
1908
+ rb_ivar_set(new_doc, id_iv_doc_handle, handle);
1909
+ return new_doc;
1910
+ }
1911
+
1912
+ /* Document.parse default path in one dispatch (TODO.perf/35):
1913
+ * leptris_parse_string + wrapper + handle. Qnil = parse failure
1914
+ * (the caller raises with the same message shape). */
1915
+ static VALUE nf_parse_binding_document(VALUE self, VALUE xml)
1916
+ {
1917
+ void *doc;
1918
+
1919
+ (void)self;
1920
+ resolve_binding_classes();
1921
+ doc = f_parse_str(RSTRING_PTR(StringValue(xml)),
1922
+ (size_t)RSTRING_LEN(StringValue(xml)), NULL);
1923
+ return doc ? build_binding_document(doc) : Qnil;
1924
+ }
1925
+
1926
+ /* Fragment lane (TODO.perf/34): parse the markup against the
1927
+ * document; returns the fragment's ADDRESS, or Qfalse when the
1928
+ * parse fails (status is not surfaced — the caller re-runs the
1929
+ * FFI path for the exact error). */
1930
+ static VALUE nf_parse_fragment_addr(VALUE self, VALUE document,
1931
+ VALUE xml)
1932
+ {
1933
+ void *frag;
1934
+ int status;
1935
+
1936
+ (void)self;
1937
+ resolve_binding_classes();
1938
+ frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
1939
+ (size_t)RSTRING_LEN(StringValue(xml)),
1940
+ doc_ptr_of(document), &status);
1941
+ return frag ? ULL2NUM((uint64_t)(uintptr_t)frag) : Qfalse;
1942
+ }
1943
+
1944
+ /* One-shot markup append (TODO.perf/34): parse the markup, walk
1945
+ * the fragment's children, append each to the parent — one
1946
+ * readonly gate + version bump for the whole batch. Returns the
1947
+ * appended count; -1 = parse failure; status codes surface as
1948
+ * negatives (-1000 - st) for the caller's check_status path. */
1949
+ static VALUE nf_append_markup(VALUE self, VALUE document,
1950
+ VALUE parent_addr, VALUE xml)
1951
+ {
1952
+ void *frag, *parent, *child, *next_child;
1953
+ int status, count = 0;
1954
+
1955
+ (void)self;
1956
+ resolve_binding_classes();
1957
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
1958
+ rb_raise(c_use_after_free_error,
1959
+ "owning document has been freed");
1960
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
1961
+ rb_raise(c_readonly_error,
1962
+ "document is readonly — mutation attempted");
1963
+ frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
1964
+ (size_t)RSTRING_LEN(StringValue(xml)),
1965
+ doc_ptr_of(document), &status);
1966
+ if (!frag)
1967
+ return INT2FIX(-1);
1968
+ parent = (void *)(uintptr_t)NUM2ULL(parent_addr);
1969
+ rb_ivar_set(document, id_iv_version,
1970
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
1971
+ /* Appending DETACHES the child from the fragment — capture
1972
+ * the next sibling before each move or the walk corrupts. */
1973
+ for (child = f_first_child(frag); child != NULL; child = next_child) {
1974
+ next_child = f_next_sibling(child);
1975
+ status = f_append_child(parent, child);
1976
+ if (status != 0)
1977
+ return INT2FIX(-1000 - status);
1978
+ count++;
1979
+ }
1980
+ return INT2FIX(count);
1981
+ }
1982
+
1853
1983
  static VALUE nf_doc_handle_attach(VALUE self, VALUE document)
1854
1984
  {
1855
1985
  struct doc_handle *h;
@@ -2071,6 +2201,14 @@ void Init_native(void)
2071
2201
  nf_last_element_child, 2);
2072
2202
  rb_define_module_function(m_native, "copy_binding_element",
2073
2203
  nf_copy_binding_element, 2);
2204
+ rb_define_module_function(m_native, "set_binding_root",
2205
+ nf_set_binding_root, 2);
2206
+ rb_define_module_function(m_native, "parse_binding_document",
2207
+ nf_parse_binding_document, 1);
2208
+ rb_define_module_function(m_native, "parse_fragment_addr",
2209
+ nf_parse_fragment_addr, 2);
2210
+ rb_define_module_function(m_native, "append_markup",
2211
+ nf_append_markup, 3);
2074
2212
  rb_define_module_function(m_native, "fast_inner_xml",
2075
2213
  nf_fast_inner_xml, 1);
2076
2214
  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.9"
5
5
  end
@@ -70,6 +70,15 @@ class Leptris::XML::Document
70
70
  # ParseOptions.new here would only discover flags==0 (#187 —
71
71
  # per-call overhead is most of a small-document parse).
72
72
  if options.nil? && !recover
73
+ # TODO.perf/35: parse + wrapper + lifetime handle in one C
74
+ # dispatch; failure raises through the shared path below.
75
+ if defined?(Leptris::XML::NATIVE_FAST)
76
+ doc = Leptris::XML::Native.parse_binding_document(xml)
77
+ raise Leptris::XML::ParseError,
78
+ "leptris_parse_string failed: " +
79
+ Leptris::XML::FFI.leptris_last_error.to_s if doc.nil?
80
+ return doc.tap { |d| d.readonly! if readonly }
81
+ end
73
82
  raw = Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, nil)
74
83
  if raw.null?
75
84
  raise Leptris::XML::ParseError,
@@ -283,14 +292,29 @@ class Leptris::XML::Document
283
292
  # document's pool until #free).
284
293
  def root=(element)
285
294
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
295
+ # Same mutation gate as every other writer — the FFI path
296
+ # historically missed it (TODO.perf/33: the C face exposed
297
+ # the divergence; readonly is one-way, so one ivar read).
298
+ if @readonly
299
+ raise Leptris::XML::ReadOnlyError,
300
+ "document is readonly — root= attempted"
301
+ end
286
302
  # A document root has no in-scope declarations of its own —
287
303
  # lift everything the element's source scope carried (#178).
288
304
  unless Leptris::XML::Element.skip_adoption_lift?(element)
289
305
  Leptris::XML::Element.lift_namespaces_for_adoption(element, {})
290
306
  end
291
- Leptris::XML::FFI.check_status(
292
- Leptris::XML::FFI.leptris_document_set_root(c_ptr, element.c_ptr))
293
- @version += 1
307
+ # TODO.perf/33: gates + bump + engine set_root in one C
308
+ # dispatch (the FFI call plus the c_ptr materialization fed
309
+ # ~5% of a fresh-doc build).
310
+ if defined?(Leptris::XML::NATIVE_FAST)
311
+ Leptris::XML::FFI.check_status(
312
+ Leptris::XML::Native.set_binding_root(self, element.c_address))
313
+ else
314
+ Leptris::XML::FFI.check_status(
315
+ Leptris::XML::FFI.leptris_document_set_root(c_ptr, element.c_ptr))
316
+ @version += 1
317
+ end
294
318
  # Seed the root memo through wrap: a cross-document element
295
319
  # must enter THIS document's identity cache with @document
296
320
  # pointing here, not ride its source-document wrapper.
@@ -9,12 +9,32 @@ class Leptris::XML::DocumentFragment
9
9
 
10
10
  attr_reader :document, :c_ptr
11
11
 
12
- def initialize(document, c_ptr)
12
+ def initialize(document, c_ptr, c_address: nil)
13
13
  @document = document
14
14
  @c_ptr = c_ptr
15
+ # TODO.perf/34: the address may arrive before any Pointer
16
+ # exists (the C parse face returns an Integer).
17
+ @c_address = c_address || c_ptr&.address
18
+ end
19
+
20
+ # Integer twin — reading it never materializes a Pointer.
21
+ def c_address
22
+ @c_address
23
+ end
24
+
25
+ def c_ptr
26
+ @c_ptr ||= ::FFI::Pointer.new(@c_address)
15
27
  end
16
28
 
17
29
  def self.parse(xml, document)
30
+ # TODO.perf/34: the C face answers the fragment address with
31
+ # no status MemoryPointer and no Pointer mint; failures fall
32
+ # back to the FFI pair for the exact error.
33
+ if defined?(Leptris::XML::NATIVE_FAST)
34
+ addr = Leptris::XML::Native.parse_fragment_addr(
35
+ document, xml.to_s)
36
+ return new(document, nil, c_address: addr) if addr != false
37
+ end
18
38
  raw, status = Leptris::XML::FFI.parse_fragment_with_status(
19
39
  xml.to_s, document.c_ptr)
20
40
  if raw.null?
@@ -25,6 +45,11 @@ class Leptris::XML::DocumentFragment
25
45
  end
26
46
 
27
47
  def children
48
+ # TODO.perf/34: one C pass (fragment nodes live in the
49
+ # document pool — the document's cache and version apply).
50
+ if defined?(Leptris::XML::NATIVE_FAST)
51
+ return Leptris::XML::Native.bulk_children(@document, @c_address)
52
+ end
28
53
  pointers, kinds = Leptris::XML::FFI.fetch_children(c_ptr)
29
54
  nodes = Array.new(pointers.size) do |i|
30
55
  Leptris::XML::Node.wrap(pointers[i], @document, node_type: kinds[i])
@@ -555,6 +555,25 @@ class Leptris::XML::Element < Leptris::XML::Node
555
555
  Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
556
556
  node_or_markup
557
557
  when String
558
+ # TODO.perf/34: one dispatch parses the markup and appends
559
+ # every fragment child (single gate + bump). Negative
560
+ # returns: -1 re-runs the FFI path for the exact parse
561
+ # error; -1000-st routes through check_status.
562
+ if native_fast_children?
563
+ r = Leptris::XML::Native.append_markup(
564
+ @document, @c_address, node_or_markup)
565
+ if r.is_a?(Integer) && r >= 0
566
+ return Leptris::XML::NodeSet.new(@document, []) if r.zero?
567
+ return Leptris::XML::NodeSet.new(@document, last_added(r))
568
+ end
569
+ if r == -1
570
+ # parse failure: reproduce the exact error via the
571
+ # legacy path (it re-fails fast)
572
+ Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
573
+ else
574
+ Leptris::XML::FFI.check_status(-r - 1000)
575
+ end
576
+ end
558
577
  frag = Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
559
578
  added = []
560
579
  frag.children.each do |n|
@@ -567,6 +586,14 @@ class Leptris::XML::Element < Leptris::XML::Node
567
586
  raise ArgumentError, "add_child expects a Node or String, got #{node_or_markup.class}"
568
587
  end
569
588
  end
589
+ # The markup-append face appends in fragment order; the added
590
+ # nodes are this element's last +count+ children.
591
+ def last_added(count)
592
+ kids = children.to_a
593
+ kids.last(count)
594
+ end
595
+ private :last_added
596
+
570
597
  alias_method :<<, :add_child
571
598
 
572
599
  def namespace
@@ -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.9
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,11 @@ 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
105
+ - TODO.perf/34-fragment-fast-lane.md
106
+ - TODO.perf/35-parse-default-c-face.md
102
107
  - TODO.restructure/01-constraint-compliance-audit.md
103
108
  - TODO.restructure/02-deep-copy-seam.md
104
109
  - TODO.restructure/03-evaluation-context-seam.md