leptris 1.9.174.8 → 1.9.174.10

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: e3bbbd00177ef272738c9d0d9562beb33cc2ed100145bc843785fd56ae0c5f22
4
- data.tar.gz: c47af3b6769fb4e9cb233128df91b36bf9cecae4d23e06b9f94ad60caf9d498a
3
+ metadata.gz: f0e347192431e6ed3ff15bed0fca55fb103ba7307a33d889014e72c3ece0dba8
4
+ data.tar.gz: f7edb28afa62063bbc1f0ab2cfcc349619a8944249f69f41b40e054e016b5b45
5
5
  SHA512:
6
- metadata.gz: e951276162615da4fdecfd7d500bdd9ddd2a378188190d9edf14341713243f1ce802fa2a129f5afb07d676ce2977e7d0e59e44b474dce656432cdb57d99b5fac
7
- data.tar.gz: 812ae80ffb569ae41fa0734858a8f487f31f3a22689fc221686b801cf1922e69d4331615a678e1dd5a5589c2c464b6fe6ae51c4504b365448ea63a6ad6215628
6
+ metadata.gz: 660309e07c7ce84a4a68bdca9c46a1970f0af6a1bd2e316acca600de60cf54cbd445cd0814232593dd1752f59965d7df8ede74c14da977005b48cbec4ec276fd
7
+ data.tar.gz: ffe8c0e4f6fc98f4bda50187b3c740e278aae6c82ec384f1ac6843673276b916910a1ab72a4c740a69e0006992418c83eb92280a619b18fac69c8568f5b7639e
data/CHANGELOG.md CHANGED
@@ -5,6 +5,43 @@ 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.10] - 2026-09-16
9
+
10
+ ### Changed
11
+
12
+ - **Status raises in C (TODO.perf/36)**: every C mutation face
13
+ raises `Leptris::XML::Error` with the exact `status_message`
14
+ format — the Ruby `check_status` dispatch leaves the fast
15
+ mutation paths (and the success/lift-marker collision it
16
+ briefly introduced — Qtrue on success now — was caught by the
17
+ bottom-up construction spec). `to_xml(encoding:)` rides the ext
18
+ serializer face (encoding VALUE, nil → NULL).
19
+ - **Document lazy Pointer (TODO.perf/37)**: `#c_ptr` materializes
20
+ once over `@c_address` (freed docs answer nil); the parse/create
21
+ faces skip the Pointer mint entirely. The ffi gem rejects
22
+ Integers for typedef'd pointer params — the search paths keep
23
+ the lazy Pointer.
24
+ - **Atomic bundle vendoring**: replacing a mapped `native.bundle`
25
+ in place gets the next loader SIGKILLed (CODESIGNING Invalid
26
+ Page) — the Rakefile now cp+mv's atomically.
27
+
28
+ ## [1.9.174.9] - 2026-09-16
29
+
30
+ ### Changed
31
+
32
+ - **Fragment fast lane (TODO.perf/34)**: `add_child(String)` runs
33
+ one C dispatch for the whole markup add — fragment parse +
34
+ every child append + a single readonly gate and version bump
35
+ (the move-during-iteration hazard handled: appending detaches,
36
+ so the walk captures next before each move). The fragment parse
37
+ face returns an address (no status MemoryPointer, no Pointer);
38
+ `DocumentFragment#children` rides the bulk face; parse failures
39
+ fall back to the legacy path for the exact error.
40
+ - **Document.parse default path as one C dispatch (TODO.perf/35)**:
41
+ engine parse + ivar-seeded wrapper + lifetime handle in one
42
+ call — small parses measured 15.6µs → 6.8µs (~2.3x, under host
43
+ load ~100; the shape is self-contained).
44
+
8
45
  ## [1.9.174.8] - 2026-09-16
9
46
 
10
47
  ### Fixed
data/Rakefile CHANGED
@@ -172,7 +172,12 @@ task :compile do
172
172
  # Build-log proof of the linkage contract: libSystem only.
173
173
  puts `otool -L #{bundle}`
174
174
  end
175
- cp(bundle, "lib/leptris/xml/#{File.basename(bundle)}")
175
+ # Atomic replace: overwriting a bundle another process still
176
+ # has mapped gets that loader SIGKILLed (CODESIGNING Invalid
177
+ # Page) — cp to a temp name, then mv.
178
+ dest = "lib/leptris/xml/#{File.basename(bundle)}"
179
+ cp(bundle, "#{dest}.new")
180
+ mv("#{dest}.new", dest)
176
181
  puts "Vendored native layer (#{File.basename(bundle)}) into lib/leptris/xml/"
177
182
  end
178
183
  end
@@ -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).
@@ -0,0 +1,34 @@
1
+ # 36 — Status raises in C + encoding on the serializer faces
2
+
3
+ Status: DONE (1.9.174.10)
4
+
5
+ Every C mutation face returns INT2FIX(st) for a Ruby check_status
6
+ dispatch (~30ns per mutation, plus a whole class of
7
+ "forgot-to-check" bug the compiler cannot catch). With
8
+ leptris_status_string and leptris_last_error both dlsym'd, the
9
+ faces raise Leptris::XML::Error in C with the exact
10
+ status_message format ("base" or "base (detail)") — call sites
11
+ shrink to the bare face call.
12
+
13
+ The ext serializer faces skip the encoding-tagged shape entirely
14
+ (to_xml(encoding: ...) still builds the options struct through
15
+ FFI): the faces gain an encoding VALUE (nil -> NULL, the Ruby
16
+ string's bytes stay valid across the synchronous engine call).
17
+
18
+ Gates: every mutation's failure message byte-identical to
19
+ check_status; encoding-tagged to_xml output identical to the FFI
20
+ path (declaration, no anchor lifetime issues); suite both modes.
21
+
22
+ ## Outcome (1.9.174.10)
23
+
24
+ All seven mutation faces raise Leptris::XML::Error in C with the
25
+ exact status_message format (status_string + optional
26
+ " (last_error)") — the Ruby check_status dispatch disappears from
27
+ every fast mutation. TRAP FOUND: the faces' success return became
28
+ Qnil, colliding with the lift-needed marker — every fast mutation
29
+ fell through to the slow path and re-applied (order-corrupting
30
+ for inserts; the v180 bottom-up spec caught it). Success now
31
+ returns Qtrue. The serializer faces take an encoding VALUE (nil
32
+ -> NULL; the Ruby string's bytes live across the synchronous
33
+ call), and to_xml(encoding:) rides the ext face instead of the
34
+ options-struct FFI path.
@@ -0,0 +1,28 @@
1
+ # 37 — Document lazy Pointer + address-based xpath contexts
2
+
3
+ Status: DONE (1.9.174.10)
4
+
5
+ Documents still mint an FFI::Pointer eagerly (the parse/create
6
+ faces pay the funcall + one allocation + its GC visit per
7
+ document). Make #c_ptr lazy like Node's (attr_reader becomes a
8
+ materializer over @c_address), skip the Pointer in
9
+ build_binding_document, and feed the search paths Integer
10
+ addresses (the ffi gem converts Integers for :pointer params) so
11
+ per-xpath document Pointers never materialize at all.
12
+
13
+ Gates: c_ptr stability and public API unchanged; every FFI
14
+ document consumer still works (options paths, c14n, fragment
15
+ fallback); suite both modes.
16
+
17
+ ## Outcome (1.9.174.10)
18
+
19
+ Document#c_ptr materializes lazily over @c_address (freed docs
20
+ answer nil — never re-materialize); build_binding_document skips
21
+ the Pointer funcall (one alloc + ~200ns per document). TRAP
22
+ FOUND: this ffi version does NOT convert Integers for typedef'd
23
+ pointer params — passing c_address into the compiled-eval calls
24
+ failed with ":pointer argument is not a valid pointer"; the
25
+ search paths keep passing the (now lazy, once-per-document)
26
+ Pointer. Also: replacing a mapped native.bundle in place gets
27
+ the next loader SIGKILLed (CODESIGNING Invalid Page) — the
28
+ vendoring cp is now an atomic cp+mv.
@@ -94,6 +94,10 @@ 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);
100
+ typedef const char *(*status_str_fn)(int);
97
101
  static set_str_fn f_elem_set_name, f_elem_set_text, f_text_set_content;
98
102
  static node_unlink_fn f_node_unlink;
99
103
  static traverse_fn f_node_traverse;
@@ -101,6 +105,10 @@ static visit_fn f_node_visit;
101
105
  static free_str_fn f_free_str;
102
106
  static node_xpath_fn f_node_xpath;
103
107
  static elem_copy_fn f_elem_copy;
108
+ static parse_str_fn f_parse_str;
109
+ static parse_frag_fn f_parse_frag;
110
+ static last_err_fn f_last_err;
111
+ static status_str_fn f_status_str;
104
112
  static set_root_fn f_set_root;
105
113
  static doc_free_fn f_doc_free;
106
114
 
@@ -119,8 +127,10 @@ static void resolve_binding_classes(void);
119
127
  static VALUE binding_cache_of(VALUE document);
120
128
  static VALUE binding_klass_for(int kind);
121
129
  static VALUE c_iteration_scope;
130
+ static VALUE c_leptris_error;
122
131
  static VALUE c_b_document, c_b_freed, c_ffi_pointer;
123
132
  static ID id_ptr_new, id_freed_new, id_alive;
133
+ static void check_status_c(int st);
124
134
 
125
135
  #define NT_ELEMENT 0
126
136
 
@@ -221,6 +231,10 @@ static void resolve_symbols(const char *lib_path)
221
231
  f_free_str = (free_str_fn)lib_sym(h, "leptris_free_string");
222
232
  f_node_xpath = (node_xpath_fn)lib_sym(h, "leptris_node_get_xpath");
223
233
  f_elem_copy = (elem_copy_fn)lib_sym(h, "leptris_element_copy");
234
+ f_parse_str = (parse_str_fn)lib_sym(h, "leptris_parse_string");
235
+ f_parse_frag = (parse_frag_fn)lib_sym(h, "leptris_parse_fragment");
236
+ f_last_err = (last_err_fn)lib_sym(h, "leptris_last_error");
237
+ f_status_str = (status_str_fn)lib_sym(h, "leptris_status_string");
224
238
  f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
225
239
  f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
226
240
  if (!f_elem_name || !f_text_content || !f_attr ||
@@ -233,7 +247,8 @@ static void resolve_symbols(const char *lib_path)
233
247
  !f_pi_data || !f_elem_set_name || !f_elem_set_text ||
234
248
  !f_text_set_content || !f_node_unlink ||
235
249
  !f_node_traverse || !f_node_visit || !f_free_str ||
236
- !f_node_xpath || !f_elem_copy ||
250
+ !f_node_xpath || !f_elem_copy || !f_parse_str ||
251
+ !f_parse_frag || !f_last_err || !f_status_str ||
237
252
  !f_doc_free ||
238
253
  !f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
239
254
  !f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
@@ -630,7 +645,8 @@ static VALUE nf_append_binding_child(VALUE self, VALUE document,
630
645
  rb_ivar_set(document, id_iv_version,
631
646
  LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
632
647
  st = f_append_child(parent, child);
633
- return INT2FIX(st);
648
+ check_status_c(st);
649
+ return Qtrue;
634
650
  }
635
651
 
636
652
  /* C-bound set_attribute (TODO.perf/11, #204 gap row 0.25x): the
@@ -659,7 +675,8 @@ static VALUE nf_set_binding_attribute(VALUE self, VALUE document,
659
675
  * embedded-NUL value raises rather than truncates. */
660
676
  st = f_set_attr(node, RSTRING_PTR(StringValue(name)),
661
677
  StringValueCStr(value));
662
- return INT2FIX(st);
678
+ check_status_c(st);
679
+ return Qtrue;
663
680
  }
664
681
 
665
682
  /* C-bound value mutations (TODO.perf/23): the same gates + bump
@@ -682,7 +699,8 @@ static VALUE nf_set_binding_name(VALUE self, VALUE document,
682
699
  rb_ivar_set(document, id_iv_version,
683
700
  LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
684
701
  st = f_elem_set_name(node, StringValueCStr(name));
685
- return INT2FIX(st);
702
+ check_status_c(st);
703
+ return Qtrue;
686
704
  }
687
705
 
688
706
  /* Element text or text-node content — kind-dispatched in C. */
@@ -706,7 +724,8 @@ static VALUE nf_set_binding_text(VALUE self, VALUE document,
706
724
  st = f_node_type(node) == NT_ELEMENT
707
725
  ? f_elem_set_text(node, StringValueCStr(content))
708
726
  : f_text_set_content(node, StringValueCStr(content));
709
- return INT2FIX(st);
727
+ check_status_c(st);
728
+ return Qtrue;
710
729
  }
711
730
 
712
731
  static VALUE nf_unlink_binding_node(VALUE self, VALUE document,
@@ -727,7 +746,8 @@ static VALUE nf_unlink_binding_node(VALUE self, VALUE document,
727
746
  rb_ivar_set(document, id_iv_version,
728
747
  LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
729
748
  st = f_node_unlink(node);
730
- return INT2FIX(st);
749
+ check_status_c(st);
750
+ return Qtrue;
731
751
  }
732
752
 
733
753
  /* C-bound root= (TODO.perf/33): gates + bump + engine set_root
@@ -751,7 +771,8 @@ static VALUE nf_set_binding_root(VALUE self, VALUE document,
751
771
  rb_ivar_set(document, id_iv_version,
752
772
  LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
753
773
  st = f_set_root(doc_ptr_of(document), node);
754
- return INT2FIX(st);
774
+ check_status_c(st);
775
+ return Qtrue;
755
776
  }
756
777
 
757
778
  /* ---- C-yield traversal (TODO.perf/27) ----------------------------
@@ -1064,7 +1085,8 @@ static VALUE nf_insert_binding_child(VALUE self, VALUE document,
1064
1085
  rb_raise(rb_eArgError, "invalid insert mode %d", m);
1065
1086
  return Qnil;
1066
1087
  }
1067
- return INT2FIX(st);
1088
+ check_status_c(st);
1089
+ return Qtrue;
1068
1090
  }
1069
1091
 
1070
1092
  /* Builder factories (#149): create in C, wrap as NativeNode — no
@@ -1215,6 +1237,7 @@ static void resolve_binding_classes(void)
1215
1237
  c_b_document = rb_path2class("Leptris::XML::Document");
1216
1238
  c_b_freed = rb_path2class("Leptris::XML::Document::Freed");
1217
1239
  c_iteration_scope = rb_path2class("Leptris::XML::IterationScope");
1240
+ c_leptris_error = rb_path2class("Leptris::XML::Error");
1218
1241
  id_ptr_new = rb_intern("new");
1219
1242
  rb_gc_register_mark_object(c_b_element);
1220
1243
  rb_gc_register_mark_object(c_b_text);
@@ -1229,6 +1252,7 @@ static void resolve_binding_classes(void)
1229
1252
  rb_gc_register_mark_object(c_b_document);
1230
1253
  rb_gc_register_mark_object(c_b_freed);
1231
1254
  rb_gc_register_mark_object(c_iteration_scope);
1255
+ rb_gc_register_mark_object(c_leptris_error);
1232
1256
  }
1233
1257
 
1234
1258
  static VALUE binding_klass_for(int kind)
@@ -1625,7 +1649,8 @@ static char *ser_buf;
1625
1649
  static size_t ser_cap;
1626
1650
 
1627
1651
  static VALUE fast_serialize(serialize_into_fn fn, void *node,
1628
- int indent, int xml_declaration)
1652
+ int indent, int xml_declaration,
1653
+ VALUE encoding)
1629
1654
  {
1630
1655
  struct serialize_opts opts;
1631
1656
  char stack_buf[4096];
@@ -1636,7 +1661,11 @@ static VALUE fast_serialize(serialize_into_fn fn, void *node,
1636
1661
 
1637
1662
  opts.indent = indent;
1638
1663
  opts.xml_declaration = xml_declaration;
1639
- opts.encoding = NULL;
1664
+ /* The Ruby string's bytes stay valid across the synchronous
1665
+ * engine call (TODO.perf/36). */
1666
+ opts.encoding = NIL_P(encoding)
1667
+ ? NULL
1668
+ : StringValueCStr(encoding);
1640
1669
  /* Probe the largest buffer we own: a big scratch from a prior
1641
1670
  * call usually fits (single serialization); cold calls start
1642
1671
  * on the stack buffer. */
@@ -1662,21 +1691,25 @@ static VALUE fast_serialize(serialize_into_fn fn, void *node,
1662
1691
  }
1663
1692
 
1664
1693
  static VALUE nf_fast_document_xml(VALUE self, VALUE addr,
1665
- VALUE indent, VALUE decl)
1694
+ VALUE indent, VALUE decl,
1695
+ VALUE encoding)
1666
1696
  {
1667
1697
  (void)self;
1668
1698
  return fast_serialize(f_doc_serialize,
1669
1699
  (void *)(uintptr_t)NUM2ULL(addr),
1670
- NUM2INT(indent), RTEST(decl) ? 1 : 0);
1700
+ NUM2INT(indent), RTEST(decl) ? 1 : 0,
1701
+ encoding);
1671
1702
  }
1672
1703
 
1673
1704
  static VALUE nf_fast_element_xml(VALUE self, VALUE addr,
1674
- VALUE indent, VALUE decl)
1705
+ VALUE indent, VALUE decl,
1706
+ VALUE encoding)
1675
1707
  {
1676
1708
  (void)self;
1677
1709
  return fast_serialize(f_elem_serialize,
1678
1710
  (void *)(uintptr_t)NUM2ULL(addr),
1679
- NUM2INT(indent), RTEST(decl) ? 1 : 0);
1711
+ NUM2INT(indent), RTEST(decl) ? 1 : 0,
1712
+ encoding);
1680
1713
  }
1681
1714
 
1682
1715
  /* ---- inner_html in one C pass (TODO.perf/18) --------------------
@@ -1851,8 +1884,6 @@ static VALUE nf_copy_binding_element(VALUE self, VALUE document,
1851
1884
  return Qnil;
1852
1885
  doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
1853
1886
  new_doc = rb_obj_alloc(c_b_document);
1854
- rb_iv_set(new_doc, "@c_ptr",
1855
- rb_funcall(c_ffi_pointer, id_ptr_new, 1, doc_addr));
1856
1887
  rb_iv_set(new_doc, "@c_address", doc_addr);
1857
1888
  freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
1858
1889
  rb_iv_set(new_doc, "@freed", freed);
@@ -1874,6 +1905,119 @@ static VALUE nf_copy_binding_element(VALUE self, VALUE document,
1874
1905
  return new_doc;
1875
1906
  }
1876
1907
 
1908
+ /* Shared document-wrapper construction from a C document
1909
+ * pointer: ivar-seeded wrapper + lifetime handle. Returns the
1910
+ * wrapper. (TODO.perf/35 — the create face inlines the same
1911
+ * steps.) */
1912
+ static VALUE build_binding_document(void *doc)
1913
+ {
1914
+ VALUE new_doc, doc_addr, handle, freed;
1915
+ struct doc_handle *h;
1916
+
1917
+ doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
1918
+ new_doc = rb_obj_alloc(c_b_document);
1919
+ rb_iv_set(new_doc, "@c_ptr",
1920
+ rb_funcall(c_ffi_pointer, id_ptr_new, 1, doc_addr));
1921
+ rb_iv_set(new_doc, "@c_address", doc_addr);
1922
+ freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
1923
+ rb_iv_set(new_doc, "@freed", freed);
1924
+ rb_iv_set(new_doc, "@readonly", Qfalse);
1925
+ rb_iv_set(new_doc, "@version", INT2FIX(0));
1926
+ handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
1927
+ &dh_type, h);
1928
+ h->doc = doc;
1929
+ rb_ivar_set(new_doc, id_iv_doc_handle, handle);
1930
+ return new_doc;
1931
+ }
1932
+
1933
+ /* check_status in C (TODO.perf/36): the exact status_message
1934
+ * format — "base" or "base (detail)" — so the mutation faces
1935
+ * raise instead of returning codes for a Ruby dispatch. */
1936
+ static void check_status_c(int st)
1937
+ {
1938
+ const char *base, *detail;
1939
+
1940
+ if (st == 0)
1941
+ return;
1942
+ base = f_status_str(st);
1943
+ detail = f_last_err();
1944
+ if (detail && *detail)
1945
+ rb_raise(c_leptris_error, "%s (%s)",
1946
+ base ? base : "?", detail);
1947
+ rb_raise(c_leptris_error, "%s", base ? base : "?");
1948
+ }
1949
+
1950
+ /* Document.parse default path in one dispatch (TODO.perf/35):
1951
+ * leptris_parse_string + wrapper + handle. Qnil = parse failure
1952
+ * (the caller raises with the same message shape). */
1953
+ static VALUE nf_parse_binding_document(VALUE self, VALUE xml)
1954
+ {
1955
+ void *doc;
1956
+
1957
+ (void)self;
1958
+ resolve_binding_classes();
1959
+ doc = f_parse_str(RSTRING_PTR(StringValue(xml)),
1960
+ (size_t)RSTRING_LEN(StringValue(xml)), NULL);
1961
+ return doc ? build_binding_document(doc) : Qnil;
1962
+ }
1963
+
1964
+ /* Fragment lane (TODO.perf/34): parse the markup against the
1965
+ * document; returns the fragment's ADDRESS, or Qfalse when the
1966
+ * parse fails (status is not surfaced — the caller re-runs the
1967
+ * FFI path for the exact error). */
1968
+ static VALUE nf_parse_fragment_addr(VALUE self, VALUE document,
1969
+ VALUE xml)
1970
+ {
1971
+ void *frag;
1972
+ int status;
1973
+
1974
+ (void)self;
1975
+ resolve_binding_classes();
1976
+ frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
1977
+ (size_t)RSTRING_LEN(StringValue(xml)),
1978
+ doc_ptr_of(document), &status);
1979
+ return frag ? ULL2NUM((uint64_t)(uintptr_t)frag) : Qfalse;
1980
+ }
1981
+
1982
+ /* One-shot markup append (TODO.perf/34): parse the markup, walk
1983
+ * the fragment's children, append each to the parent — one
1984
+ * readonly gate + version bump for the whole batch. Returns the
1985
+ * appended count; -1 = parse failure; status codes surface as
1986
+ * negatives (-1000 - st) for the caller's check_status path. */
1987
+ static VALUE nf_append_markup(VALUE self, VALUE document,
1988
+ VALUE parent_addr, VALUE xml)
1989
+ {
1990
+ void *frag, *parent, *child, *next_child;
1991
+ int status, count = 0;
1992
+
1993
+ (void)self;
1994
+ resolve_binding_classes();
1995
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
1996
+ rb_raise(c_use_after_free_error,
1997
+ "owning document has been freed");
1998
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
1999
+ rb_raise(c_readonly_error,
2000
+ "document is readonly — mutation attempted");
2001
+ frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
2002
+ (size_t)RSTRING_LEN(StringValue(xml)),
2003
+ doc_ptr_of(document), &status);
2004
+ if (!frag)
2005
+ return INT2FIX(-1);
2006
+ parent = (void *)(uintptr_t)NUM2ULL(parent_addr);
2007
+ rb_ivar_set(document, id_iv_version,
2008
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
2009
+ /* Appending DETACHES the child from the fragment — capture
2010
+ * the next sibling before each move or the walk corrupts. */
2011
+ for (child = f_first_child(frag); child != NULL; child = next_child) {
2012
+ next_child = f_next_sibling(child);
2013
+ status = f_append_child(parent, child);
2014
+ if (status != 0)
2015
+ check_status_c(status);
2016
+ count++;
2017
+ }
2018
+ return INT2FIX(count);
2019
+ }
2020
+
1877
2021
  static VALUE nf_doc_handle_attach(VALUE self, VALUE document)
1878
2022
  {
1879
2023
  struct doc_handle *h;
@@ -2097,6 +2241,12 @@ void Init_native(void)
2097
2241
  nf_copy_binding_element, 2);
2098
2242
  rb_define_module_function(m_native, "set_binding_root",
2099
2243
  nf_set_binding_root, 2);
2244
+ rb_define_module_function(m_native, "parse_binding_document",
2245
+ nf_parse_binding_document, 1);
2246
+ rb_define_module_function(m_native, "parse_fragment_addr",
2247
+ nf_parse_fragment_addr, 2);
2248
+ rb_define_module_function(m_native, "append_markup",
2249
+ nf_append_markup, 3);
2100
2250
  rb_define_module_function(m_native, "fast_inner_xml",
2101
2251
  nf_fast_inner_xml, 1);
2102
2252
  rb_define_module_function(m_native, "doc_handle_attach",
@@ -2119,7 +2269,7 @@ void Init_native(void)
2119
2269
  nf_bulk_element_children, 2);
2120
2270
  rb_define_module_function(m_native, "bulk_xpath", nf_bulk_xpath, 2);
2121
2271
  rb_define_module_function(m_native, "fast_document_xml",
2122
- nf_fast_document_xml, 3);
2272
+ nf_fast_document_xml, 4);
2123
2273
  rb_define_module_function(m_native, "fast_element_xml",
2124
- nf_fast_element_xml, 3);
2274
+ nf_fast_element_xml, 4);
2125
2275
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.174.8"
4
+ VERSION = "1.9.174.10"
5
5
  end
@@ -3,7 +3,6 @@
3
3
  require "ffi"
4
4
 
5
5
  class Leptris::XML::Document
6
- attr_reader :c_ptr
7
6
 
8
7
  # @api private
9
8
  # Internal flag container shared between the Document instance and its
@@ -57,6 +56,19 @@ class Leptris::XML::Document
57
56
  # document for trees that are freed before any wrap.
58
57
  end
59
58
 
59
+ # TODO.perf/37: the Integer address is canonical; the Pointer
60
+ # materializes only when read (parse/create faces skip it).
61
+ # A freed document answers nil — never re-materialize a stale
62
+ # or absent address.
63
+ def c_ptr
64
+ @c_ptr ||= (@c_address && ::FFI::Pointer.new(@c_address))
65
+ end
66
+
67
+ # The Integer twin of #c_ptr.
68
+ def c_address
69
+ @c_address
70
+ end
71
+
60
72
  def wrapper_cache
61
73
  @wrapper_cache ||= {}
62
74
  end
@@ -70,6 +82,15 @@ class Leptris::XML::Document
70
82
  # ParseOptions.new here would only discover flags==0 (#187 —
71
83
  # per-call overhead is most of a small-document parse).
72
84
  if options.nil? && !recover
85
+ # TODO.perf/35: parse + wrapper + lifetime handle in one C
86
+ # dispatch; failure raises through the shared path below.
87
+ if defined?(Leptris::XML::NATIVE_FAST)
88
+ doc = Leptris::XML::Native.parse_binding_document(xml)
89
+ raise Leptris::XML::ParseError,
90
+ "leptris_parse_string failed: " +
91
+ Leptris::XML::FFI.leptris_last_error.to_s if doc.nil?
92
+ return doc.tap { |d| d.readonly! if readonly }
93
+ end
73
94
  raw = Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, nil)
74
95
  if raw.null?
75
96
  raise Leptris::XML::ParseError,
@@ -299,8 +320,7 @@ class Leptris::XML::Document
299
320
  # dispatch (the FFI call plus the c_ptr materialization fed
300
321
  # ~5% of a fresh-doc build).
301
322
  if defined?(Leptris::XML::NATIVE_FAST)
302
- Leptris::XML::FFI.check_status(
303
- Leptris::XML::Native.set_binding_root(self, element.c_address))
323
+ Leptris::XML::Native.set_binding_root(self, element.c_address)
304
324
  else
305
325
  Leptris::XML::FFI.check_status(
306
326
  Leptris::XML::FFI.leptris_document_set_root(c_ptr, element.c_ptr))
@@ -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])
@@ -25,9 +25,8 @@ class Leptris::XML::Element < Leptris::XML::Node
25
25
 
26
26
  def name=(new_name)
27
27
  if @native_fast
28
- Leptris::XML::FFI.check_status(
29
- Leptris::XML::Native.set_binding_name(
30
- @document, @c_address, new_name))
28
+ Leptris::XML::Native.set_binding_name(
29
+ @document, @c_address, new_name)
31
30
  return @name = new_name
32
31
  end
33
32
  ensure_writable!
@@ -54,9 +53,8 @@ class Leptris::XML::Element < Leptris::XML::Node
54
53
 
55
54
  def content=(new_content)
56
55
  if @native_fast
57
- Leptris::XML::FFI.check_status(
58
- Leptris::XML::Native.set_binding_text(
59
- @document, @c_address, new_content.to_s))
56
+ Leptris::XML::Native.set_binding_text(
57
+ @document, @c_address, new_content.to_s)
60
58
  @content = new_content.to_s
61
59
  @content_version = @document.version if @document
62
60
  return new_content
@@ -138,9 +136,8 @@ class Leptris::XML::Element < Leptris::XML::Node
138
136
  # set in one dispatch; the bump drops the version-stamped
139
137
  # attribute memos on both surfaces.
140
138
  if native_fast_children?
141
- Leptris::XML::FFI.check_status(
142
- Leptris::XML::Native.set_binding_attribute(
143
- @document, @c_address, key.to_s, value.to_s))
139
+ Leptris::XML::Native.set_binding_attribute(
140
+ @document, @c_address, key.to_s, value.to_s)
144
141
  seed_attribute_memo(key.to_s, value.to_s)
145
142
  return value
146
143
  end
@@ -374,10 +371,8 @@ class Leptris::XML::Element < Leptris::XML::Node
374
371
  # bump + engine insert in one dispatch; Qnil = the child needs
375
372
  # the namespace lift — the full path below handles it.
376
373
  if native_fast_children?
377
- st = Leptris::XML::Native.insert_binding_child(
378
- @document, @c_address, node.c_ptr.address, 1)
379
- unless st.nil?
380
- Leptris::XML::FFI.check_status(st)
374
+ unless Leptris::XML::Native.insert_binding_child(
375
+ @document, @c_address, node.c_ptr.address, 1).nil?
381
376
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
382
377
  return node
383
378
  end
@@ -397,10 +392,8 @@ class Leptris::XML::Element < Leptris::XML::Node
397
392
  # bump + engine insert in one dispatch; Qnil = the child needs
398
393
  # the namespace lift — the full path below handles it.
399
394
  if native_fast_children?
400
- st = Leptris::XML::Native.insert_binding_child(
401
- @document, @c_address, node.c_ptr.address, 2)
402
- unless st.nil?
403
- Leptris::XML::FFI.check_status(st)
395
+ unless Leptris::XML::Native.insert_binding_child(
396
+ @document, @c_address, node.c_ptr.address, 2).nil?
404
397
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
405
398
  return node
406
399
  end
@@ -420,10 +413,8 @@ class Leptris::XML::Element < Leptris::XML::Node
420
413
  # bump + engine insert in one dispatch; Qnil = the child needs
421
414
  # the namespace lift — the full path below handles it.
422
415
  if native_fast_children?
423
- st = Leptris::XML::Native.insert_binding_child(
424
- @document, @c_address, node.c_ptr.address, 3)
425
- unless st.nil?
426
- Leptris::XML::FFI.check_status(st)
416
+ unless Leptris::XML::Native.insert_binding_child(
417
+ @document, @c_address, node.c_ptr.address, 3).nil?
427
418
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
428
419
  return node
429
420
  end
@@ -539,10 +530,10 @@ class Leptris::XML::Element < Leptris::XML::Node
539
530
  # means the child needs the namespace lift — fall through
540
531
  # to the full path.
541
532
  if native_fast_children?
542
- st = Leptris::XML::Native.append_binding_child(
543
- @document, @c_address, node_or_markup.c_ptr.address)
544
- unless st.nil?
545
- Leptris::XML::FFI.check_status(st)
533
+ # The face raises on failure (TODO.perf/36); Qnil = the
534
+ # child needs the lift — the full path handles it.
535
+ unless Leptris::XML::Native.append_binding_child(
536
+ @document, @c_address, node_or_markup.c_ptr.address).nil?
546
537
  Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
547
538
  return node_or_markup
548
539
  end
@@ -555,6 +546,23 @@ class Leptris::XML::Element < Leptris::XML::Node
555
546
  Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
556
547
  node_or_markup
557
548
  when String
549
+ # TODO.perf/34: one dispatch parses the markup and appends
550
+ # every fragment child (single gate + bump). Negative
551
+ # returns: -1 re-runs the FFI path for the exact parse
552
+ # error; -1000-st routes through check_status.
553
+ if native_fast_children?
554
+ r = Leptris::XML::Native.append_markup(
555
+ @document, @c_address, node_or_markup)
556
+ if r != -1
557
+ # Appends raise in C on status failures (TODO.perf/36);
558
+ # -1 alone means the parse failed.
559
+ return Leptris::XML::NodeSet.new(@document, []) if r.zero?
560
+ return Leptris::XML::NodeSet.new(@document, last_added(r))
561
+ end
562
+ # parse failure: reproduce the exact error via the legacy
563
+ # path (it re-fails fast)
564
+ Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
565
+ end
558
566
  frag = Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
559
567
  added = []
560
568
  frag.children.each do |n|
@@ -567,6 +575,14 @@ class Leptris::XML::Element < Leptris::XML::Node
567
575
  raise ArgumentError, "add_child expects a Node or String, got #{node_or_markup.class}"
568
576
  end
569
577
  end
578
+ # The markup-append face appends in fragment order; the added
579
+ # nodes are this element's last +count+ children.
580
+ def last_added(count)
581
+ kids = children.to_a
582
+ kids.last(count)
583
+ end
584
+ private :last_added
585
+
570
586
  alias_method :<<, :add_child
571
587
 
572
588
  def namespace
@@ -486,8 +486,7 @@ class Leptris::XML::Node
486
486
 
487
487
  def unlink
488
488
  if @native_fast
489
- Leptris::XML::FFI.check_status(
490
- Leptris::XML::Native.unlink_binding_node(@document, @c_address))
489
+ Leptris::XML::Native.unlink_binding_node(@document, @c_address)
491
490
  @parent = nil
492
491
  return self
493
492
  end
@@ -55,6 +55,10 @@ module Leptris::XML::Searchable
55
55
  expr = paths.one? && paths.first.is_a?(String) ? paths.first
56
56
  : paths.join(" | ")
57
57
 
58
+ # TODO.perf/37: the document's Pointer is lazy (materializes
59
+ # once per document lifetime, not per parse); the ffi gem
60
+ # does NOT convert Integers for typedef'd pointer params, so
61
+ # the Pointer object is still what crosses here.
58
62
  doc_ptr = is_a?(Leptris::XML::Document) ? c_ptr : document.c_ptr
59
63
  context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
60
64
 
@@ -100,6 +104,10 @@ module Leptris::XML::Searchable
100
104
  expr = paths.one? && paths.first.is_a?(String) ? paths.first
101
105
  : paths.join(" | ")
102
106
 
107
+ # TODO.perf/37: the document's Pointer is lazy (materializes
108
+ # once per document lifetime, not per parse); the ffi gem
109
+ # does NOT convert Integers for typedef'd pointer params, so
110
+ # the Pointer object is still what crosses here.
103
111
  doc_ptr = is_a?(Leptris::XML::Document) ? c_ptr : document.c_ptr
104
112
  context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
105
113
 
@@ -32,16 +32,16 @@ module Leptris::XML::Serialization
32
32
  # cycle lives at the FFI seam (FFI.serialize_into_string); this
33
33
  # module owns only options selection and construction.
34
34
  def self.to_xml(ffi_function, c_ptr, indent: 0, no_decl: false, encoding: nil)
35
- # TODO.perf/04: the common shape (no encoding) runs the whole
36
- # sized-buffer cycle in the ext — one C dispatch, no options
37
- # marshaling, no Ruby buffer management.
38
- if defined?(Leptris::XML::NATIVE_FAST) && encoding.nil?
35
+ # TODO.perf/04+36: the whole sized-buffer cycle in the ext —
36
+ # one C dispatch, no options marshaling, no Ruby buffer
37
+ # management; encoding rides the face (nil -> no override).
38
+ if defined?(Leptris::XML::NATIVE_FAST)
39
39
  decl = !no_decl
40
40
  return Leptris::XML::Native.fast_element_xml(
41
- c_ptr.address, indent.to_i, decl) if
41
+ c_ptr.address, indent.to_i, decl, encoding) if
42
42
  ffi_function == ELEMENT_SERIALIZE_INTO
43
43
  return Leptris::XML::Native.fast_document_xml(
44
- c_ptr.address, indent.to_i, decl) if
44
+ c_ptr.address, indent.to_i, decl, encoding) if
45
45
  ffi_function == DOCUMENT_SERIALIZE_INTO
46
46
  end
47
47
  opts =
@@ -59,7 +59,8 @@ module Leptris::XML::Serialization
59
59
  # loop: no kwargs, no options rebuild, no method allocation —
60
60
  # one FFI fast-path dispatch per child.
61
61
  def self.element_xml_default(c_ptr)
62
- return Leptris::XML::Native.fast_element_xml(c_ptr.address, 0, true) if
62
+ return Leptris::XML::Native.fast_element_xml(c_ptr.address, 0, true,
63
+ nil) if
63
64
  defined?(Leptris::XML::NATIVE_FAST)
64
65
  Leptris::XML::FFI.serialize_into_string(
65
66
  ELEMENT_SERIALIZE_INTO, c_ptr, DEFAULT_OPTIONS.pointer)
@@ -20,9 +20,8 @@ class Leptris::XML::Text < Leptris::XML::Node
20
20
 
21
21
  def content=(new_content)
22
22
  if @native_fast
23
- Leptris::XML::FFI.check_status(
24
- Leptris::XML::Native.set_binding_text(
25
- @document, @c_address, new_content.to_s))
23
+ Leptris::XML::Native.set_binding_text(
24
+ @document, @c_address, new_content.to_s)
26
25
  return new_content
27
26
  end
28
27
  ensure_writable!
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.174.8
4
+ version: 1.9.174.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -102,6 +102,10 @@ files:
102
102
  - TODO.perf/31-battery-rounds-4-8.md
103
103
  - TODO.perf/32-clean-host-floor-table.md
104
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
107
+ - TODO.perf/36-raise-in-c-serializer-encoding.md
108
+ - TODO.perf/37-document-lazy-pointer.md
105
109
  - TODO.restructure/01-constraint-compliance-audit.md
106
110
  - TODO.restructure/02-deep-copy-seam.md
107
111
  - TODO.restructure/03-evaluation-context-seam.md