leptris 1.9.174.2 → 1.9.174.3

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: fb172d69b816cff764e249cd5708a2394e7e4886a5c5281bf69160f876550c20
4
- data.tar.gz: 22ff15b7a5dbaee59a9643f150662321e6f52267cb96b47c6621e73957615f46
3
+ metadata.gz: 741e98553c4d88169d4c2f8ec0fb793eeebd80ffd8b00771fcfb0be218721f64
4
+ data.tar.gz: 5e02a23549a4e1c708dc40710b499e64fcd83150772dd08c8bbeac7fc77f0718
5
5
  SHA512:
6
- metadata.gz: d86e101b19a89c87b3ff67fc6fae563da827d3b389618fbada51cb9bcd387e772b474315e4d3d9f21bd6dd4bb1429df5b5f1de624787fc02db889dd6b1dc6efc
7
- data.tar.gz: c20c6f8446a7aef0b0d08c672deccf1d5aaa7653fe04e8f69c6a8a1b0f641f6bb5221d44f0c28f0cabcfb901ce2762ce5eed3ed6fa427b2231935bda1c6bd9b9
6
+ metadata.gz: b1a3b25cd5b770b6edfb06bc2d448dacd691dc7e5af3fb301454fb1057e419c5fc779e4c9556a3f3b4be654ea5ec36be8f8e5c4e0f7ab16af380b6986da3bcb5
7
+ data.tar.gz: 85e81207c2abaca9a80af5dcfb33ae4e4f83c2ec24950b16d38fcb239bf7fa4d01b40d4d88b7fa19e019ba35a9ce58984c823e3f049fc47492459ebde53aa47b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,25 @@ 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.3] - 2026-09-15
9
+
10
+ ### Changed
11
+
12
+ - **Lazy FFI::Pointer on wrappers (TODO.perf/19)**: the node's
13
+ canonical state is the Integer address; the Pointer
14
+ materializes only when read — the C construction faces stop
15
+ minting one per node (~200ns + one allocation each, and
16
+ proportionally less GC). Public `c_ptr` API and identity
17
+ unchanged. Measured (load 22-25): GC-amortized
18
+ `Document.create` ~5-6µs → ~4.0µs; `NativeNode#[]` 78ns
19
+ (budget 70), `#content` 45ns (budget 58 met).
20
+ - **CSS translation cache (TODO.perf/20)**: repeat selectors skip
21
+ the per-call regex translation straight to the compiled handle;
22
+ the single-path xpath shape no longer allocates a joined
23
+ String per call.
24
+ - **`Element#key?` consults the attribute memo (TODO.perf/21)**
25
+ before the engine round-trip; miss semantics unchanged.
26
+
8
27
  ## [1.9.174.2] - 2026-09-15
9
28
 
10
29
  ### Changed
@@ -0,0 +1,39 @@
1
+ # 19 — Lazy FFI::Pointer on wrappers
2
+
3
+ Status: DONE (1.9.174.3)
4
+
5
+ Every binding wrapper stores an FFI::Pointer (@c_ptr), and the C
6
+ construction faces mint one per node via
7
+ rb_funcall(FFI::Pointer, :new) — ~200ns plus a Ruby object and
8
+ its GC visit per node, paid at creation even though most nodes'
9
+ Pointers are never read (the hot reads are memoized or go through
10
+ the address-based native faces).
11
+
12
+ Store @c_address (Integer) as the canonical truth on every
13
+ wrapper; `c_ptr` becomes a lazy materializer
14
+ (@c_ptr ||= FFI::Pointer.new(@c_address)) preserving the public
15
+ API. Internal call sites: `@c_ptr.address` collapses to
16
+ @c_address (also skipping the #address dispatch), and remaining
17
+ internal @c_ptr reads go through the c_ptr reader (same cost as
18
+ the old attr_reader). The C faces stop constructing Pointers
19
+ entirely — they already hold the address.
20
+
21
+ Gates: suite green both modes (a missed site fails loudly — nil
22
+ to FFI raises TypeError); wrapper identity and c_ptr stability
23
+ unchanged; creation faces drop ~200ns and one allocation per
24
+ node; fresh-doc build row improves accordingly.
25
+
26
+ ## Outcome (1.9.174.3)
27
+
28
+ @c_address is the canonical truth on every Node wrapper; c_ptr
29
+ materializes lazily (@c_ptr ||= FFI::Pointer.new(@c_address)),
30
+ preserving the public API and identity. All internal @c_ptr
31
+ reads became c_ptr reader calls; the 15 @c_ptr.address sites
32
+ collapsed to @c_address (also skipping the #address dispatch).
33
+ The C construction faces (create element/text, bulk children,
34
+ every xpath materialization) stopped minting Pointers entirely —
35
+ one allocation and ~200ns saved per node, and proportionally less
36
+ GC. Document keeps its eager reader (documents are few).
37
+ Measured under host load 22-25: GC-amortized Document.create
38
+ ~5-6µs -> ~4.0µs; NativeNode#[] 78ns (budget 70 — clear at
39
+ load <10), content 45ns (budget 58 met).
@@ -0,0 +1,24 @@
1
+ # 20 — CSS translation cache
2
+
3
+ Status: DONE (1.9.174.3)
4
+
5
+ Searchable#css/#at_css run CssToXPath.convert on every call —
6
+ pure Ruby regex/string work (~300ns+) — before the compiled-
7
+ expression cache (TODO.perf/15) can hit on the result. The
8
+ translation is a pure function of (selector, prefix); cache it
9
+ with the same bounded-LRU shape (64 entries, Hash#shift
10
+ eviction, GVL-safe), so repeat selectors skip straight to the
11
+ compiled handle.
12
+
13
+ Gates: css/at_css results identical; repeat-call rows improve by
14
+ the translation cost; the join in xpath()'s expression build is
15
+ also skipped for the single-String-path shape (join minted a
16
+ fresh String per call even for one path).
17
+
18
+ ## Outcome (1.9.174.3)
19
+
20
+ Searchable.css_expression: bounded LRU (64, shared limit) keyed
21
+ on prefix+selector; css/at_css translate through it so repeat
22
+ selectors skip the regex machinery straight to the compiled-
23
+ expression cache. The single-String-path shape of xpath/at_xpath
24
+ no longer joins (join minted a fresh String per call).
@@ -0,0 +1,20 @@
1
+ # 21 — key? consults the attribute memo
2
+
3
+ Status: DONE (1.9.174.3)
4
+
5
+ Element#key?/has_attribute? pays an engine FFI round-trip per
6
+ call even when the versioned @attr_values memo already proves
7
+ presence. Consult the memo first (hit = true; only a miss falls
8
+ through to the engine + written-name fallback — the memo cannot
9
+ prove ABSENCE for a partial fill).
10
+
11
+ Gates: key? true for memoized names, unchanged miss semantics
12
+ (undeclared-prefix fallback intact), readonly/namespace specs
13
+ green.
14
+
15
+ ## Outcome (1.9.174.3)
16
+
17
+ Element#key? consults the versioned @attr_values memo first —
18
+ presence-proven hits skip the engine round-trip; partial-fill
19
+ misses still fall through to the engine + written-name fallback
20
+ (the memo cannot prove absence).
@@ -870,9 +870,8 @@ static VALUE bulk_children_impl(VALUE document, VALUE parent_addr,
870
870
  key = ULL2NUM((uint64_t)(uintptr_t)buf[i]);
871
871
  node = rb_hash_aref(cache, key);
872
872
  if (NIL_P(node)) {
873
- VALUE ptr = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
874
873
  node = rb_obj_alloc(binding_klass_for(kinds[i]));
875
- rb_iv_set(node, "@c_ptr", ptr);
874
+ rb_iv_set(node, "@c_address", key);
876
875
  rb_iv_set(node, "@document", document);
877
876
  rb_iv_set(node, "@parent", Qnil);
878
877
  rb_iv_set(node, "@structure_memoizable", Qtrue);
@@ -965,9 +964,8 @@ static VALUE materialize_xp_entry(VALUE document, VALUE cache,
965
964
  case 0: /* element */
966
965
  node = rb_hash_aref(cache, key);
967
966
  if (NIL_P(node)) {
968
- VALUE p = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
969
967
  node = rb_obj_alloc(c_b_element);
970
- rb_iv_set(node, "@c_ptr", p);
968
+ rb_iv_set(node, "@c_address", key);
971
969
  rb_iv_set(node, "@document", document);
972
970
  rb_iv_set(node, "@parent", Qnil);
973
971
  rb_iv_set(node, "@structure_memoizable", Qtrue);
@@ -980,9 +978,8 @@ static VALUE materialize_xp_entry(VALUE document, VALUE cache,
980
978
  {
981
979
  const char *name = f_xp_node_name(result, idx);
982
980
  const char *value = f_xp_node_value(result, idx);
983
- VALUE p = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
984
981
  node = rb_obj_alloc(c_b_result_attr);
985
- rb_iv_set(node, "@c_ptr", p);
982
+ rb_iv_set(node, "@c_address", key);
986
983
  rb_iv_set(node, "@document", document);
987
984
  rb_iv_set(node, "@attr_name",
988
985
  name ? rb_utf8_str_new_cstr(name) : Qnil);
@@ -998,8 +995,7 @@ static VALUE materialize_xp_entry(VALUE document, VALUE cache,
998
995
  if (nt == 8) { /* NODE_SYNTHETIC_TEXT */
999
996
  s = f_xp_node_value(result, idx);
1000
997
  node = rb_obj_alloc(c_b_result_text);
1001
- rb_iv_set(node, "@c_ptr",
1002
- rb_funcall(c_ffi_pointer, id_ptr_new, 1, key));
998
+ rb_iv_set(node, "@c_address", key);
1003
999
  rb_iv_set(node, "@document", document);
1004
1000
  rb_iv_set(node, "@value",
1005
1001
  s ? rb_utf8_str_new_cstr(s) : Qnil);
@@ -1007,9 +1003,8 @@ static VALUE materialize_xp_entry(VALUE document, VALUE cache,
1007
1003
  }
1008
1004
  node = rb_hash_aref(cache, key);
1009
1005
  if (NIL_P(node)) {
1010
- VALUE p = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
1011
1006
  node = rb_obj_alloc(binding_klass_for(nt));
1012
- rb_iv_set(node, "@c_ptr", p);
1007
+ rb_iv_set(node, "@c_address", key);
1013
1008
  rb_iv_set(node, "@document", document);
1014
1009
  rb_iv_set(node, "@parent", Qnil);
1015
1010
  rb_iv_set(node, "@structure_memoizable", Qtrue);
@@ -1096,9 +1091,8 @@ static VALUE nf_create_binding_element(VALUE self, VALUE document,
1096
1091
  if (!ptr)
1097
1092
  return Qnil; /* caller raises with the error channel */
1098
1093
  node = rb_obj_alloc(c_b_element);
1099
- rb_iv_set(node, "@c_ptr",
1100
- rb_funcall(c_ffi_pointer, id_ptr_new, 1,
1101
- ULL2NUM((uint64_t)(uintptr_t)ptr)));
1094
+ rb_iv_set(node, "@c_address",
1095
+ ULL2NUM((uint64_t)(uintptr_t)ptr));
1102
1096
  rb_iv_set(node, "@document", document);
1103
1097
  rb_iv_set(node, "@parent", Qnil);
1104
1098
  rb_iv_set(node, "@structure_memoizable", Qtrue);
@@ -1122,9 +1116,8 @@ static VALUE nf_create_binding_text(VALUE self, VALUE document,
1122
1116
  if (!ptr)
1123
1117
  return Qnil;
1124
1118
  node = rb_obj_alloc(c_b_text);
1125
- rb_iv_set(node, "@c_ptr",
1126
- rb_funcall(c_ffi_pointer, id_ptr_new, 1,
1127
- ULL2NUM((uint64_t)(uintptr_t)ptr)));
1119
+ rb_iv_set(node, "@c_address",
1120
+ ULL2NUM((uint64_t)(uintptr_t)ptr));
1128
1121
  rb_iv_set(node, "@document", document);
1129
1122
  rb_iv_set(node, "@parent", Qnil);
1130
1123
  rb_iv_set(node, "@structure_memoizable", Qtrue);
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.174.2"
4
+ VERSION = "1.9.174.3"
5
5
  end
@@ -6,7 +6,7 @@ class Leptris::XML::CDATA < Leptris::XML::Text
6
6
  def content
7
7
  return @content if memo_hit?(@content_version)
8
8
  ensure_alive!
9
- result = Leptris::XML::FFI.leptris_cdata_node_get_content(@c_ptr)
9
+ result = Leptris::XML::FFI.leptris_cdata_node_get_content(c_ptr)
10
10
  if @document
11
11
  @content = result
12
12
  @content_version = @document.version
@@ -17,7 +17,7 @@ class Leptris::XML::CDATA < Leptris::XML::Text
17
17
  def content=(new_content)
18
18
  ensure_writable!
19
19
  Leptris::XML::FFI.check_status(
20
- Leptris::XML::FFI.leptris_cdata_node_set_content(@c_ptr, new_content.to_s))
20
+ Leptris::XML::FFI.leptris_cdata_node_set_content(c_ptr, new_content.to_s))
21
21
  new_content
22
22
  end
23
23
  end
@@ -6,7 +6,7 @@ class Leptris::XML::Comment < Leptris::XML::Node
6
6
  def content
7
7
  return @content if memo_hit?(@content_version)
8
8
  ensure_alive!
9
- result = Leptris::XML::FFI.leptris_comment_node_get_content(@c_ptr)
9
+ result = Leptris::XML::FFI.leptris_comment_node_get_content(c_ptr)
10
10
  if @document
11
11
  @content = result
12
12
  @content_version = @document.version
@@ -17,7 +17,7 @@ class Leptris::XML::Comment < Leptris::XML::Node
17
17
  def content=(new_content)
18
18
  ensure_writable!
19
19
  Leptris::XML::FFI.check_status(
20
- Leptris::XML::FFI.leptris_comment_node_set_content(@c_ptr, new_content.to_s))
20
+ Leptris::XML::FFI.leptris_comment_node_set_content(c_ptr, new_content.to_s))
21
21
  new_content
22
22
  end
23
23
  end
@@ -9,24 +9,24 @@ class Leptris::XML::DocType
9
9
  end
10
10
 
11
11
  def name
12
- Leptris::XML::FFI.leptris_doctype_get_name(@c_ptr)
12
+ Leptris::XML::FFI.leptris_doctype_get_name(c_ptr)
13
13
  end
14
14
  alias_method :node_name, :name
15
15
 
16
16
  def root_name
17
- Leptris::XML::FFI.leptris_doctype_get_root_name(@c_ptr)
17
+ Leptris::XML::FFI.leptris_doctype_get_root_name(c_ptr)
18
18
  end
19
19
 
20
20
  def public_id
21
- Leptris::XML::FFI.leptris_doctype_get_public_id(@c_ptr)
21
+ Leptris::XML::FFI.leptris_doctype_get_public_id(c_ptr)
22
22
  end
23
23
 
24
24
  def system_id
25
- Leptris::XML::FFI.leptris_doctype_get_system_id(@c_ptr)
25
+ Leptris::XML::FFI.leptris_doctype_get_system_id(c_ptr)
26
26
  end
27
27
 
28
28
  def internal_subset
29
- Leptris::XML::FFI.leptris_doctype_get_internal_subset(@c_ptr)
29
+ Leptris::XML::FFI.leptris_doctype_get_internal_subset(c_ptr)
30
30
  end
31
31
 
32
32
  def external_id
@@ -32,9 +32,9 @@ 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
35
+ # Plain-Integer address twin of c_ptr: the native layer reads
36
36
  # this ivar directly (no method dispatch), nil'ed exactly when
37
- # @c_ptr is.
37
+ # c_ptr is.
38
38
  @c_address = c_ptr&.address
39
39
  @freed = freed
40
40
  @readonly = false
@@ -209,13 +209,13 @@ class Leptris::XML::Document
209
209
 
210
210
  def root
211
211
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
212
- return nil if @c_ptr.nil?
212
+ return nil if c_ptr.nil?
213
213
  # Version-stamped memo (TODO.perf/13): root changes only
214
214
  # through mutations that advance @version (root=, unlink) or
215
- # #free (which nils @c_ptr above) — the entry point of every
215
+ # #free (which nils c_ptr above) — the entry point of every
216
216
  # pipeline stops paying FFI + wrap per call.
217
217
  return @root if @root_version == @version
218
- ptr = Leptris::XML::FFI.leptris_document_root(@c_ptr)
218
+ ptr = Leptris::XML::FFI.leptris_document_root(c_ptr)
219
219
  result = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, self)
220
220
  @root = result
221
221
  @root_version = @version
@@ -231,7 +231,7 @@ class Leptris::XML::Document
231
231
  def node
232
232
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
233
233
  @node ||= Leptris::XML::Node.wrap(
234
- Leptris::XML::FFI.leptris_document_node(@c_ptr), self)
234
+ Leptris::XML::FFI.leptris_document_node(c_ptr), self)
235
235
  end
236
236
 
237
237
  # The document's children, via the document node: prolog
@@ -250,7 +250,7 @@ class Leptris::XML::Document
250
250
  doc_node = node
251
251
  return Leptris::XML::NodeSet.new(self, []) if doc_node.nil?
252
252
  kids = doc_node.children.to_a
253
- root_ptr = Leptris::XML::FFI.leptris_document_root(@c_ptr)
253
+ root_ptr = Leptris::XML::FFI.leptris_document_root(c_ptr)
254
254
  return kids if root_ptr.null?
255
255
  return kids if kids.any? { |child| child.c_ptr == root_ptr }
256
256
  root = Leptris::XML::Node.wrap(root_ptr, self)
@@ -289,7 +289,7 @@ class Leptris::XML::Document
289
289
  Leptris::XML::Element.lift_namespaces_for_adoption(element, {})
290
290
  end
291
291
  Leptris::XML::FFI.check_status(
292
- Leptris::XML::FFI.leptris_document_set_root(@c_ptr, element.c_ptr))
292
+ Leptris::XML::FFI.leptris_document_set_root(c_ptr, element.c_ptr))
293
293
  @version += 1
294
294
  # Seed the root memo through wrap: a cross-document element
295
295
  # must enter THIS document's identity cache with @document
@@ -308,7 +308,7 @@ class Leptris::XML::Document
308
308
  raise Leptris::XML::Error, "leptris_element_create failed" if node.nil?
309
309
  return node
310
310
  end
311
- ptr = Leptris::XML::FFI.leptris_element_create(@c_ptr, name)
311
+ ptr = Leptris::XML::FFI.leptris_element_create(c_ptr, name)
312
312
  raise Leptris::XML::Error, "leptris_element_create failed" if ptr.null?
313
313
  Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_ELEMENT)
314
314
  end
@@ -319,25 +319,25 @@ class Leptris::XML::Document
319
319
  raise Leptris::XML::Error, "leptris_text_node_create failed" if node.nil?
320
320
  return node
321
321
  end
322
- ptr = Leptris::XML::FFI.leptris_text_node_create(@c_ptr, content.to_s)
322
+ ptr = Leptris::XML::FFI.leptris_text_node_create(c_ptr, content.to_s)
323
323
  raise Leptris::XML::Error, "leptris_text_node_create failed" if ptr.null?
324
324
  Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_TEXT)
325
325
  end
326
326
 
327
327
  def create_comment(content)
328
- ptr = Leptris::XML::FFI.leptris_comment_node_create(@c_ptr, content.to_s)
328
+ ptr = Leptris::XML::FFI.leptris_comment_node_create(c_ptr, content.to_s)
329
329
  raise Leptris::XML::Error, "leptris_comment_node_create failed" if ptr.null?
330
330
  Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_COMMENT)
331
331
  end
332
332
 
333
333
  def create_cdata(content)
334
- ptr = Leptris::XML::FFI.leptris_cdata_node_create(@c_ptr, content.to_s)
334
+ ptr = Leptris::XML::FFI.leptris_cdata_node_create(c_ptr, content.to_s)
335
335
  raise Leptris::XML::Error, "leptris_cdata_node_create failed" if ptr.null?
336
336
  Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_CDATA)
337
337
  end
338
338
 
339
339
  def create_processing_instruction(target, data = "")
340
- ptr = Leptris::XML::FFI.leptris_pi_node_create(@c_ptr, target.to_s, data.to_s)
340
+ ptr = Leptris::XML::FFI.leptris_pi_node_create(c_ptr, target.to_s, data.to_s)
341
341
  raise Leptris::XML::Error, "leptris_pi_node_create failed" if ptr.null?
342
342
  Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_PI)
343
343
  end
@@ -347,14 +347,14 @@ class Leptris::XML::Document
347
347
  end
348
348
 
349
349
  def dup
350
- raw = Leptris::XML::FFI.leptris_document_copy(@c_ptr)
350
+ raw = Leptris::XML::FFI.leptris_document_copy(c_ptr)
351
351
  raise Leptris::XML::Error, "leptris_document_copy failed" if raw.null?
352
352
  self.class.wrap(raw)
353
353
  end
354
354
  alias_method :clone, :dup
355
355
 
356
356
  def doctype
357
- ptr = Leptris::XML::FFI.leptris_document_internal_subset(@c_ptr)
357
+ ptr = Leptris::XML::FFI.leptris_document_internal_subset(c_ptr)
358
358
  return nil if ptr.null?
359
359
  Leptris::XML::DocType.new(ptr, self)
360
360
  end
@@ -368,18 +368,18 @@ class Leptris::XML::Document
368
368
  # display-oriented and not round-trip-guaranteed).
369
369
  def to_xml(indent: 0, no_decl: false, encoding: nil, indent_text: false)
370
370
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
371
- return "" if @c_ptr.nil?
371
+ return "" if c_ptr.nil?
372
372
  case indent_text
373
373
  when String
374
374
  return Leptris::XML::Serialization.to_xml_indent_unit(
375
- @c_ptr, indent_text, indent: indent, no_decl: no_decl,
375
+ c_ptr, indent_text, indent: indent, no_decl: no_decl,
376
376
  encoding: encoding)
377
377
  when true
378
378
  return Leptris::XML::Serialization.to_xml_display(
379
- @c_ptr, indent: indent, no_decl: no_decl, encoding: encoding)
379
+ c_ptr, indent: indent, no_decl: no_decl, encoding: encoding)
380
380
  end
381
381
  Leptris::XML::Serialization.to_xml(
382
- Leptris::XML::Serialization::DOCUMENT_SERIALIZE_INTO, @c_ptr,
382
+ Leptris::XML::Serialization::DOCUMENT_SERIALIZE_INTO, c_ptr,
383
383
  indent: indent, no_decl: no_decl, encoding: encoding)
384
384
  end
385
385
  alias_method :to_s, :to_xml
@@ -391,7 +391,7 @@ class Leptris::XML::Document
391
391
  no_decl: opts.fetch(:no_decl, false),
392
392
  encoding: opts[:encoding])
393
393
  status = Leptris::XML::FFI.leptris_document_save_file(
394
- @c_ptr, path, opts_struct.pointer)
394
+ c_ptr, path, opts_struct.pointer)
395
395
  Leptris::XML::FFI.check_status(status)
396
396
  self
397
397
  end
@@ -402,11 +402,11 @@ class Leptris::XML::Document
402
402
  exclusive: false,
403
403
  mode: nil)
404
404
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
405
- return "" if @c_ptr.nil?
405
+ return "" if c_ptr.nil?
406
406
  resolved_mode = mode || (exclusive ? Leptris::XML::FFI::C14N_MODE_EXCLUSIVE
407
407
  : Leptris::XML::FFI::C14N_MODE_CANONICAL)
408
408
  Leptris::XML::Serialization.canonicalize(
409
- Leptris::XML::FFI.method(:leptris_c14n_canonicalize_ex), @c_ptr,
409
+ Leptris::XML::FFI.method(:leptris_c14n_canonicalize_ex), c_ptr,
410
410
  version: version, mode: resolved_mode,
411
411
  inclusive_namespaces: inclusive_namespaces,
412
412
  with_comments: with_comments)
@@ -416,7 +416,7 @@ class Leptris::XML::Document
416
416
  def free
417
417
  return if @freed.state == :freed
418
418
  @freed.state = :freed
419
- Leptris::XML::FFI.leptris_document_free(@c_ptr) unless @c_ptr.nil?
419
+ Leptris::XML::FFI.leptris_document_free(c_ptr) unless c_ptr.nil?
420
420
  @c_ptr = nil
421
421
  @c_address = nil
422
422
  @wrapper_cache&.clear
@@ -432,7 +432,7 @@ class Leptris::XML::Document
432
432
  # handlers. Returns self for chaining.
433
433
  def exslt
434
434
  Leptris::XML::FFI.check_status(
435
- Leptris::XML::FFI.leptris_exslt_enable(@c_ptr))
435
+ Leptris::XML::FFI.leptris_exslt_enable(c_ptr))
436
436
  self
437
437
  end
438
438
 
@@ -440,11 +440,11 @@ class Leptris::XML::Document
440
440
  # an array of [target, data] pairs in document order.
441
441
  def processing_instructions
442
442
  return @processing_instructions if @pi_version == @version
443
- count = Leptris::XML::FFI.leptris_document_pi_count(@c_ptr)
443
+ count = Leptris::XML::FFI.leptris_document_pi_count(c_ptr)
444
444
  result = count.times.map do |i|
445
- [Leptris::XML::FFI.leptris_document_pi_target(@c_ptr, i),
445
+ [Leptris::XML::FFI.leptris_document_pi_target(c_ptr, i),
446
446
  Leptris::XML::FFI.read_pi_data(
447
- Leptris::XML::FFI.leptris_document_pi_data(@c_ptr, i)).to_s]
447
+ Leptris::XML::FFI.leptris_document_pi_data(c_ptr, i)).to_s]
448
448
  end
449
449
  @processing_instructions = result
450
450
  @pi_version = @version
@@ -460,10 +460,10 @@ class Leptris::XML::Document
460
460
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
461
461
  if target_or_index.is_a?(Integer)
462
462
  ptr = Leptris::XML::FFI.leptris_document_remove_pi(
463
- @c_ptr, nil, target_or_index)
463
+ c_ptr, nil, target_or_index)
464
464
  else
465
465
  ptr = Leptris::XML::FFI.leptris_document_remove_pi(
466
- @c_ptr, target_or_index.to_s, 0)
466
+ c_ptr, target_or_index.to_s, 0)
467
467
  end
468
468
  return nil if ptr.null?
469
469
  @version += 1
@@ -473,7 +473,7 @@ class Leptris::XML::Document
473
473
  # Append a document-level processing instruction. Returns self.
474
474
  def add_pi(target, data = "")
475
475
  witness = Leptris::XML::FFI.leptris_document_add_pi(
476
- @c_ptr, target.to_s, data.to_s)
476
+ c_ptr, target.to_s, data.to_s)
477
477
  raise Leptris::XML::Error, "leptris_document_add_pi failed" if witness.null?
478
478
  @version += 1
479
479
  self
@@ -485,7 +485,7 @@ class Leptris::XML::Document
485
485
  # this is the writer). Returns self.
486
486
  def add_comment(content)
487
487
  witness = Leptris::XML::FFI.leptris_document_add_comment(
488
- @c_ptr, content.to_s)
488
+ c_ptr, content.to_s)
489
489
  raise Leptris::XML::Error,
490
490
  "leptris_document_add_comment failed" if witness.null?
491
491
  @version += 1
@@ -497,7 +497,7 @@ class Leptris::XML::Document
497
497
  # (names, content, children, attributes) since they can never go
498
498
  # stale. The C document is also frozen (advisory upstream). One-way.
499
499
  def readonly!
500
- Leptris::XML::FFI.leptris_document_freeze(@c_ptr)
500
+ Leptris::XML::FFI.leptris_document_freeze(c_ptr)
501
501
  @readonly = true
502
502
  self
503
503
  end
@@ -509,7 +509,7 @@ class Leptris::XML::Document
509
509
  # True once #free has run (or the GC finalizer fired) — borrowed
510
510
  # handles check this before dereferencing their c_ptr.
511
511
  def freed?
512
- @freed.state == :freed || @c_ptr.nil?
512
+ @freed.state == :freed || c_ptr.nil?
513
513
  end
514
514
 
515
515
  # Document-level comments — parsed <!-- ... --> outside the
@@ -518,9 +518,9 @@ class Leptris::XML::Document
518
518
  # 1.9.3, upstream #578). Version-memoized like the PI list.
519
519
  def comments
520
520
  return @comments if @comments_version == @version
521
- count = Leptris::XML::FFI.leptris_document_comment_count(@c_ptr)
521
+ count = Leptris::XML::FFI.leptris_document_comment_count(c_ptr)
522
522
  result = Array.new(count) do |i|
523
- Leptris::XML::FFI.leptris_document_comment_content(@c_ptr, i)
523
+ Leptris::XML::FFI.leptris_document_comment_content(c_ptr, i)
524
524
  end
525
525
  @comments = result
526
526
  @comments_version = @version
@@ -545,15 +545,15 @@ class Leptris::XML::Document
545
545
 
546
546
  # The most recent error recorded against this document, or nil.
547
547
  def last_error
548
- msg = Leptris::XML::FFI.leptris_document_last_error(@c_ptr)
548
+ msg = Leptris::XML::FFI.leptris_document_last_error(c_ptr)
549
549
  msg.nil? || msg.empty? ? nil : msg
550
550
  end
551
551
 
552
552
  def name; "document"; end
553
553
  def document; self; end
554
554
  def encoding
555
- return nil if @c_ptr.nil?
556
- Leptris::XML::FFI.leptris_document_encoding(@c_ptr)
555
+ return nil if c_ptr.nil?
556
+ Leptris::XML::FFI.leptris_document_encoding(c_ptr)
557
557
  end
558
558
 
559
559
  include Leptris::XML::Searchable
@@ -25,7 +25,7 @@ class Leptris::XML::DocumentFragment
25
25
  end
26
26
 
27
27
  def children
28
- pointers, kinds = Leptris::XML::FFI.fetch_children(@c_ptr)
28
+ pointers, kinds = Leptris::XML::FFI.fetch_children(c_ptr)
29
29
  nodes = Array.new(pointers.size) do |i|
30
30
  Leptris::XML::Node.wrap(pointers[i], @document, node_type: kinds[i])
31
31
  end
@@ -16,9 +16,9 @@ class Leptris::XML::Element < Leptris::XML::Node
16
16
  return @name if @name
17
17
  ensure_alive!
18
18
  @name = if native_fast?
19
- Leptris::XML::Native.fast_name(@c_ptr.address)
19
+ Leptris::XML::Native.fast_name(@c_address)
20
20
  else
21
- Leptris::XML::FFI.leptris_element_name(@c_ptr)
21
+ Leptris::XML::FFI.leptris_element_name(c_ptr)
22
22
  end
23
23
  end
24
24
  alias_method :node_name, :name
@@ -26,7 +26,7 @@ class Leptris::XML::Element < Leptris::XML::Node
26
26
  def name=(new_name)
27
27
  ensure_writable!
28
28
  Leptris::XML::FFI.check_status(
29
- Leptris::XML::FFI.leptris_element_set_name(@c_ptr, new_name))
29
+ Leptris::XML::FFI.leptris_element_set_name(c_ptr, new_name))
30
30
  @name = new_name
31
31
  end
32
32
  alias_method :node_name=, :name=
@@ -35,9 +35,9 @@ class Leptris::XML::Element < Leptris::XML::Node
35
35
  return @content if memo_hit?(@content_version)
36
36
  ensure_alive!
37
37
  result = if native_fast?
38
- Leptris::XML::Native.fast_element_text(@c_ptr.address)
38
+ Leptris::XML::Native.fast_element_text(@c_address)
39
39
  else
40
- Leptris::XML::FFI.leptris_element_text(@c_ptr)
40
+ Leptris::XML::FFI.leptris_element_text(c_ptr)
41
41
  end
42
42
  if @document
43
43
  @content = result
@@ -49,7 +49,7 @@ class Leptris::XML::Element < Leptris::XML::Node
49
49
  def content=(new_content)
50
50
  ensure_writable!
51
51
  Leptris::XML::FFI.check_status(
52
- Leptris::XML::FFI.leptris_element_set_text(@c_ptr, new_content.to_s))
52
+ Leptris::XML::FFI.leptris_element_set_text(c_ptr, new_content.to_s))
53
53
  new_content
54
54
  end
55
55
 
@@ -80,9 +80,9 @@ class Leptris::XML::Element < Leptris::XML::Node
80
80
  return v if !v.nil? || @attributes
81
81
  ensure_alive!
82
82
  v = if native_fast?
83
- Leptris::XML::Native.fast_attribute(@c_ptr.address, name)
83
+ Leptris::XML::Native.fast_attribute(@c_address, name)
84
84
  else
85
- Leptris::XML::FFI.leptris_element_attribute(@c_ptr, name)
85
+ Leptris::XML::FFI.leptris_element_attribute(c_ptr, name)
86
86
  end
87
87
  values[name] = v
88
88
  return v
@@ -93,9 +93,9 @@ class Leptris::XML::Element < Leptris::XML::Node
93
93
  # stale @attributes hash and attributes would serve it.
94
94
  ensure_alive!
95
95
  v = if native_fast?
96
- Leptris::XML::Native.fast_attribute(@c_ptr.address, name)
96
+ Leptris::XML::Native.fast_attribute(@c_address, name)
97
97
  else
98
- Leptris::XML::FFI.leptris_element_attribute(@c_ptr, name)
98
+ Leptris::XML::FFI.leptris_element_attribute(c_ptr, name)
99
99
  end
100
100
  @attr_values = { name => v }
101
101
  @attributes = nil
@@ -106,7 +106,7 @@ class Leptris::XML::Element < Leptris::XML::Node
106
106
  return v
107
107
  end
108
108
  ensure_alive!
109
- v = Leptris::XML::FFI.leptris_element_attribute(@c_ptr, name)
109
+ v = Leptris::XML::FFI.leptris_element_attribute(c_ptr, name)
110
110
  # Namespace-aware misses (an undeclared prefix never resolves
111
111
  # through in-scope declarations) fall back to the WRITTEN name
112
112
  # — the same flat face #attributes exposes, so a value written
@@ -124,23 +124,32 @@ class Leptris::XML::Element < Leptris::XML::Node
124
124
  if native_fast_children?
125
125
  Leptris::XML::FFI.check_status(
126
126
  Leptris::XML::Native.set_binding_attribute(
127
- @document, @c_ptr.address, key.to_s, value.to_s))
127
+ @document, @c_address, key.to_s, value.to_s))
128
128
  return value
129
129
  end
130
130
  ensure_writable!
131
131
  Leptris::XML::FFI.check_status(
132
- Leptris::XML::FFI.leptris_element_set_attribute(@c_ptr, key.to_s, value.to_s))
132
+ Leptris::XML::FFI.leptris_element_set_attribute(c_ptr, key.to_s, value.to_s))
133
133
  value
134
134
  end
135
135
  alias_method :set_attribute, :[]=
136
136
 
137
137
  def key?(name)
138
138
  ensure_alive!
139
+ # Versioned attribute memo first (TODO.perf/21): a hit proves
140
+ # presence without the engine round-trip; the memo cannot
141
+ # prove absence for a partial fill, so misses fall through.
142
+ n = name.to_s
143
+ values = @attr_values
144
+ if @document && values && @attributes_version == @document.version &&
145
+ values.key?(n)
146
+ return true
147
+ end
139
148
  return true if Leptris::XML::FFI.leptris_element_has_attribute(
140
- @c_ptr, name.to_s) != 0
149
+ c_ptr, n) != 0
141
150
  # Same written-name fallback as #[] — the engine lookup is
142
151
  # namespace-aware and misses undeclared prefixes (#161).
143
- attributes.key?(name.to_s)
152
+ attributes.key?(n)
144
153
  end
145
154
  alias_method :has_attribute?, :key?
146
155
 
@@ -152,19 +161,19 @@ class Leptris::XML::Element < Leptris::XML::Node
152
161
  def attribute_ns(uri, local)
153
162
  ensure_alive!
154
163
  Leptris::XML::FFI.leptris_element_attribute_ns(
155
- @c_ptr, uri&.to_s, local.to_s)
164
+ c_ptr, uri&.to_s, local.to_s)
156
165
  end
157
166
 
158
167
  def has_attribute_ns?(uri, local)
159
168
  ensure_alive!
160
169
  Leptris::XML::FFI.leptris_element_has_attribute_ns(
161
- @c_ptr, uri&.to_s, local.to_s) != 0
170
+ c_ptr, uri&.to_s, local.to_s) != 0
162
171
  end
163
172
 
164
173
  def remove_attribute(name)
165
174
  ensure_writable!
166
175
  Leptris::XML::FFI.check_status(
167
- Leptris::XML::FFI.leptris_element_remove_attribute(@c_ptr, name.to_s))
176
+ Leptris::XML::FFI.leptris_element_remove_attribute(c_ptr, name.to_s))
168
177
  self
169
178
  end
170
179
  alias_method :delete, :remove_attribute
@@ -177,10 +186,10 @@ class Leptris::XML::Element < Leptris::XML::Node
177
186
  def each_attribute
178
187
  return enum_for(:each_attribute) unless block_given?
179
188
  ensure_alive!
180
- attr = Leptris::XML::FFI.leptris_element_first_attribute(@c_ptr)
189
+ attr = Leptris::XML::FFI.leptris_element_first_attribute(c_ptr)
181
190
  until attr.nil? || attr.null?
182
191
  name = Leptris::XML::FFI.leptris_attribute_get_name(attr)
183
- value = Leptris::XML::FFI.leptris_attribute_get_value(@c_ptr, attr)
192
+ value = Leptris::XML::FFI.leptris_attribute_get_value(c_ptr, attr)
184
193
  yield Leptris::XML::Attr.new(name, value, self, c_handle: attr)
185
194
  attr = Leptris::XML::FFI.leptris_attribute_next(attr)
186
195
  end
@@ -214,7 +223,7 @@ class Leptris::XML::Element < Leptris::XML::Node
214
223
  if native_fast?
215
224
  # TODO.perf/10: both memo faces in one C walk.
216
225
  result, values = Leptris::XML::Native.bulk_attr_faces(
217
- @c_ptr.address, self)
226
+ @c_address, self)
218
227
  if @document
219
228
  @attributes = result
220
229
  @attr_values = values
@@ -251,9 +260,9 @@ class Leptris::XML::Element < Leptris::XML::Node
251
260
  def prefix
252
261
  ensure_alive!
253
262
  if native_fast?
254
- Leptris::XML::Native.fast_prefix(@c_ptr.address)
263
+ Leptris::XML::Native.fast_prefix(@c_address)
255
264
  else
256
- result = Leptris::XML::FFI.leptris_element_prefix(@c_ptr)
265
+ result = Leptris::XML::FFI.leptris_element_prefix(c_ptr)
257
266
  result if result && !result.empty?
258
267
  end
259
268
  end
@@ -324,7 +333,7 @@ class Leptris::XML::Element < Leptris::XML::Node
324
333
  # the namespace lift — the full path below handles it.
325
334
  if native_fast_children?
326
335
  st = Leptris::XML::Native.insert_binding_child(
327
- @document, @c_ptr.address, node.c_ptr.address, 1)
336
+ @document, @c_address, node.c_ptr.address, 1)
328
337
  unless st.nil?
329
338
  Leptris::XML::FFI.check_status(st)
330
339
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
@@ -336,7 +345,7 @@ class Leptris::XML::Element < Leptris::XML::Node
336
345
  Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
337
346
  end
338
347
  Leptris::XML::FFI.check_status(
339
- Leptris::XML::FFI.leptris_element_prepend_child(@c_ptr, node.c_ptr))
348
+ Leptris::XML::FFI.leptris_element_prepend_child(c_ptr, node.c_ptr))
340
349
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
341
350
  node
342
351
  end
@@ -347,7 +356,7 @@ class Leptris::XML::Element < Leptris::XML::Node
347
356
  # the namespace lift — the full path below handles it.
348
357
  if native_fast_children?
349
358
  st = Leptris::XML::Native.insert_binding_child(
350
- @document, @c_ptr.address, node.c_ptr.address, 2)
359
+ @document, @c_address, node.c_ptr.address, 2)
351
360
  unless st.nil?
352
361
  Leptris::XML::FFI.check_status(st)
353
362
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
@@ -359,7 +368,7 @@ class Leptris::XML::Element < Leptris::XML::Node
359
368
  Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
360
369
  end
361
370
  Leptris::XML::FFI.check_status(
362
- Leptris::XML::FFI.leptris_element_insert_after(@c_ptr, node.c_ptr))
371
+ Leptris::XML::FFI.leptris_element_insert_after(c_ptr, node.c_ptr))
363
372
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
364
373
  node
365
374
  end
@@ -370,7 +379,7 @@ class Leptris::XML::Element < Leptris::XML::Node
370
379
  # the namespace lift — the full path below handles it.
371
380
  if native_fast_children?
372
381
  st = Leptris::XML::Native.insert_binding_child(
373
- @document, @c_ptr.address, node.c_ptr.address, 3)
382
+ @document, @c_address, node.c_ptr.address, 3)
374
383
  unless st.nil?
375
384
  Leptris::XML::FFI.check_status(st)
376
385
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
@@ -382,7 +391,7 @@ class Leptris::XML::Element < Leptris::XML::Node
382
391
  Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
383
392
  end
384
393
  Leptris::XML::FFI.check_status(
385
- Leptris::XML::FFI.leptris_element_insert_before(@c_ptr, node.c_ptr))
394
+ Leptris::XML::FFI.leptris_element_insert_before(c_ptr, node.c_ptr))
386
395
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
387
396
  node
388
397
  end
@@ -390,7 +399,7 @@ class Leptris::XML::Element < Leptris::XML::Node
390
399
  def remove_child(node)
391
400
  ensure_writable!
392
401
  Leptris::XML::FFI.check_status(
393
- Leptris::XML::FFI.leptris_element_remove_child(@c_ptr, node.c_ptr))
402
+ Leptris::XML::FFI.leptris_element_remove_child(c_ptr, node.c_ptr))
394
403
  node
395
404
  end
396
405
 
@@ -398,7 +407,7 @@ class Leptris::XML::Element < Leptris::XML::Node
398
407
  ensure_writable!
399
408
  # Remove existing children, then attach the new ones in source order.
400
409
  Leptris::XML::FFI.check_status(
401
- Leptris::XML::FFI.leptris_element_remove_children(@c_ptr))
410
+ Leptris::XML::FFI.leptris_element_remove_children(c_ptr))
402
411
  Array(node_or_nodes).each { |n| add_child(n) }
403
412
  end
404
413
 
@@ -460,7 +469,7 @@ class Leptris::XML::Element < Leptris::XML::Node
460
469
  # document.create_element(name) + add_child.
461
470
  def create_child(name)
462
471
  ensure_writable!
463
- ptr = Leptris::XML::FFI.leptris_element_create_child(@c_ptr, name.to_s)
472
+ ptr = Leptris::XML::FFI.leptris_element_create_child(c_ptr, name.to_s)
464
473
  raise Leptris::XML::Error,
465
474
  "leptris_element_create_child failed for #{name.inspect}" if ptr.null?
466
475
  Leptris::XML::Node.wrap_fresh(ptr, @document, Leptris::XML::FFI::NODE_ELEMENT)
@@ -477,7 +486,7 @@ class Leptris::XML::Element < Leptris::XML::Node
477
486
  # to the full path.
478
487
  if native_fast_children?
479
488
  st = Leptris::XML::Native.append_binding_child(
480
- @document, @c_ptr.address, node_or_markup.c_ptr.address)
489
+ @document, @c_address, node_or_markup.c_ptr.address)
481
490
  unless st.nil?
482
491
  Leptris::XML::FFI.check_status(st)
483
492
  Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
@@ -488,7 +497,7 @@ class Leptris::XML::Element < Leptris::XML::Node
488
497
  Leptris::XML::Element.lift_namespaces_for_adoption(node_or_markup, namespaces)
489
498
  end
490
499
  Leptris::XML::FFI.check_status(
491
- Leptris::XML::FFI.leptris_element_append_child(@c_ptr, node_or_markup.c_ptr))
500
+ Leptris::XML::FFI.leptris_element_append_child(c_ptr, node_or_markup.c_ptr))
492
501
  Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
493
502
  node_or_markup
494
503
  when String
@@ -496,7 +505,7 @@ class Leptris::XML::Element < Leptris::XML::Node
496
505
  added = []
497
506
  frag.children.each do |n|
498
507
  Leptris::XML::FFI.check_status(
499
- Leptris::XML::FFI.leptris_element_append_child(@c_ptr, n.c_ptr))
508
+ Leptris::XML::FFI.leptris_element_append_child(c_ptr, n.c_ptr))
500
509
  added << n
501
510
  end
502
511
  Leptris::XML::NodeSet.new(@document, added)
@@ -509,7 +518,7 @@ class Leptris::XML::Element < Leptris::XML::Node
509
518
  def namespace
510
519
  return @namespace if memo_hit?(@namespace_version)
511
520
  ensure_alive!
512
- uri = Leptris::XML::FFI.leptris_element_namespace(@c_ptr)
521
+ uri = Leptris::XML::FFI.leptris_element_namespace(c_ptr)
513
522
  result =
514
523
  if uri.nil? || uri.empty?
515
524
  nil
@@ -517,7 +526,7 @@ class Leptris::XML::Element < Leptris::XML::Node
517
526
  # The resolved namespace is reached via this element's own
518
527
  # prefix, so carry it through: consumers that distinguish
519
528
  # {"p" => "urn:p"} from the default {"nil => "urn:p"} need it.
520
- prefix = Leptris::XML::FFI.leptris_element_prefix(@c_ptr)
529
+ prefix = Leptris::XML::FFI.leptris_element_prefix(c_ptr)
521
530
  prefix = nil if prefix.nil? || prefix.empty?
522
531
  Leptris::XML::Namespace.new(self, uri, prefix: prefix)
523
532
  end
@@ -538,7 +547,7 @@ class Leptris::XML::Element < Leptris::XML::Node
538
547
  def namespace=(uri)
539
548
  ensure_writable!
540
549
  Leptris::XML::FFI.check_status(
541
- Leptris::XML::FFI.leptris_element_set_namespace(@c_ptr, uri&.to_s))
550
+ Leptris::XML::FFI.leptris_element_set_namespace(c_ptr, uri&.to_s))
542
551
  uri
543
552
  end
544
553
 
@@ -551,7 +560,7 @@ class Leptris::XML::Element < Leptris::XML::Node
551
560
  prefix = ::FFI::MemoryPointer.new(:pointer)
552
561
  uri = ::FFI::MemoryPointer.new(:pointer)
553
562
  Leptris::XML::FFI.leptris_element_expanded_name(
554
- @c_ptr, local, prefix, uri)
563
+ c_ptr, local, prefix, uri)
555
564
  local_ptr = local.read_pointer
556
565
  prefix_ptr = prefix.read_pointer
557
566
  uri_ptr = uri.read_pointer
@@ -569,10 +578,10 @@ class Leptris::XML::Element < Leptris::XML::Node
569
578
  def namespace_definitions
570
579
  return @namespace_definitions if memo_hit?(@namespace_definitions_version)
571
580
  ensure_alive!
572
- count = Leptris::XML::FFI.leptris_element_namespace_count(@c_ptr)
581
+ count = Leptris::XML::FFI.leptris_element_namespace_count(c_ptr)
573
582
  result = count.times.map do |i|
574
- prefix = Leptris::XML::FFI.leptris_element_namespace_decl_prefix(@c_ptr, i)
575
- uri = Leptris::XML::FFI.leptris_element_namespace_decl_uri(@c_ptr, i)
583
+ prefix = Leptris::XML::FFI.leptris_element_namespace_decl_prefix(c_ptr, i)
584
+ uri = Leptris::XML::FFI.leptris_element_namespace_decl_uri(c_ptr, i)
576
585
  Leptris::XML::Namespace.new(self, uri, prefix: prefix)
577
586
  end
578
587
  if @document
@@ -612,7 +621,7 @@ class Leptris::XML::Element < Leptris::XML::Node
612
621
  # One C pass (TODO.perf/18): the child chain, per-kind
613
622
  # serialization, and the escape set all in one dispatch —
614
623
  # byte-identical to the Ruby loop below.
615
- return Leptris::XML::Native.fast_inner_xml(@c_ptr.address) if
624
+ return Leptris::XML::Native.fast_inner_xml(@c_address) if
616
625
  @native_fast
617
626
  children.map do |child|
618
627
  case child
@@ -646,9 +655,9 @@ class Leptris::XML::Element < Leptris::XML::Node
646
655
  end
647
656
  return Leptris::XML::Serialization.to_xml_element_unit(
648
657
  self, indent_text, indent: indent) if indent_text.is_a?(String)
649
- return Leptris::XML::Serialization.element_xml_expand_empty(@c_ptr) if expand_empty
658
+ return Leptris::XML::Serialization.element_xml_expand_empty(c_ptr) if expand_empty
650
659
  Leptris::XML::Serialization.to_xml(
651
- Leptris::XML::Serialization::ELEMENT_SERIALIZE_INTO, @c_ptr,
660
+ Leptris::XML::Serialization::ELEMENT_SERIALIZE_INTO, c_ptr,
652
661
  indent: indent, no_decl: no_decl, encoding: encoding)
653
662
  end
654
663
 
@@ -660,7 +669,7 @@ class Leptris::XML::Element < Leptris::XML::Node
660
669
  resolved_mode = mode || (exclusive ? Leptris::XML::FFI::C14N_MODE_EXCLUSIVE
661
670
  : Leptris::XML::FFI::C14N_MODE_CANONICAL)
662
671
  Leptris::XML::Serialization.canonicalize(
663
- Leptris::XML::FFI.method(:leptris_c14n_canonicalize_subtree_ex), @c_ptr,
672
+ Leptris::XML::FFI.method(:leptris_c14n_canonicalize_subtree_ex), c_ptr,
664
673
  version: version, mode: resolved_mode,
665
674
  inclusive_namespaces: inclusive_namespaces,
666
675
  with_comments: with_comments)
@@ -671,7 +680,7 @@ class Leptris::XML::Element < Leptris::XML::Node
671
680
  ensure_writable!
672
681
  Leptris::XML::FFI.check_status(
673
682
  Leptris::XML::FFI.leptris_element_add_namespace_definition(
674
- @c_ptr, prefix.to_s, href.to_s))
683
+ c_ptr, prefix.to_s, href.to_s))
675
684
  Leptris::XML::Namespace.new(self, href.to_s, prefix: prefix.nil? ? nil : prefix.to_s)
676
685
  end
677
686
  alias_method :add_namespace, :add_namespace_definition
@@ -679,14 +688,14 @@ class Leptris::XML::Element < Leptris::XML::Node
679
688
  def default_namespace=(href)
680
689
  ensure_writable!
681
690
  Leptris::XML::FFI.check_status(
682
- Leptris::XML::FFI.leptris_element_set_default_namespace(@c_ptr, href.to_s))
691
+ Leptris::XML::FFI.leptris_element_set_default_namespace(c_ptr, href.to_s))
683
692
  href
684
693
  end
685
694
 
686
695
  def remove_namespace_definition(prefix)
687
696
  ensure_writable!
688
697
  Leptris::XML::FFI.check_status(
689
- Leptris::XML::FFI.leptris_element_remove_namespace_definition(@c_ptr, prefix.to_s))
698
+ Leptris::XML::FFI.leptris_element_remove_namespace_definition(c_ptr, prefix.to_s))
690
699
  self
691
700
  end
692
701
  end
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Leptris::XML::Node
4
- attr_reader :c_ptr
4
+ # TODO.perf/19: @c_address is the canonical truth; the Pointer
5
+ # materializes only when actually read (most wrappers' Pointers
6
+ # never are — hot reads are memoized or address-based).
7
+ def c_ptr
8
+ @c_ptr ||= ::FFI::Pointer.new(@c_address)
9
+ end
5
10
 
6
11
  # Iterparse-yielded elements are owned by an IterationScope (the
7
12
  # internal lifetime/memoization authority) — the public #document
@@ -12,6 +17,7 @@ class Leptris::XML::Node
12
17
 
13
18
  def initialize(c_ptr, document, parent: nil, node_type: nil)
14
19
  @c_ptr = c_ptr
20
+ @c_address = c_ptr&.address
15
21
  @document = document
16
22
  @parent = parent
17
23
  # Structural-memo stamp (TODO.perf/13): a constructor-seeded
@@ -142,7 +148,7 @@ class Leptris::XML::Node
142
148
  def type
143
149
  return @node_type if @node_type
144
150
  ensure_alive!
145
- @node_type = Leptris::XML::FFI.leptris_node_get_type(@c_ptr)
151
+ @node_type = Leptris::XML::FFI.leptris_node_get_type(c_ptr)
146
152
  end
147
153
  alias_method :node_type, :type
148
154
 
@@ -165,7 +171,7 @@ class Leptris::XML::Node
165
171
  def digest(drop_ws: false)
166
172
  ensure_alive!
167
173
  Leptris::XML::FFI.leptris_node_digest(
168
- @c_ptr, drop_ws ? 1 : 0)
174
+ c_ptr, drop_ws ? 1 : 0)
169
175
  end
170
176
  def text?; type == Leptris::XML::FFI::NODE_TEXT; end
171
177
  def comment?; type == Leptris::XML::FFI::NODE_COMMENT; end
@@ -187,7 +193,7 @@ class Leptris::XML::Node
187
193
  return @parent
188
194
  end
189
195
  ensure_alive!
190
- ptr = Leptris::XML::FFI.leptris_node_parent(@c_ptr)
196
+ ptr = Leptris::XML::FFI.leptris_node_parent(c_ptr)
191
197
  result = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, @document)
192
198
  if @document
193
199
  @parent = result
@@ -205,7 +211,7 @@ class Leptris::XML::Node
205
211
  # unstamped derivation at most once per version.
206
212
  def unstamped_parent
207
213
  ensure_alive!
208
- ptr = Leptris::XML::FFI.leptris_node_parent(@c_ptr)
214
+ ptr = Leptris::XML::FFI.leptris_node_parent(c_ptr)
209
215
  ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, @document)
210
216
  end
211
217
 
@@ -274,7 +280,7 @@ class Leptris::XML::Node
274
280
 
275
281
  def line
276
282
  ensure_alive!
277
- Leptris::XML::FFI.leptris_node_line(@c_ptr)
283
+ Leptris::XML::FFI.leptris_node_line(c_ptr)
278
284
  end
279
285
 
280
286
  # Byte offset of the node's markup in its parse source (the '<'
@@ -283,19 +289,19 @@ class Leptris::XML::Node
283
289
  # documents >= 2 GiB.
284
290
  def byte_offset
285
291
  ensure_alive!
286
- Leptris::XML::FFI.leptris_node_byte_offset(@c_ptr)
292
+ Leptris::XML::FFI.leptris_node_byte_offset(c_ptr)
287
293
  end
288
294
 
289
295
  def <=>(other)
290
296
  return nil unless other.is_a?(Leptris::XML::Node)
291
297
  return nil unless @document == other.document
292
298
  ensure_alive!
293
- Leptris::XML::FFI.leptris_node_compare(@c_ptr, other.c_ptr)
299
+ Leptris::XML::FFI.leptris_node_compare(c_ptr, other.c_ptr)
294
300
  end
295
301
 
296
302
  def child
297
303
  ensure_alive!
298
- ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
304
+ ptr = Leptris::XML::FFI.leptris_node_first_child(c_ptr)
299
305
  return nil if ptr.null?
300
306
  Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
301
307
  end
@@ -310,7 +316,7 @@ class Leptris::XML::Node
310
316
  # TODO.perf/01 tail: one C pass constructs every binding
311
317
  # wrapper (class dispatch + ivars + identity cache) — the
312
318
  # per-child Ruby wrap frames disappear.
313
- nodes = Leptris::XML::Native.bulk_children(@document, @c_ptr.address)
319
+ nodes = Leptris::XML::Native.bulk_children(@document, @c_address)
314
320
  result = Leptris::XML::NodeSet.new(@document, nodes)
315
321
  if @document
316
322
  @children = result
@@ -319,7 +325,7 @@ class Leptris::XML::Node
319
325
  return result
320
326
  end
321
327
  parent = as_element_or_self
322
- pointers, kinds = Leptris::XML::FFI.fetch_children(@c_ptr)
328
+ pointers, kinds = Leptris::XML::FFI.fetch_children(c_ptr)
323
329
  nodes = Array.new(pointers.size) do |i|
324
330
  Leptris::XML::Node.wrap(pointers[i], @document,
325
331
  parent: parent, node_type: kinds[i])
@@ -337,7 +343,7 @@ class Leptris::XML::Node
337
343
  return @next_sibling
338
344
  end
339
345
  ensure_alive!
340
- ptr = Leptris::XML::FFI.leptris_node_next_sibling(@c_ptr)
346
+ ptr = Leptris::XML::FFI.leptris_node_next_sibling(c_ptr)
341
347
  result = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, @document, parent: @parent)
342
348
  if @document
343
349
  @next_sibling = result
@@ -352,7 +358,7 @@ class Leptris::XML::Node
352
358
  return @previous_sibling
353
359
  end
354
360
  ensure_alive!
355
- ptr = Leptris::XML::FFI.leptris_node_previous_sibling(@c_ptr)
361
+ ptr = Leptris::XML::FFI.leptris_node_previous_sibling(c_ptr)
356
362
  result = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, @document, parent: @parent)
357
363
  if @document
358
364
  @previous_sibling = result
@@ -368,7 +374,7 @@ class Leptris::XML::Node
368
374
  # Raw pointer scan: non-element siblings are typed with one C
369
375
  # call each — never wrapped, never cached — and the found
370
376
  # element carries the ELEMENT hint into the wrap.
371
- ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
377
+ ptr = Leptris::XML::FFI.leptris_node_first_child(c_ptr)
372
378
  result = nil
373
379
  until ptr.nil? || ptr.null?
374
380
  if Leptris::XML::FFI.leptris_node_get_type(ptr) ==
@@ -391,7 +397,7 @@ class Leptris::XML::Node
391
397
  # Element receivers: the element-only batch fetches pointers
392
398
  # without wrapping any text child; only the last is wrapped.
393
399
  if is_a?(Leptris::XML::Element)
394
- kids = Leptris::XML::FFI.fetch_element_children(@c_ptr)
400
+ kids = Leptris::XML::FFI.fetch_element_children(c_ptr)
395
401
  return nil if kids.empty?
396
402
  return Leptris::XML::Node.wrap(
397
403
  kids.last, @document, parent: self,
@@ -408,10 +414,10 @@ class Leptris::XML::Node
408
414
  # ELEMENT hint rides along). Other nodes keep the filter.
409
415
  result =
410
416
  if native_fast_children?
411
- Leptris::XML::Native.bulk_element_children(@document, @c_ptr.address)
417
+ Leptris::XML::Native.bulk_element_children(@document, @c_address)
412
418
  elsif is_a?(Leptris::XML::Element)
413
419
  parent = as_element_or_self
414
- Leptris::XML::FFI.fetch_element_children(@c_ptr).map do |ptr|
420
+ Leptris::XML::FFI.fetch_element_children(c_ptr).map do |ptr|
415
421
  Leptris::XML::Node.wrap(ptr, @document, parent: parent,
416
422
  node_type: Leptris::XML::FFI::NODE_ELEMENT)
417
423
  end
@@ -441,7 +447,7 @@ class Leptris::XML::Node
441
447
  def unlink
442
448
  ensure_writable!
443
449
  Leptris::XML::FFI.check_status(
444
- Leptris::XML::FFI.leptris_node_unlink(@c_ptr))
450
+ Leptris::XML::FFI.leptris_node_unlink(c_ptr))
445
451
  @parent = nil
446
452
  self
447
453
  end
@@ -468,7 +474,7 @@ class Leptris::XML::Node
468
474
  Leptris::XML::Node.wrap(node_ptr, document),
469
475
  entering == 1, depth)
470
476
  end
471
- Leptris::XML::FFI.leptris_node_visit(@c_ptr, visitor, nil)
477
+ Leptris::XML::FFI.leptris_node_visit(c_ptr, visitor, nil)
472
478
  self
473
479
  end
474
480
 
@@ -497,7 +503,7 @@ class Leptris::XML::Node
497
503
  return enum_for(:traverse) unless block_given?
498
504
  ensure_alive!
499
505
  error = nil
500
- self_address = @c_ptr.address
506
+ self_address = @c_address
501
507
  callback = ::FFI::Function.new(:int, [:pointer, :pointer], blocking: true) do |node_ptr, _|
502
508
  begin
503
509
  yield Leptris::XML::Node.wrap(node_ptr, @document)
@@ -508,7 +514,7 @@ class Leptris::XML::Node
508
514
  end
509
515
  end
510
516
  Leptris::XML::FFI.leptris_node_traverse(
511
- @c_ptr, Leptris::XML::FFI::TRAVERSE_POST_ORDER, callback, nil)
517
+ c_ptr, Leptris::XML::FFI::TRAVERSE_POST_ORDER, callback, nil)
512
518
  raise error if error
513
519
  self
514
520
  end
@@ -516,7 +522,7 @@ class Leptris::XML::Node
516
522
  def path
517
523
  return @path if memo_hit?(@path_version)
518
524
  ensure_alive!
519
- str_ptr = Leptris::XML::FFI.leptris_node_get_xpath(@c_ptr)
525
+ str_ptr = Leptris::XML::FFI.leptris_node_get_xpath(c_ptr)
520
526
  result = str_ptr.null? ? nil : Leptris::XML::FFI.read_owned_string(str_ptr)
521
527
  if @document
522
528
  @path = result
@@ -555,7 +561,7 @@ class Leptris::XML::Node
555
561
 
556
562
  def ==(other)
557
563
  return false unless other.is_a?(Leptris::XML::Node)
558
- @c_ptr == other.c_ptr
564
+ c_ptr == other.c_ptr
559
565
  end
560
566
 
561
567
  def inspect
@@ -4,7 +4,7 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
4
4
  def name
5
5
  return @name if memo_hit?(@name_version)
6
6
  ensure_alive!
7
- result = Leptris::XML::FFI.leptris_pi_node_get_target(@c_ptr)
7
+ result = Leptris::XML::FFI.leptris_pi_node_get_target(c_ptr)
8
8
  if @document
9
9
  @name = result
10
10
  @name_version = @document.version
@@ -17,7 +17,7 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
17
17
  return @content if memo_hit?(@content_version)
18
18
  ensure_alive!
19
19
  result = Leptris::XML::FFI.read_pi_data(
20
- Leptris::XML::FFI.leptris_pi_node_get_data(@c_ptr)).to_s
20
+ Leptris::XML::FFI.leptris_pi_node_get_data(c_ptr)).to_s
21
21
  if @document
22
22
  @content = result
23
23
  @content_version = @document.version
@@ -31,14 +31,14 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
31
31
  def target=(new_target)
32
32
  ensure_writable!
33
33
  Leptris::XML::FFI.check_status(
34
- Leptris::XML::FFI.leptris_pi_node_set_target(@c_ptr, new_target.to_s))
34
+ Leptris::XML::FFI.leptris_pi_node_set_target(c_ptr, new_target.to_s))
35
35
  new_target
36
36
  end
37
37
 
38
38
  def data=(new_data)
39
39
  ensure_writable!
40
40
  Leptris::XML::FFI.check_status(
41
- Leptris::XML::FFI.leptris_pi_node_set_data(@c_ptr, new_data.to_s))
41
+ Leptris::XML::FFI.leptris_pi_node_set_data(c_ptr, new_data.to_s))
42
42
  new_data
43
43
  end
44
44
 
@@ -48,7 +48,7 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
48
48
  # comes out.
49
49
  def unlink
50
50
  ensure_writable!
51
- rc = Leptris::XML::FFI.leptris_node_unlink(@c_ptr)
51
+ rc = Leptris::XML::FFI.leptris_node_unlink(c_ptr)
52
52
  if rc == Leptris::XML::FFI::LEPTRIS_ERROR_NOT_FOUND
53
53
  index = document_pi_index
54
54
  if index.negative?
@@ -56,7 +56,7 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
56
56
  "PI not found in its document — already removed?"
57
57
  end
58
58
  removed = @document.remove_pi(index)
59
- unless removed && removed.c_ptr == @c_ptr
59
+ unless removed && removed.c_ptr == c_ptr
60
60
  raise Leptris::XML::Error, "document-level PI removal missed"
61
61
  end
62
62
  @parent = nil
@@ -75,7 +75,7 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
75
75
  pi_index = -1
76
76
  @document.children.each do |child|
77
77
  pi_index += 1 if child.pi?
78
- return pi_index if child.c_ptr == @c_ptr
78
+ return pi_index if child.c_ptr == c_ptr
79
79
  end
80
80
  -1
81
81
  end
@@ -29,10 +29,31 @@ module Leptris::XML::Searchable
29
29
  nil # fall back to the string path — its error surface is the contract
30
30
  end
31
31
 
32
+ # CSS translation cache (TODO.perf/20): the selector -> XPath
33
+ # translation is a pure function of (selector, prefix); without
34
+ # it every css call re-ran the regex machinery before the
35
+ # compiled-expression cache could hit. Same bounded-LRU shape.
36
+ def self.css_expression(selector, prefix)
37
+ cache = (@css_expressions ||= {})
38
+ key = "#{prefix}#{selector}"
39
+ if (hit = cache[key])
40
+ cache.delete(key)
41
+ cache[key] = hit # LRU refresh
42
+ return hit
43
+ end
44
+ expr = Leptris::XML::CssToXPath.convert(selector, prefix: prefix)
45
+ cache.shift while cache.size >= COMPILED_CACHE_LIMIT
46
+ cache[key] = expr
47
+ expr
48
+ end
49
+
32
50
  def xpath(*paths)
33
51
  handler, ns, version = parse_search_args(paths)
34
52
  raise ArgumentError, "custom XPath handlers not supported" if handler
35
- expr = paths.join(" | ")
53
+ # Single-String shape (the common call): join would mint a
54
+ # fresh String per call even for one path.
55
+ expr = paths.one? && paths.first.is_a?(String) ? paths.first
56
+ : paths.join(" | ")
36
57
 
37
58
  doc_ptr = is_a?(Leptris::XML::Document) ? c_ptr : document.c_ptr
38
59
  context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
@@ -65,7 +86,10 @@ module Leptris::XML::Searchable
65
86
  def at_xpath(*paths)
66
87
  handler, ns, version = parse_search_args(paths)
67
88
  raise ArgumentError, "custom XPath handlers not supported" if handler
68
- expr = paths.join(" | ")
89
+ # Single-String shape (the common call): join would mint a
90
+ # fresh String per call even for one path.
91
+ expr = paths.one? && paths.first.is_a?(String) ? paths.first
92
+ : paths.join(" | ")
69
93
 
70
94
  doc_ptr = is_a?(Leptris::XML::Document) ? c_ptr : document.c_ptr
71
95
  context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
@@ -121,7 +145,7 @@ module Leptris::XML::Searchable
121
145
  # Nokogiri semantics: css is receiver-relative — absolute "//"
122
146
  # from a Document, descendant ".//" from elements and fragments.
123
147
  prefix = is_a?(Leptris::XML::Document) ? "//" : ".//"
124
- expr = args.map { |r| Leptris::XML::CssToXPath.convert(r, prefix: prefix) }
148
+ expr = args.map { |r| Leptris::XML::Searchable.css_expression(r, prefix) }
125
149
  .join(" | ")
126
150
  xpath(expr)
127
151
  end
@@ -132,7 +156,7 @@ module Leptris::XML::Searchable
132
156
  raise ArgumentError, "version: is XPath-only" if version
133
157
  raise ArgumentError, "custom CSS handlers not supported" if handler
134
158
  prefix = is_a?(Leptris::XML::Document) ? "//" : ".//"
135
- expr = args.map { |r| Leptris::XML::CssToXPath.convert(r, prefix: prefix) }
159
+ expr = args.map { |r| Leptris::XML::Searchable.css_expression(r, prefix) }
136
160
  .join(" | ")
137
161
  at_xpath(expr)
138
162
  end
@@ -6,7 +6,7 @@ class Leptris::XML::Text < Leptris::XML::Node
6
6
  def content
7
7
  return @content if memo_hit?(@content_version)
8
8
  ensure_alive!
9
- result = Leptris::XML::FFI.leptris_text_node_get_content(@c_ptr)
9
+ result = Leptris::XML::FFI.leptris_text_node_get_content(c_ptr)
10
10
  if @document
11
11
  @content = result
12
12
  @content_version = @document.version
@@ -17,7 +17,7 @@ class Leptris::XML::Text < Leptris::XML::Node
17
17
  def content=(new_content)
18
18
  ensure_writable!
19
19
  Leptris::XML::FFI.check_status(
20
- Leptris::XML::FFI.leptris_text_node_set_content(@c_ptr, new_content.to_s))
20
+ Leptris::XML::FFI.leptris_text_node_set_content(c_ptr, new_content.to_s))
21
21
  new_content
22
22
  end
23
23
  end
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.2
4
+ version: 1.9.174.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -87,6 +87,9 @@ files:
87
87
  - TODO.perf/16-at-xpath-single-result-seam.md
88
88
  - TODO.perf/17-precomputed-fast-path-flags.md
89
89
  - TODO.perf/18-inner-html-one-c-pass.md
90
+ - TODO.perf/19-lazy-node-pointer.md
91
+ - TODO.perf/20-css-translation-cache.md
92
+ - TODO.perf/21-key-memo-consult.md
90
93
  - TODO.restructure/01-constraint-compliance-audit.md
91
94
  - TODO.restructure/02-deep-copy-seam.md
92
95
  - TODO.restructure/03-evaluation-context-seam.md