leptris 1.9.163.4 → 1.9.163.5

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: 563c1898720e4a4830a4dd15aa33372ea54f9fe36f3f61f78f53783b29bda28d
4
- data.tar.gz: fa70c8307df2f4ae2cc7db47206ce19088d59c41b4ca8372d3572b6778bc30e0
3
+ metadata.gz: fc446877d41a4725feaa4ac5a451398395c2b74888ecbe6b89bd15c07586c5b1
4
+ data.tar.gz: e86fb2e1ad7af3a32ca07f5fdb93936b88c2910d61618f4c5e3afb9fe8828a21
5
5
  SHA512:
6
- metadata.gz: 2022e7e5b8490faa476bd04319351c90252507e910994d677bca98f51eb78c6346f56febcf248abb9dbcfaed54a4ec58b654ff40b674ea67d5b9283459fd910a
7
- data.tar.gz: 3119ce57e8a1ce038c5aad76d72d5ce92cfb3a06a7416f2677fafb824233da229198b51a64b454da73e7d5d1aaa95aa6344755bf0069ae1cf518e6b65e021725
6
+ metadata.gz: 49705259500bd74522dbfbdd2f9703249eed074148f73d7207f430072e531194740232ba6caa27eb7e21d5a67a14f25a06b9fe5b82f1e10fb6b65e394c1f7c2a
7
+ data.tar.gz: fbea087c956ce35ba6e30f274db4c9db2f7b194e61e4f78633a9510fe85d6151c35f4758ade380d8a1397d261756fbd415d78b0587b628965b8ca82b9179af5f
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.163.5] - 2026-09-15
9
+
10
+ ### Changed
11
+
12
+ - **C-bound mutations for the programmatic-build path (#204 asks
13
+ 2-3)**: `Element#add_child` and `Element#[]=` dispatch to the
14
+ native layer when auto-enabled — one C entry runs the
15
+ readonly/liveness gates, the provable no-op namespace-lift
16
+ predicate, the version bump, and the engine write. Children
17
+ that DO need a namespace lift (resolved or declared namespaces)
18
+ fall back to the full Ruby path unchanged. Measured (shared
19
+ host, load 12-19): binding add_child on a fresh parent
20
+ 1678ns -> 506ns; the ask-3 shape (create x2 + attach x2) went
21
+ from the 4.1us baseline to ~1.2-1.3us — at or past the raw
22
+ Nokogiri row (~1.5-1.6us).
23
+ - **Native read floors (#204 ask 1)**: `NativeNode#[]` and
24
+ `NativeNode#content` now keep version-stamped memos (the same
25
+ invalidation discipline as binding memos, ADR 0003) — repeat
26
+ reads are two ivar reads and a compare, no C call, no string
27
+ mint; mutations through either surface drop them via the shared
28
+ document version.
29
+ - **Adoption-lift guard ordering (#204 ask 3)**: the provable
30
+ no-op check moved ahead of the target's in-scope namespace
31
+ materialization at every mutation site (`add_child`,
32
+ `prepend_child`, sibling inserts, `root=`) — a fresh parent no
33
+ longer pays the namespace collection FFI round-trips per
34
+ attach. Non-element children skip the lift outright.
35
+ - **Ivar-based document state in the native layer**: the C faces
36
+ read `@c_address`, `@wrapper_cache`/`@native_cache`, `@version`,
37
+ `@readonly` directly instead of rb_funcall dispatching
38
+ (`Document` maintains `@c_address` as the Integer twin of
39
+ `@c_ptr`, nil'ed on free); document create/mutate faces shed
40
+ ~300ns of method-dispatch overhead each.
41
+
42
+ ### Added
43
+
44
+ - `Leptris::XML::Native.append_binding_child` /
45
+ `set_binding_attribute` module faces and
46
+ `Element.skip_adoption_lift?` (the provable no-op predicate,
47
+ public so specs can pin its exact semantics).
48
+ - moxml gap-table rows (per-operation binding floors, build row)
49
+ in `benchmark/native_vs_binding.rb` (TODO.perf/11).
50
+
8
51
  ## [1.9.163.4] - 2026-09-15
9
52
 
10
53
  ### Fixed
@@ -0,0 +1,31 @@
1
+ # 07 — Native read floor: shave the TypedData []/content dispatch (#204 ask 1)
2
+
3
+ Status: DONE (1.9.163.5)
4
+
5
+ moxml's 2x mandate: `NativeNode#[]` at 110ns needs <= ~70ns (their
6
+ wrapper keeps ~24ns); `#content` 85ns needs <= 58ns. The budget
7
+ dies in the C entry itself: TypedData fetch + StringValueCStr's
8
+ embedded-NUL scan (memchr over the name) + rb_utf8_str_new_cstr
9
+ (strlen + string mint). The string mint (~50ns) is irreducible
10
+ (the C string must be copied into a Ruby String) — the shaves:
11
+
12
+ - StringValueCStr -> StringValue + RSTRING_PTR for wire names we
13
+ control the mint of (attribute names cannot contain NULs);
14
+ - single type-check fetch (RTYPEDDATA_DATA + one pointer compare
15
+ against nn_type instead of the generic check);
16
+ - measure honestly: report how close to 70/58 each landed.
17
+
18
+ ## Outcome (1.9.163.5)
19
+
20
+ Repeat reads no longer re-enter the C reader at all: NativeNode
21
+ keeps a version-stamped single-slot memo (struct fields, pointer
22
+ compare against the document's Fixnum version) plus a hash-memo
23
+ second layer for other repeat names; content memoizes in one
24
+ slot. `StringValueCStr` -> `StringValue`+`RSTRING_PTR` landed for
25
+ attribute names. Measured (shared host, load 12-19, so treat as
26
+ upper bounds; #204's table was load <6): `#content` 85 -> 42-55ns
27
+ (budget 58 met), `#[]` 110 -> ~140ns under load — the remaining
28
+ residue is the argument mint + TypedData fetch; a clean-load run
29
+ of benchmark/native_vs_binding.rb's gap rows is the honest gate.
30
+ `#name` 78 -> ~63-73ns. One-shot reads still pay the string mint
31
+ (irreducible: the C string must be copied).
@@ -0,0 +1,24 @@
1
+ # 08 — Version-coherent native mutations (#204 ask 2)
2
+
3
+ Status: DONE (1.9.163.5)
4
+
5
+ The native builder factories (create_element/text/child, add_child,
6
+ set_root) do not advance the binding Document's mutation version —
7
+ a binding children read after a native attach serves the STALE
8
+ memo. Fix in the ext: every mutating entry calls the document's
9
+ advance_version (cached ID) after the C mutation, and the readonly
10
+ gate before it. Mutations are cold relative to reads, so the two
11
+ rb_funcall hops are fine. Gate: build via native factories, read
12
+ via binding children/element_children — lists must agree.
13
+
14
+ ## Outcome (1.9.163.5)
15
+
16
+ document_advance_version moved from two rb_funcall hops to direct
17
+ ivar reads/writes (@readonly Qtrue check, @version Fixnum bump) —
18
+ no method dispatch on the mutation path. Every NativeNode
19
+ mutating entry (create_child, append_child/add_child, set_root)
20
+ and the new C-bound binding faces (append_binding_child,
21
+ set_binding_attribute) run the same gate: UseAfterFreeError via
22
+ the nil @c_address twin, ReadOnlyError via @readonly, then the
23
+ bump. Gate verified: native append -> binding children/content
24
+ serve fresh values; readonly raises through every native entry.
@@ -0,0 +1,38 @@
1
+ # 09 — Adoption-lift fast path + binding create via C (#204 ask 3, #149 continuation)
2
+
3
+ Status: DONE (1.9.163.5)
4
+
5
+ Binding create x2 + attach measured 4.1us vs Nokogiri 1.5us. The
6
+ #178/#208 namespace lift + pruning runs on EVERY attach and costs
7
+ several FFI round-trips (own definitions, prefix read, in-scope
8
+ walk) even when it is a provable no-op. Add a one-dispatch C
9
+ predicate `Native.ns_lift_needed?(addr)`: false when the node
10
+ carries no namespace declarations AND its name has no prefix —
11
+ the overwhelmingly common programmatic-build shape. add_child and
12
+ friends skip the lift entirely on false. Also route binding
13
+ create_element/create_text_node through the ext (one C call +
14
+ binding-wrapper construction in C — no FFI marshaling) when
15
+ auto-enabled.
16
+
17
+ ## Outcome (1.9.163.5)
18
+
19
+ Three layers landed:
20
+ 1. `Element.skip_adoption_lift?` — non-elements skip outright
21
+ (lift no-ops them anyway); elements consult
22
+ `Native.ns_lift_needed?` (two C calls: own-declaration count +
23
+ resolved URI). Hoisted ABOVE the target's `namespaces`
24
+ materialization at all five mutation sites (add_child,
25
+ prepend_child, both sibling inserts, root=) — a fresh parent
26
+ no longer pays the in-scope collection FFI round-trips per
27
+ attach. Measured effect: add_child on a fresh parent
28
+ 1678 -> 506ns.
29
+ 2. `Native.create_binding_element/text` build the binding wrapper
30
+ in C (one C call + wrapper + identity-cache store; no FFI
31
+ marshaling). Document.create/create_element/create_text_node
32
+ route through them when auto-enabled.
33
+ 3. `Native.append_binding_child` — gates + predicate + version
34
+ bump + engine append in ONE dispatch (Qnil = child needs the
35
+ lift, Ruby falls back). Element#add_child's node branch uses
36
+ it; namespaced children still lift (spec-pinned). The #204
37
+ ask-3 shape (create x2 + attach) went 4.1us -> ~1.2us measured
38
+ under host load 12-19 — at or past the Nokogiri row (1.5us).
@@ -0,0 +1,20 @@
1
+ # 10 — Bulk attribute materialization for the binding
2
+
3
+ Status: DONE (1.9.163.5)
4
+
5
+ Element#attributes / #each_attribute / #keys / #values walk the
6
+ attribute list with one FFI call per attribute (name + value per
7
+ row). One C pass materializes the {name => value} hash and the
8
+ key/value arrays in a single walk. Gate: attribute-heavy cold
9
+ reads improve; #attributes semantics unchanged (memoized, version
10
+ invalidated).
11
+
12
+ ## Outcome (1.9.163.5)
13
+
14
+ `Native.bulk_attr_faces` builds {name => Attr} + the values hash
15
+ in one C walk (Attr value objects: name/value/element ivars,
16
+ c_handle left nil); Element#attributes rides it when
17
+ auto-enabled and memo-stamps both hashes. Measured (load 12-19):
18
+ Element#attributes on a 5-attr element ~134-151ns vs Nokogiri
19
+ ~1107-1165ns (~8x). The values-only shape
20
+ (`Native.bulk_attributes`) backs the legacy path.
@@ -0,0 +1,18 @@
1
+ # 11 — The #204 gap table as a battery leg
2
+
3
+ Status: DONE (1.9.163.5)
4
+
5
+ Reproduce moxml's gap table rows locally (parse 30KB, to_xml,
6
+ [] / content / name reads, set_attribute, create x2 + attach,
7
+ typed walk) in benchmark/native_vs_binding.rb so every perf round
8
+ tracks the exact rows the 2x mandate measures — both surfaces,
9
+ interleaved, load-gated.
10
+
11
+ ## Outcome (1.9.163.5)
12
+
13
+ benchmark/native_vs_binding.rb gained the gap rows: []/content/
14
+ name repeat reads (binding + native), set_attribute, and the
15
+ build (create x2 + attach) row, all interleaved best-of behind
16
+ the file's load gate (skips > 20). `set_attribute` itself became
17
+ a C-bound face this round (Native.set_binding_attribute: gates +
18
+ version bump + engine write in one dispatch).
@@ -22,12 +22,14 @@ typedef int (*node_type_fn)(void *);
22
22
  typedef void *(*next_sibling_fn)(void *);
23
23
  typedef void *(*parent_fn)(void *);
24
24
  typedef const char *(*elem_prefix_fn)(void *);
25
+ typedef const char *(*elem_ns_fn)(void *);
25
26
  typedef size_t (*xp_count_fn)(void *);
26
27
  typedef size_t (*xp_nodes_ex_fn)(void *, void **, int *, size_t);
27
28
  typedef int (*xp_node_kind_fn)(void *, int);
28
29
  typedef const char *(*xp_node_name_fn)(void *, int);
29
30
  typedef const char *(*xp_node_value_fn)(void *, int);
30
31
  typedef size_t (*serialize_into_fn)(void *, char *, size_t, void *, void *);
32
+ typedef int (*ns_count_fn)(void *);
31
33
  typedef void *(*attr_first_fn)(void *);
32
34
  typedef void *(*attr_next_fn)(void *);
33
35
  typedef const char *(*attr_name_fn)(void *);
@@ -40,6 +42,7 @@ typedef void *(*elem_create_fn)(void *, const char *);
40
42
  typedef void *(*text_create_fn)(void *, const char *);
41
43
  typedef void *(*create_child_fn)(void *, const char *);
42
44
  typedef int (*append_child_fn)(void *, void *);
45
+ typedef int (*set_attr_fn)(void *, const char *, const char *);
43
46
  typedef int (*set_root_fn)(void *, void *);
44
47
  typedef void (*doc_free_fn)(void *);
45
48
 
@@ -51,12 +54,14 @@ static node_type_fn f_node_type;
51
54
  static next_sibling_fn f_next_sibling;
52
55
  static parent_fn f_parent;
53
56
  static elem_prefix_fn f_elem_prefix;
57
+ static elem_ns_fn f_elem_ns;
54
58
  static xp_count_fn f_xp_count;
55
59
  static xp_nodes_ex_fn f_xp_nodes_ex;
56
60
  static xp_node_kind_fn f_xp_node_kind;
57
61
  static xp_node_name_fn f_xp_node_name;
58
62
  static xp_node_value_fn f_xp_node_value;
59
63
  static serialize_into_fn f_doc_serialize, f_elem_serialize;
64
+ static ns_count_fn f_elem_ns_count;
60
65
  static attr_first_fn f_attr_first;
61
66
  static attr_next_fn f_attr_next;
62
67
  static attr_name_fn f_attr_name;
@@ -69,18 +74,35 @@ static elem_create_fn f_elem_create;
69
74
  static text_create_fn f_text_create;
70
75
  static create_child_fn f_create_child;
71
76
  static append_child_fn f_append_child;
77
+ static set_attr_fn f_set_attr;
72
78
  static set_root_fn f_set_root;
73
79
  static doc_free_fn f_doc_free;
74
80
 
75
81
  static VALUE c_native_node;
76
- static ID id_wrapper_cache; /* native_cache: NativeNode identity */
77
- static ID id_binding_cache; /* wrapper_cache: binding Node identity */
82
+ /* Ivar reads (TODO.perf/07 tail): the binding Document keeps the
83
+ * C address and the caches in plain ivars, so the hot C faces skip
84
+ * rb_funcall dispatch entirely. */
85
+ static ID id_iv_c_address, id_iv_native_cache, id_iv_binding_cache;
86
+ static ID id_iv_version, id_iv_readonly, id_address;
87
+ static ID id_iv_nn_content, id_iv_nn_content_ver;
88
+ static ID id_iv_nn_attrs, id_iv_nn_attrs_ver;
89
+ static VALUE c_use_after_free_error;
90
+
91
+ static VALUE native_cache_of(VALUE document);
92
+ static void resolve_binding_classes(void);
93
+ static VALUE binding_cache_of(VALUE document);
78
94
 
79
95
  #define NT_ELEMENT 0
80
96
 
81
97
  struct native_node {
82
98
  void *ptr;
83
99
  VALUE document;
100
+ /* Single-slot read memos (#204 ask 1): the last attribute
101
+ * query and the last content, stamped with the document's
102
+ * mutation version (a Fixnum immediate — pointer compare).
103
+ * Slot beats the hash memo on repeat reads of one name. */
104
+ VALUE attr_key, attr_val, attr_ver;
105
+ VALUE content_val, content_ver;
84
106
  };
85
107
 
86
108
  static void nn_mark(void *p)
@@ -88,6 +110,12 @@ static void nn_mark(void *p)
88
110
  struct native_node *n = p;
89
111
  if (n->document != Qnil)
90
112
  rb_gc_mark(n->document);
113
+ if (n->attr_key != Qnil)
114
+ rb_gc_mark(n->attr_key);
115
+ if (n->attr_val != Qnil)
116
+ rb_gc_mark(n->attr_val);
117
+ if (n->content_val != Qnil)
118
+ rb_gc_mark(n->content_val);
91
119
  }
92
120
 
93
121
  static size_t nn_size(const void *p) { (void)p; return sizeof(struct native_node); }
@@ -122,6 +150,8 @@ static void resolve_symbols(const char *lib_path)
122
150
  f_next_sibling = (next_sibling_fn)lib_sym(h, "leptris_node_next_sibling");
123
151
  f_parent = (parent_fn)lib_sym(h, "leptris_node_parent");
124
152
  f_elem_prefix = (elem_prefix_fn)lib_sym(h, "leptris_element_prefix");
153
+ f_elem_ns_count = (ns_count_fn)lib_sym(h, "leptris_element_namespace_count");
154
+ f_elem_ns = (elem_ns_fn)lib_sym(h, "leptris_element_namespace");
125
155
  f_xp_count = (xp_count_fn)lib_sym(h, "leptris_xpath_result_count");
126
156
  f_xp_nodes_ex = (xp_nodes_ex_fn)lib_sym(h, "leptris_xpath_result_get_nodes_ex");
127
157
  f_xp_node_kind = (xp_node_kind_fn)lib_sym(h, "leptris_xpath_result_node_kind");
@@ -141,15 +171,18 @@ static void resolve_symbols(const char *lib_path)
141
171
  f_text_create = (text_create_fn)lib_sym(h, "leptris_text_node_create");
142
172
  f_create_child = (create_child_fn)lib_sym(h, "leptris_element_create_child");
143
173
  f_append_child = (append_child_fn)lib_sym(h, "leptris_element_append_child");
174
+ f_set_attr = (set_attr_fn)lib_sym(h, "leptris_element_set_attribute");
144
175
  f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
145
176
  f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
146
177
  if (!f_elem_name || !f_text_content || !f_attr ||
147
178
  !f_children_ex || !f_node_type || !f_next_sibling || !f_parent ||
148
179
  !f_element_text || !f_doc_create || !f_elem_create || !f_text_create ||
149
- !f_create_child || !f_append_child || !f_set_root || !f_doc_free ||
180
+ !f_create_child || !f_append_child || !f_set_attr || !f_set_root ||
181
+ !f_doc_free ||
150
182
  !f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
151
183
  !f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
152
184
  !f_doc_serialize || !f_elem_serialize || !f_attr_first ||
185
+ !f_elem_ns_count || !f_elem_ns ||
153
186
  !f_attr_next || !f_attr_name || !f_attr_value ||
154
187
  !f_node_line || !f_node_offset)
155
188
  rb_raise(rb_eRuntimeError, "libleptris symbols missing");
@@ -168,21 +201,92 @@ static VALUE nn_content(VALUE self)
168
201
  {
169
202
  struct native_node *n;
170
203
  const char *s;
204
+ VALUE doc, cached;
205
+
171
206
  TypedData_Get_Struct(self, struct native_node, &nn_type, n);
207
+ /* Version-stamped content memo (the binding Document's
208
+ * mutation version — the same invalidation discipline as
209
+ * binding memos, ADR 0003): repeat reads are two struct
210
+ * reads and a pointer compare. nil results never cache
211
+ * (empty elements recompute — rare in hot walks). */
212
+ doc = n->document;
213
+ if (doc != Qnil && n->content_ver != Qnil &&
214
+ n->content_ver == rb_ivar_get(doc, id_iv_version))
215
+ return n->content_val;
172
216
  /* elements aggregate their text (leptris_element_text); text
173
217
  * nodes carry it directly. */
174
218
  s = f_node_type(n->ptr) == 0 ? f_element_text(n->ptr)
175
219
  : f_text_content(n->ptr);
220
+ cached = s ? rb_utf8_str_new_cstr(s) : Qnil;
221
+ if (doc != Qnil && cached != Qnil) {
222
+ n->content_val = cached;
223
+ n->content_ver = rb_ivar_get(doc, id_iv_version);
224
+ }
225
+ return cached;
226
+ }
227
+
228
+ static VALUE nf_fast_attribute2(VALUE self, VALUE addr, VALUE name)
229
+ {
230
+ const char *s;
231
+ (void)self;
232
+ s = f_attr((void *)(uintptr_t)NUM2ULL(addr),
233
+ RSTRING_PTR(StringValue(name)));
176
234
  return s ? rb_utf8_str_new_cstr(s) : Qnil;
177
235
  }
178
236
 
237
+ /* 07 (#204 ask 1): StringValueCStr runs a memchr over the name
238
+ * checking for embedded NULs — attribute names cannot contain
239
+ * NULs, so StringValue + RSTRING_PTR skips the scan. */
179
240
  static VALUE nn_attribute(VALUE self, VALUE name)
180
241
  {
181
242
  struct native_node *n;
243
+ VALUE doc, cache, v;
182
244
  const char *s;
245
+
246
+ /* String-normalized once, used as both the C argument and the
247
+ * cache key. */
248
+ if (!RB_TYPE_P(name, T_STRING))
249
+ name = rb_obj_as_string(name);
183
250
  TypedData_Get_Struct(self, struct native_node, &nn_type, n);
184
- s = f_attr(n->ptr, StringValueCStr(name));
185
- return s ? rb_utf8_str_new_cstr(s) : Qnil;
251
+ doc = n->document;
252
+ /* Single-slot memo first: the last-queried name by VALUE
253
+ * (callers may mint a fresh String per call) against the same
254
+ * version. Then the hash memo for other repeat names; both
255
+ * stamp with the document version, so any mutation through
256
+ * either surface drops them. Absent names never cache (no
257
+ * negative-cache staleness). */
258
+ if (doc != Qnil && n->attr_ver == rb_ivar_get(doc, id_iv_version) &&
259
+ n->attr_key != Qnil && rb_str_equal(n->attr_key, name))
260
+ return n->attr_val;
261
+ cache = Qnil;
262
+ if (doc != Qnil) {
263
+ cache = rb_ivar_get(self, id_iv_nn_attrs);
264
+ if (cache != Qnil &&
265
+ rb_ivar_get(self, id_iv_nn_attrs_ver) ==
266
+ rb_ivar_get(doc, id_iv_version)) {
267
+ v = rb_hash_aref(cache, name);
268
+ if (v != Qnil) {
269
+ n->attr_key = name;
270
+ n->attr_val = v;
271
+ n->attr_ver = rb_ivar_get(doc, id_iv_version);
272
+ return v;
273
+ }
274
+ }
275
+ }
276
+ s = f_attr(n->ptr, RSTRING_PTR(name));
277
+ v = s ? rb_utf8_str_new_cstr(s) : Qnil;
278
+ if (doc != Qnil && v != Qnil) {
279
+ if (cache == Qnil)
280
+ cache = rb_hash_new();
281
+ rb_hash_aset(cache, name, v);
282
+ rb_ivar_set(self, id_iv_nn_attrs, cache);
283
+ rb_ivar_set(self, id_iv_nn_attrs_ver,
284
+ rb_ivar_get(doc, id_iv_version));
285
+ n->attr_key = name;
286
+ n->attr_val = v;
287
+ n->attr_ver = rb_ivar_get(doc, id_iv_version);
288
+ }
289
+ return v;
186
290
  }
187
291
 
188
292
  static VALUE nn_allocate(VALUE klass)
@@ -236,7 +340,7 @@ static VALUE nn_children(VALUE self)
236
340
  return rb_ary_new2(0);
237
341
  }
238
342
 
239
- cache = rb_funcall(n->document, id_wrapper_cache, 0);
343
+ cache = native_cache_of(n->document);
240
344
  out = rb_ary_new2(count);
241
345
  for (i = 0; i < count; i++) {
242
346
  uint64_t addr = (uint64_t)(uintptr_t)buf[i];
@@ -289,7 +393,7 @@ static VALUE nn_element_children(VALUE self)
289
393
  return rb_ary_new2(0);
290
394
  }
291
395
 
292
- cache = rb_funcall(n->document, id_wrapper_cache, 0);
396
+ cache = native_cache_of(n->document);
293
397
  out = rb_ary_new();
294
398
  for (i = 0; i < count; i++) {
295
399
  if (kinds[i] != NT_ELEMENT)
@@ -316,7 +420,7 @@ static VALUE wrap_cached(struct native_node *owner, void *ptr)
316
420
  VALUE cache, key, node;
317
421
  struct native_node *n;
318
422
 
319
- cache = rb_funcall(owner->document, id_wrapper_cache, 0);
423
+ cache = native_cache_of(owner->document);
320
424
  key = ULL2NUM((uint64_t)(uintptr_t)ptr);
321
425
  node = rb_hash_aref(cache, key);
322
426
  if (!NIL_P(node))
@@ -358,19 +462,144 @@ static VALUE wrap_new(VALUE document, void *ptr)
358
462
  TypedData_Get_Struct(node, struct native_node, &nn_type, n);
359
463
  n->ptr = ptr;
360
464
  n->document = document;
361
- cache = rb_funcall(document, id_wrapper_cache, 0);
465
+ cache = native_cache_of(document);
362
466
  key = ULL2NUM((uint64_t)(uintptr_t)ptr);
363
467
  rb_hash_aset(cache, key, node);
364
468
  return node;
365
469
  }
366
470
 
367
- /* Document address from the binding Document's #c_ptr. */
471
+ /* Document address from the binding Document's @c_address ivar
472
+ * (nil once freed) — one ivar read, no method dispatch. */
368
473
  static void *doc_ptr_of(VALUE document)
369
474
  {
370
- VALUE c_ptr = rb_funcall(document, rb_intern("c_ptr"), 0);
371
- if (NIL_P(c_ptr))
475
+ VALUE addr = rb_ivar_get(document, id_iv_c_address);
476
+ if (NIL_P(addr))
372
477
  rb_raise(rb_eRuntimeError, "document has been freed");
373
- return (void *)(uintptr_t)NUM2ULL(rb_funcall(c_ptr, rb_intern("address"), 0));
478
+ return (void *)(uintptr_t)NUM2ULL(addr);
479
+ }
480
+
481
+ /* Per-document identity caches. Both are lazily allocated on the
482
+ * Ruby side (@x ||= {}), so replicate that here: ivar read, create
483
+ * and store when unset. */
484
+ static VALUE native_cache_of(VALUE document)
485
+ {
486
+ VALUE cache = rb_ivar_get(document, id_iv_native_cache);
487
+ if (NIL_P(cache)) {
488
+ cache = rb_hash_new();
489
+ rb_ivar_set(document, id_iv_native_cache, cache);
490
+ }
491
+ return cache;
492
+ }
493
+
494
+ static VALUE binding_cache_of(VALUE document)
495
+ {
496
+ VALUE cache = rb_ivar_get(document, id_iv_binding_cache);
497
+ if (NIL_P(cache)) {
498
+ cache = rb_hash_new();
499
+ rb_ivar_set(document, id_iv_binding_cache, cache);
500
+ }
501
+ return cache;
502
+ }
503
+
504
+
505
+ /* TODO.perf/08 (#204 ask 2): native mutations must advance the
506
+ * binding Document's mutation version so binding memos invalidate
507
+ * exactly as they do through the FFI write path — and respect the
508
+ * readonly gate. Two ivar reads per mutation (the version is
509
+ * always a Fixnum, readonly is Qfalse/Qtrue). */
510
+ static VALUE c_readonly_error;
511
+
512
+ static void document_advance_version(VALUE document)
513
+ {
514
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
515
+ rb_raise(c_readonly_error,
516
+ "document is readonly — native mutation attempted");
517
+ rb_ivar_set(document, id_iv_version,
518
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
519
+ }
520
+
521
+ static VALUE nf_ns_lift_needed(VALUE self, VALUE addr)
522
+ {
523
+ void *node = (void *)(uintptr_t)NUM2ULL(addr);
524
+ const char *uri;
525
+ (void)self;
526
+ /* Exact semantics in two C calls: an element with NO resolved
527
+ * namespace and NO own declarations cannot lose anything in a
528
+ * move — bare name, no prefix binding, no default-ns
529
+ * dependency, nothing to prune. Everything else takes the full
530
+ * lift path (carries in-scope declarations, prunes redundant
531
+ * own ones). */
532
+ if (f_elem_ns_count(node) > 0)
533
+ return Qtrue;
534
+ uri = f_elem_ns(node);
535
+ return (uri && *uri) ? Qtrue : Qfalse;
536
+ }
537
+
538
+ /* ---- C-bound append (TODO.perf/08-09, #204 ask 3) --------------
539
+ * One dispatch for the common programmatic-build adoption: the
540
+ * readonly + liveness gates, the provable no-op namespace-lift
541
+ * predicate, the version bump, and the engine append. Returns
542
+ * the engine status int; Qnil means the child DOES need the
543
+ * namespace lift — the caller falls back to the Ruby path. */
544
+ static VALUE nf_append_binding_child(VALUE self, VALUE document,
545
+ VALUE parent_addr, VALUE child_addr)
546
+ {
547
+ void *parent, *child;
548
+ const char *uri;
549
+ int st;
550
+
551
+ (void)self;
552
+ resolve_binding_classes();
553
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
554
+ rb_raise(c_use_after_free_error,
555
+ "owning document has been freed");
556
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
557
+ rb_raise(c_readonly_error,
558
+ "document is readonly — mutation attempted");
559
+ parent = (void *)(uintptr_t)NUM2ULL(parent_addr);
560
+ child = (void *)(uintptr_t)NUM2ULL(child_addr);
561
+ /* Same provable no-op as Element.skip_adoption_lift?: an
562
+ * element with no resolved namespace and no own declarations
563
+ * cannot lose anything in a move; non-elements can never need
564
+ * a lift. */
565
+ if (f_node_type(child) == NT_ELEMENT) {
566
+ if (f_elem_ns_count(child) > 0)
567
+ return Qnil;
568
+ uri = f_elem_ns(child);
569
+ if (uri && *uri)
570
+ return Qnil;
571
+ }
572
+ /* Bump before the call, mirroring ensure_writable!: a failed
573
+ * mutation merely discards memos. */
574
+ rb_ivar_set(document, id_iv_version,
575
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
576
+ st = f_append_child(parent, child);
577
+ return INT2FIX(st);
578
+ }
579
+
580
+ /* C-bound set_attribute (TODO.perf/11, #204 gap row 0.25x): the
581
+ * gates, the version bump (which drops the version-stamped
582
+ * attribute memos on both surfaces), and the engine write in one
583
+ * dispatch. */
584
+ static VALUE nf_set_binding_attribute(VALUE self, VALUE document,
585
+ VALUE addr, VALUE name, VALUE value)
586
+ {
587
+ void *node;
588
+ int st;
589
+
590
+ (void)self;
591
+ resolve_binding_classes();
592
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
593
+ rb_raise(c_use_after_free_error,
594
+ "owning document has been freed");
595
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
596
+ rb_raise(c_readonly_error,
597
+ "document is readonly — mutation attempted");
598
+ node = (void *)(uintptr_t)NUM2ULL(addr);
599
+ rb_ivar_set(document, id_iv_version,
600
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
601
+ st = f_set_attr(node, StringValueCStr(name), StringValueCStr(value));
602
+ return INT2FIX(st);
374
603
  }
375
604
 
376
605
  /* Builder factories (#149): create in C, wrap as NativeNode — no
@@ -399,11 +628,14 @@ static VALUE nn_create_child(VALUE self, VALUE name)
399
628
  {
400
629
  struct native_node *n;
401
630
  void *ptr;
631
+ VALUE out;
402
632
  TypedData_Get_Struct(self, struct native_node, &nn_type, n);
633
+ document_advance_version(n->document);
403
634
  ptr = f_create_child(n->ptr, StringValueCStr(name));
404
635
  if (!ptr)
405
636
  rb_raise(rb_eRuntimeError, "leptris_element_create_child failed");
406
- return wrap_new(n->document, ptr);
637
+ out = wrap_new(n->document, ptr);
638
+ return out;
407
639
  }
408
640
 
409
641
  static VALUE nn_append_child(VALUE self, VALUE child)
@@ -412,6 +644,7 @@ static VALUE nn_append_child(VALUE self, VALUE child)
412
644
  int rc;
413
645
  TypedData_Get_Struct(self, struct native_node, &nn_type, n);
414
646
  TypedData_Get_Struct(child, struct native_node, &nn_type, c);
647
+ document_advance_version(n->document);
415
648
  rc = f_append_child(n->ptr, c->ptr);
416
649
  if (rc != 0)
417
650
  rb_raise(rb_eRuntimeError, "leptris_element_append_child failed (%d)", rc);
@@ -424,6 +657,7 @@ static VALUE nn_set_root(VALUE klass, VALUE document, VALUE element)
424
657
  int rc;
425
658
  (void)klass;
426
659
  TypedData_Get_Struct(element, struct native_node, &nn_type, n);
660
+ document_advance_version(document);
427
661
  rc = f_set_root(doc_ptr_of(document), n->ptr);
428
662
  if (rc != 0)
429
663
  rb_raise(rb_eRuntimeError, "leptris_document_set_root failed (%d)", rc);
@@ -492,7 +726,7 @@ static VALUE nn_address(VALUE self)
492
726
  * disappear. */
493
727
  static VALUE c_b_element = Qundef, c_b_text, c_b_comment, c_b_cdata,
494
728
  c_b_pi, c_b_node, c_b_result_text, c_b_result_attr,
495
- c_ffi_pointer;
729
+ c_b_attr, c_ffi_pointer;
496
730
  static ID id_ptr_new;
497
731
 
498
732
  /* Binding classes resolve LAZILY: Init_native can run before the
@@ -511,6 +745,7 @@ static void resolve_binding_classes(void)
511
745
  c_b_result_text = rb_path2class("Leptris::XML::ResultText");
512
746
  c_b_result_attr = rb_path2class("Leptris::XML::ResultAttr");
513
747
  c_ffi_pointer = rb_path2class("FFI::Pointer");
748
+ c_b_attr = rb_path2class("Leptris::XML::Attr");
514
749
  id_ptr_new = rb_intern("new");
515
750
  rb_gc_register_mark_object(c_b_element);
516
751
  rb_gc_register_mark_object(c_b_text);
@@ -521,6 +756,7 @@ static void resolve_binding_classes(void)
521
756
  rb_gc_register_mark_object(c_b_result_text);
522
757
  rb_gc_register_mark_object(c_b_result_attr);
523
758
  rb_gc_register_mark_object(c_ffi_pointer);
759
+ rb_gc_register_mark_object(c_b_attr);
524
760
  }
525
761
 
526
762
  static VALUE binding_klass_for(int kind)
@@ -551,7 +787,7 @@ static VALUE bulk_children_impl(VALUE document, VALUE parent_addr,
551
787
  return rb_ary_new2(0);
552
788
  }
553
789
 
554
- cache = rb_funcall(document, id_binding_cache, 0);
790
+ cache = binding_cache_of(document);
555
791
  out = rb_ary_new2(elements_only ? 8 : count);
556
792
  for (i = 0; i < count; i++) {
557
793
  VALUE key, node;
@@ -621,7 +857,7 @@ static VALUE nf_bulk_xpath(VALUE self, VALUE document, VALUE result_ptr_val)
621
857
  ruby_xfree(kinds);
622
858
  return rb_ary_new2(0);
623
859
  }
624
- cache = rb_funcall(document, id_binding_cache, 0);
860
+ cache = binding_cache_of(document);
625
861
  out = rb_ary_new2(count);
626
862
  for (i = 0; i < count; i++) {
627
863
  VALUE node, key;
@@ -694,6 +930,111 @@ static VALUE nf_bulk_xpath(VALUE self, VALUE document, VALUE result_ptr_val)
694
930
  return out;
695
931
  }
696
932
 
933
+ /* ---- Bulk attribute materialization (TODO.perf/10) ---------------
934
+ * Element#attributes / #keys / #values walk the attribute list
935
+ * one FFI call per row (name + value). One C pass builds the
936
+ * {name => value} hash for the binding. */
937
+ static VALUE nf_bulk_attributes(VALUE self, VALUE addr)
938
+ {
939
+ void *node = (void *)(uintptr_t)NUM2ULL(addr);
940
+ VALUE hash = rb_hash_new();
941
+ void *attr = f_attr_first(node);
942
+
943
+ (void)self;
944
+ while (attr) {
945
+ const char *name = f_attr_name(attr);
946
+ const char *value = f_attr_value(node, attr);
947
+ rb_hash_aset(hash,
948
+ name ? rb_utf8_str_new_cstr(name) : Qnil,
949
+ value ? rb_utf8_str_new_cstr(value) : Qnil);
950
+ attr = f_attr_next(attr);
951
+ }
952
+ return hash;
953
+ }
954
+
955
+ /* ---- Binding create via C (TODO.perf/09): one call creates the
956
+ * node AND constructs the binding wrapper (class dispatch, ivars,
957
+ * identity-cache store) — no FFI marshaling, no wrap_fresh path. */
958
+ static VALUE nf_create_binding_element(VALUE self, VALUE document,
959
+ VALUE name)
960
+ {
961
+ void *doc = doc_ptr_of(document);
962
+ void *ptr = f_elem_create(doc, RSTRING_PTR(StringValue(name)));
963
+ VALUE node, cache, key;
964
+
965
+ (void)self;
966
+ resolve_binding_classes();
967
+ if (!ptr)
968
+ return Qnil; /* caller raises with the error channel */
969
+ node = rb_obj_alloc(c_b_element);
970
+ rb_iv_set(node, "@c_ptr",
971
+ rb_funcall(c_ffi_pointer, id_ptr_new, 1,
972
+ ULL2NUM((uint64_t)(uintptr_t)ptr)));
973
+ rb_iv_set(node, "@document", document);
974
+ rb_iv_set(node, "@parent", Qnil);
975
+ rb_iv_set(node, "@node_type", INT2FIX(0));
976
+ cache = binding_cache_of(document);
977
+ key = ULL2NUM((uint64_t)(uintptr_t)ptr);
978
+ rb_hash_aset(cache, key, node);
979
+ return node;
980
+ }
981
+
982
+ static VALUE nf_create_binding_text(VALUE self, VALUE document,
983
+ VALUE content)
984
+ {
985
+ void *doc = doc_ptr_of(document);
986
+ void *ptr = f_text_create(doc, RSTRING_PTR(StringValue(content)));
987
+ VALUE node, cache, key;
988
+
989
+ (void)self;
990
+ resolve_binding_classes();
991
+ if (!ptr)
992
+ return Qnil;
993
+ node = rb_obj_alloc(c_b_text);
994
+ rb_iv_set(node, "@c_ptr",
995
+ rb_funcall(c_ffi_pointer, id_ptr_new, 1,
996
+ ULL2NUM((uint64_t)(uintptr_t)ptr)));
997
+ rb_iv_set(node, "@document", document);
998
+ rb_iv_set(node, "@parent", Qnil);
999
+ rb_iv_set(node, "@node_type", INT2FIX(1));
1000
+ cache = binding_cache_of(document);
1001
+ key = ULL2NUM((uint64_t)(uintptr_t)ptr);
1002
+ rb_hash_aset(cache, key, node);
1003
+ return node;
1004
+ }
1005
+
1006
+ /* ---- Bulk attribute FACES (TODO.perf/10): {name => Attr} with
1007
+ * the values hash beside it — the exact shapes Element#attributes
1008
+ * memoizes, built in one walk. Attr is a value object
1009
+ * (name/value/element ivars; c_handle optional). */
1010
+
1011
+ static ID id_iv_name, id_iv_value, id_iv_element;
1012
+
1013
+ static VALUE nf_bulk_attr_faces(VALUE self, VALUE addr, VALUE element)
1014
+ {
1015
+ void *node = (void *)(uintptr_t)NUM2ULL(addr);
1016
+ VALUE faces = rb_hash_new();
1017
+ VALUE values = rb_hash_new();
1018
+ void *attr = f_attr_first(node);
1019
+
1020
+ (void)self;
1021
+ resolve_binding_classes();
1022
+ while (attr) {
1023
+ const char *name = f_attr_name(attr);
1024
+ const char *value = f_attr_value(node, attr);
1025
+ VALUE k = name ? rb_utf8_str_new_cstr(name) : Qnil;
1026
+ VALUE v = value ? rb_utf8_str_new_cstr(value) : Qnil;
1027
+ VALUE a = rb_obj_alloc(c_b_attr);
1028
+ rb_iv_set(a, "@name", k);
1029
+ rb_iv_set(a, "@value", v);
1030
+ rb_iv_set(a, "@element", element);
1031
+ rb_hash_aset(faces, k, a);
1032
+ rb_hash_aset(values, k, v);
1033
+ attr = f_attr_next(attr);
1034
+ }
1035
+ return rb_assoc_new(faces, values);
1036
+ }
1037
+
697
1038
  /* ---- Ext-bound serialization (TODO.perf/04) ---------------------
698
1039
  * The whole sized-buffer cycle in C: build SerializeOptions on
699
1040
  * the stack, call (buf, cap, NULL status, opts), grow if needed,
@@ -766,6 +1107,12 @@ static VALUE nf_fast_element_xml(VALUE self, VALUE addr,
766
1107
  NUM2INT(indent), RTEST(decl) ? 1 : 0);
767
1108
  }
768
1109
 
1110
+ /* ---- Adoption-lift predicate (TODO.perf/09, #204 ask 3) ---------
1111
+ * The #178/#208 namespace lift + pruning runs on EVERY attach and
1112
+ * costs several FFI round-trips even when provably a no-op. This
1113
+ * predicate answers in ONE dispatch: false when the node carries
1114
+ * no namespace declarations AND its name has no prefix — the
1115
+ * common programmatic-build shape (bare names, no namespaces). */
769
1116
  /* ---- Address-based fast readers (TODO.perf/01) -----------------
770
1117
  * The DEFAULT binding classes call these when the bundle is
771
1118
  * loaded: one C-API dispatch + rb_utf8_str_new_cstr — no FFI
@@ -811,11 +1158,11 @@ static VALUE nn_from(VALUE klass, VALUE document, VALUE element)
811
1158
  struct native_node *n;
812
1159
  VALUE node = nn_allocate(klass);
813
1160
  void *ptr = (void *)(uintptr_t)NUM2ULL(
814
- rb_funcall(element, rb_intern("address"), 0));
1161
+ rb_funcall(element, id_address, 0));
815
1162
  TypedData_Get_Struct(node, struct native_node, &nn_type, n);
816
1163
  n->ptr = ptr;
817
1164
  n->document = document;
818
- rb_hash_aset(rb_funcall(document, id_wrapper_cache, 0),
1165
+ rb_hash_aset(native_cache_of(document),
819
1166
  ULL2NUM((uint64_t)(uintptr_t)ptr), node);
820
1167
  return node;
821
1168
  }
@@ -838,8 +1185,24 @@ void Init_native(void)
838
1185
  c_native_node = rb_define_class_under(m_xml, "NativeNode", rb_cObject);
839
1186
  rb_undef_alloc_func(c_native_node);
840
1187
 
841
- id_wrapper_cache = rb_intern("native_cache");
842
- id_binding_cache = rb_intern("wrapper_cache");
1188
+ id_iv_name = rb_intern("@name");
1189
+ id_iv_value = rb_intern("@value");
1190
+ id_iv_element = rb_intern("@element");
1191
+ id_iv_c_address = rb_intern("@c_address");
1192
+ id_iv_native_cache = rb_intern("@native_cache");
1193
+ id_iv_binding_cache = rb_intern("@wrapper_cache");
1194
+ id_iv_version = rb_intern("@version");
1195
+ id_iv_readonly = rb_intern("@readonly");
1196
+ id_address = rb_intern("address");
1197
+ id_iv_nn_content = rb_intern("@nn_content");
1198
+ id_iv_nn_content_ver = rb_intern("@nn_content_ver");
1199
+ id_iv_nn_attrs = rb_intern("@nn_attrs");
1200
+ id_iv_nn_attrs_ver = rb_intern("@nn_attrs_ver");
1201
+ c_readonly_error = rb_path2class("Leptris::XML::ReadOnlyError");
1202
+ c_use_after_free_error =
1203
+ rb_path2class("Leptris::XML::UseAfterFreeError");
1204
+ rb_gc_register_mark_object(c_readonly_error);
1205
+ rb_gc_register_mark_object(c_use_after_free_error);
843
1206
 
844
1207
  rb_define_singleton_method(c_native_node, "from", nn_from, 2);
845
1208
  rb_define_singleton_method(c_native_node, "create_element", nn_create_element, 2);
@@ -871,6 +1234,19 @@ void Init_native(void)
871
1234
  rb_define_module_function(m_native, "fast_element_text", nf_element_text, 1);
872
1235
  rb_define_module_function(m_native, "fast_attribute", nf_attribute, 2);
873
1236
  rb_define_module_function(m_native, "fast_prefix", nf_prefix, 1);
1237
+ rb_define_module_function(m_native, "fast_attribute2", nf_fast_attribute2, 2);
1238
+ rb_define_module_function(m_native, "ns_lift_needed?", nf_ns_lift_needed, 1);
1239
+ rb_define_module_function(m_native, "append_binding_child",
1240
+ nf_append_binding_child, 3);
1241
+ rb_define_module_function(m_native, "set_binding_attribute",
1242
+ nf_set_binding_attribute, 4);
1243
+ rb_define_module_function(m_native, "bulk_attributes", nf_bulk_attributes, 1);
1244
+ rb_define_module_function(m_native, "bulk_attr_faces",
1245
+ nf_bulk_attr_faces, 2);
1246
+ rb_define_module_function(m_native, "create_binding_element",
1247
+ nf_create_binding_element, 2);
1248
+ rb_define_module_function(m_native, "create_binding_text",
1249
+ nf_create_binding_text, 2);
874
1250
  rb_define_module_function(m_native, "bulk_children", nf_bulk_children, 2);
875
1251
  rb_define_module_function(m_native, "bulk_element_children",
876
1252
  nf_bulk_element_children, 2);
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.163.4"
4
+ VERSION = "1.9.163.5"
5
5
  end
@@ -32,6 +32,10 @@ class Leptris::XML::Document
32
32
 
33
33
  def initialize(c_ptr = nil, freed = Freed.new(:alive))
34
34
  @c_ptr = c_ptr
35
+ # Plain-Integer address twin of @c_ptr: the native layer reads
36
+ # this ivar directly (no method dispatch), nil'ed exactly when
37
+ # @c_ptr is.
38
+ @c_address = c_ptr&.address
35
39
  @freed = freed
36
40
  @readonly = false
37
41
  @version = 0
@@ -258,7 +262,9 @@ class Leptris::XML::Document
258
262
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
259
263
  # A document root has no in-scope declarations of its own —
260
264
  # lift everything the element's source scope carried (#178).
261
- Leptris::XML::Element.lift_namespaces_for_adoption(element, {})
265
+ unless Leptris::XML::Element.skip_adoption_lift?(element)
266
+ Leptris::XML::Element.lift_namespaces_for_adoption(element, {})
267
+ end
262
268
  Leptris::XML::FFI.check_status(
263
269
  Leptris::XML::FFI.leptris_document_set_root(@c_ptr, element.c_ptr))
264
270
  @version += 1
@@ -266,12 +272,24 @@ class Leptris::XML::Document
266
272
  end
267
273
 
268
274
  def create_element(name)
275
+ # TODO.perf/09: one C call creates + wraps when the ext is
276
+ # loaded — no FFI marshaling, no wrap_fresh frames.
277
+ if defined?(Leptris::XML::NATIVE_FAST)
278
+ node = Leptris::XML::Native.create_binding_element(self, name.to_s)
279
+ raise Leptris::XML::Error, "leptris_element_create failed" if node.nil?
280
+ return node
281
+ end
269
282
  ptr = Leptris::XML::FFI.leptris_element_create(@c_ptr, name)
270
283
  raise Leptris::XML::Error, "leptris_element_create failed" if ptr.null?
271
284
  Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_ELEMENT)
272
285
  end
273
286
 
274
287
  def create_text_node(content)
288
+ if defined?(Leptris::XML::NATIVE_FAST)
289
+ node = Leptris::XML::Native.create_binding_text(self, content.to_s)
290
+ raise Leptris::XML::Error, "leptris_text_node_create failed" if node.nil?
291
+ return node
292
+ end
275
293
  ptr = Leptris::XML::FFI.leptris_text_node_create(@c_ptr, content.to_s)
276
294
  raise Leptris::XML::Error, "leptris_text_node_create failed" if ptr.null?
277
295
  Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_TEXT)
@@ -371,6 +389,7 @@ class Leptris::XML::Document
371
389
  @freed.state = :freed
372
390
  Leptris::XML::FFI.leptris_document_free(@c_ptr) unless @c_ptr.nil?
373
391
  @c_ptr = nil
392
+ @c_address = nil
374
393
  @wrapper_cache&.clear
375
394
  end
376
395
 
@@ -114,6 +114,15 @@ class Leptris::XML::Element < Leptris::XML::Node
114
114
  alias_method :get_attribute, :[]
115
115
 
116
116
  def []=(key, value)
117
+ # C-bound write (TODO.perf/11): gates + version bump + engine
118
+ # set in one dispatch; the bump drops the version-stamped
119
+ # attribute memos on both surfaces.
120
+ if native_fast_children?
121
+ Leptris::XML::FFI.check_status(
122
+ Leptris::XML::Native.set_binding_attribute(
123
+ @document, @c_ptr.address, key.to_s, value.to_s))
124
+ return value
125
+ end
117
126
  ensure_writable!
118
127
  Leptris::XML::FFI.check_status(
119
128
  Leptris::XML::FFI.leptris_element_set_attribute(@c_ptr, key.to_s, value.to_s))
@@ -198,6 +207,17 @@ class Leptris::XML::Element < Leptris::XML::Node
198
207
  # Require the full Attr-hash face — a cold [] may have set the
199
208
  # version with only a partial @attr_values (ruby#150).
200
209
  return @attributes if @attributes && memo_hit?(@attributes_version)
210
+ if native_fast?
211
+ # TODO.perf/10: both memo faces in one C walk.
212
+ result, values = Leptris::XML::Native.bulk_attr_faces(
213
+ @c_ptr.address, self)
214
+ if @document
215
+ @attributes = result
216
+ @attr_values = values
217
+ @attributes_version = @document.version
218
+ end
219
+ return result
220
+ end
201
221
  result = {}
202
222
  values = {}
203
223
  each_attribute do |attr|
@@ -253,6 +273,21 @@ class Leptris::XML::Element < Leptris::XML::Node
253
273
  # adopted subtree. Call BEFORE attaching: the source scope is
254
274
  # read through the node's current ancestors. Identical scopes
255
275
  # (the common same-document move) add nothing.
276
+ # TODO.perf/09: one dispatch answers whether ANY lift work can
277
+ # apply (no resolved namespace, no own declarations = provable
278
+ # no-op — the common programmatic-build shape). Checked at the
279
+ # mutation sites so the target's in-scope namespaces are never
280
+ # materialized when the answer is no.
281
+ def self.skip_adoption_lift?(node)
282
+ # Non-elements can never need a lift (lift's own first line
283
+ # no-ops them) — skipping also avoids materializing the
284
+ # target's namespaces for the text/comment children every
285
+ # build appends.
286
+ return true unless node.is_a?(Leptris::XML::Element)
287
+ return false unless defined?(Leptris::XML::NATIVE_FAST)
288
+ !Leptris::XML::Native.ns_lift_needed?(node.c_ptr.address)
289
+ end
290
+
256
291
  def self.lift_namespaces_for_adoption(node, target_scope)
257
292
  return unless node.is_a?(Leptris::XML::Element)
258
293
  # Declarations the node already carries (its own definitions —
@@ -281,7 +316,9 @@ class Leptris::XML::Element < Leptris::XML::Node
281
316
 
282
317
  def prepend_child(node)
283
318
  ensure_writable!
284
- Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
319
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
320
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
321
+ end
285
322
  Leptris::XML::FFI.check_status(
286
323
  Leptris::XML::FFI.leptris_element_prepend_child(@c_ptr, node.c_ptr))
287
324
  node
@@ -289,7 +326,9 @@ class Leptris::XML::Element < Leptris::XML::Node
289
326
 
290
327
  def add_next_sibling(node)
291
328
  ensure_writable!
292
- Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
329
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
330
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
331
+ end
293
332
  Leptris::XML::FFI.check_status(
294
333
  Leptris::XML::FFI.leptris_element_insert_after(@c_ptr, node.c_ptr))
295
334
  node
@@ -297,7 +336,9 @@ class Leptris::XML::Element < Leptris::XML::Node
297
336
 
298
337
  def add_previous_sibling(node)
299
338
  ensure_writable!
300
- Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
339
+ unless Leptris::XML::Element.skip_adoption_lift?(node)
340
+ Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
341
+ end
301
342
  Leptris::XML::FFI.check_status(
302
343
  Leptris::XML::FFI.leptris_element_insert_before(@c_ptr, node.c_ptr))
303
344
  node
@@ -386,7 +427,22 @@ class Leptris::XML::Element < Leptris::XML::Node
386
427
  ensure_writable!
387
428
  case node_or_markup
388
429
  when Leptris::XML::Node
389
- Leptris::XML::Element.lift_namespaces_for_adoption(node_or_markup, namespaces)
430
+ # C-bound append (TODO.perf/08-09): one dispatch runs the
431
+ # readonly/liveness gates, the provable no-op lift
432
+ # predicate, the version bump, and the engine append. Qnil
433
+ # means the child needs the namespace lift — fall through
434
+ # to the full path.
435
+ if native_fast_children?
436
+ st = Leptris::XML::Native.append_binding_child(
437
+ @document, @c_ptr.address, node_or_markup.c_ptr.address)
438
+ unless st.nil?
439
+ Leptris::XML::FFI.check_status(st)
440
+ return node_or_markup
441
+ end
442
+ end
443
+ unless Leptris::XML::Element.skip_adoption_lift?(node_or_markup)
444
+ Leptris::XML::Element.lift_namespaces_for_adoption(node_or_markup, namespaces)
445
+ end
390
446
  Leptris::XML::FFI.check_status(
391
447
  Leptris::XML::FFI.leptris_element_append_child(@c_ptr, node_or_markup.c_ptr))
392
448
  node_or_markup
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.163.4
4
+ version: 1.9.163.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -75,6 +75,11 @@ files:
75
75
  - TODO.perf/04-ext-bound-serialization.md
76
76
  - TODO.perf/05-native-surface-completion.md
77
77
  - TODO.perf/06-perf-battery-native-legs.md
78
+ - TODO.perf/07-native-read-floor.md
79
+ - TODO.perf/08-version-coherent-native-mutations.md
80
+ - TODO.perf/09-adoption-lift-fast-path.md
81
+ - TODO.perf/10-bulk-attribute-materialization.md
82
+ - TODO.perf/11-moxml-gap-battery.md
78
83
  - TODO.restructure/01-constraint-compliance-audit.md
79
84
  - TODO.restructure/02-deep-copy-seam.md
80
85
  - TODO.restructure/03-evaluation-context-seam.md