leptris 1.9.174.6 → 1.9.174.7

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: 0ac7143c5a7730e13077c52448c2b90a7705959a4fe458e7c29e7782d7ee6df1
4
- data.tar.gz: 23d3530348907214d341704fa7b098728fa8b677422c8f595e12ae33c2b88ff3
3
+ metadata.gz: 2b55134beeb4963b45dab45f2702b840c8db5e4f1c5e282622f5e4c4b5f55b1e
4
+ data.tar.gz: 92278400c3c9a6ff0450a151583a9edd383677b621d6c95bdbd5286d0ba0b8a1
5
5
  SHA512:
6
- metadata.gz: 6395fec9ab3701df1b77f32bbe950feff8b918b9f79374b5c107d9e282bb5e9e5fdd91c152dc46526fc201910e69b7c57e8ee968d11e222be3cfdd346c6c7ab8
7
- data.tar.gz: a55e0b100034203af05855e8881fbfa161d30d6043b3831080ebff8d0f9ca658ca74fd11e1d30aef6887b3108493e4f690f0e08133ca4cf3bca36399cbcacdc4
6
+ metadata.gz: 3e476a9c2aaecd83069e118a4a7c1aa4ed141df2c853b238f4eb745b916be151ce46b0a5c29686f3d481d2bb816fe4c6f4744df7e76835bfe53ccc82c998134d
7
+ data.tar.gz: af8d33a0aaa87d762405979bcbc08be5af416e8dac2de924027eb900184df6740fcc4a8ef30e533becf131db08c2502a9dfc4a634b9678d9a87b5fa0513af212
data/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ 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.7] - 2026-09-16
9
+
10
+ ### Changed
11
+
12
+ - **Namespace-bound xpath on the compiled path (TODO.perf/29)**:
13
+ the ns branch rides the compiled-expression cache (the handle
14
+ is ns-independent) plus the cached ns set through
15
+ `XPath#eval_ns_ptrs`, then the eager materializer — measured
16
+ (load ~16): repeat ns-xpath 7.9µs → 3.1µs (2.5x).
17
+ - **dup + element-child faces (TODO.perf/30)**: `Element#dup`
18
+ runs engine create + handle + element_copy + rooted wrap in
19
+ one C dispatch (the namespace-lift decision stays in Ruby) —
20
+ ns-bearing dup 37.0µs → 9.6µs (3.9x); `first/last_element_child`
21
+ answer in one C walk each (the FFI paths paid O(N) scans).
22
+
8
23
  ## [1.9.174.6] - 2026-09-15
9
24
 
10
25
  ### Changed
@@ -0,0 +1,23 @@
1
+ # 29 — Namespace-bound xpath on the compiled + eager path
2
+
3
+ Status: DONE (1.9.174.7)
4
+
5
+ TODO.perf/15 excluded namespace-bound evaluations from the
6
+ compiled-expression cache — they still pay the engine's string
7
+ parse per call (~2-3µs) on top of an already-cached ns set. The
8
+ engine has leptris_xpath_compiled_eval_ns; route the ns branch
9
+ of xpath/at_xpath through the compiled cache (the handle is
10
+ ns-independent — compile once) and the cached ns set, then let
11
+ the eager materializer (TODO.perf/22) take the result.
12
+
13
+ Gates: ns-bound results identical (prefix resolution, missing
14
+ prefixes raising as today); the version-pinned path untouched;
15
+ suite both modes.
16
+
17
+ ## Outcome (1.9.174.7)
18
+
19
+ The ns branch of xpath/at_xpath rides the compiled-expression
20
+ cache (the handle is ns-independent) + the cached ns set via
21
+ XPath#eval_ns_ptrs (leptris_xpath_compiled_eval_ns); results
22
+ flow into the eager materializer. Measured (load ~16): repeat
23
+ ns-xpath 7.9µs -> 3.1µs (2.5x).
@@ -0,0 +1,31 @@
1
+ # 30 — dup one-dispatch + first/last element child faces
2
+
3
+ Status: DONE (1.9.174.7)
4
+
5
+ Node#dup runs create + FFI element_copy + root= (three dispatches
6
+ plus the lift-guard walk) for an engine-copier op that is already
7
+ 2.3x — bind the whole copy: engine create + handle + element_copy
8
+ + root wrap in one face. And the element-child readers pay O(N):
9
+ first_element_child scans siblings with two FFI calls per
10
+ non-element (text-heavy parents), last_element_children batches
11
+ ALL children to keep one — one C walk each answers in a single
12
+ dispatch.
13
+
14
+ Gates: dup semantics (comment/PI children, namespaces — the
15
+ copy_of seam contract #696/#721/#812); first/last on mixed
16
+ content, empty elements, document receivers; suite both modes.
17
+
18
+ ## Outcome (1.9.174.7)
19
+
20
+ Native.copy_binding_element: engine create + lifetime handle +
21
+ document wrapper + element_copy + rooted+memo-seeded wrap in one
22
+ dispatch; the namespace-lift decision stays in Ruby (skip_
23
+ adoption_lift? + lift — the copy_of seam's #696/#721/#812
24
+ semantics), and the return shape matches copy_of's contract (the
25
+ copied root ELEMENT — my first version returned the document and
26
+ the probe caught it). Native.first/last_element_child: one C
27
+ walk each (the FFI scan paid two calls per skipped sibling; the
28
+ batch fetch materialized every child to keep one). Measured
29
+ (load ~16): ns-bearing element dup 37.0µs -> 9.6µs (3.9x);
30
+ first_element_child over 1000 mixed children 98ns (memo-hit
31
+ shape), last 3.2µs cold-walk.
@@ -93,12 +93,14 @@ typedef void (*visit_cb_fn)(void *, void *, int, int);
93
93
  typedef void (*visit_fn)(void *, visit_cb_fn, void *);
94
94
  typedef void (*free_str_fn)(void *);
95
95
  typedef const char *(*node_xpath_fn)(void *);
96
+ typedef void *(*elem_copy_fn)(void *, void *);
96
97
  static set_str_fn f_elem_set_name, f_elem_set_text, f_text_set_content;
97
98
  static node_unlink_fn f_node_unlink;
98
99
  static traverse_fn f_node_traverse;
99
100
  static visit_fn f_node_visit;
100
101
  static free_str_fn f_free_str;
101
102
  static node_xpath_fn f_node_xpath;
103
+ static elem_copy_fn f_elem_copy;
102
104
  static set_root_fn f_set_root;
103
105
  static doc_free_fn f_doc_free;
104
106
 
@@ -117,6 +119,8 @@ static void resolve_binding_classes(void);
117
119
  static VALUE binding_cache_of(VALUE document);
118
120
  static VALUE binding_klass_for(int kind);
119
121
  static VALUE c_iteration_scope;
122
+ static VALUE c_b_document, c_b_freed, c_ffi_pointer;
123
+ static ID id_ptr_new, id_freed_new, id_alive;
120
124
 
121
125
  #define NT_ELEMENT 0
122
126
 
@@ -216,6 +220,7 @@ static void resolve_symbols(const char *lib_path)
216
220
  f_node_visit = (visit_fn)lib_sym(h, "leptris_node_visit");
217
221
  f_free_str = (free_str_fn)lib_sym(h, "leptris_free_string");
218
222
  f_node_xpath = (node_xpath_fn)lib_sym(h, "leptris_node_get_xpath");
223
+ f_elem_copy = (elem_copy_fn)lib_sym(h, "leptris_element_copy");
219
224
  f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
220
225
  f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
221
226
  if (!f_elem_name || !f_text_content || !f_attr ||
@@ -228,7 +233,7 @@ static void resolve_symbols(const char *lib_path)
228
233
  !f_pi_data || !f_elem_set_name || !f_elem_set_text ||
229
234
  !f_text_set_content || !f_node_unlink ||
230
235
  !f_node_traverse || !f_node_visit || !f_free_str ||
231
- !f_node_xpath ||
236
+ !f_node_xpath || !f_elem_copy ||
232
237
  !f_doc_free ||
233
238
  !f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
234
239
  !f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
@@ -915,6 +920,72 @@ static VALUE nf_fast_pi_data(VALUE self, VALUE addr)
915
920
  return s ? rb_utf8_str_new_cstr(s) : Qnil;
916
921
  }
917
922
 
923
+ /* Shared construction for address-walk faces (TODO.perf/30):
924
+ * cache lookup + kind-dispatched binding wrapper. */
925
+ static VALUE wrap_addr_node(VALUE document, void *ptr)
926
+ {
927
+ VALUE cache, key, node;
928
+ int kind;
929
+
930
+ kind = f_node_type(ptr);
931
+ cache = binding_cache_of(document);
932
+ key = ULL2NUM((uint64_t)(uintptr_t)ptr);
933
+ node = rb_hash_aref(cache, key);
934
+ if (NIL_P(node)) {
935
+ node = rb_obj_alloc(binding_klass_for(kind));
936
+ rb_iv_set(node, "@c_address", key);
937
+ rb_iv_set(node, "@document", document);
938
+ rb_iv_set(node, "@parent", Qnil);
939
+ if (rb_obj_class(document) == c_iteration_scope) {
940
+ rb_iv_set(node, "@structure_memoizable", Qfalse);
941
+ rb_iv_set(node, "@native_fast", Qfalse);
942
+ rb_iv_set(node, "@pub_document", Qnil);
943
+ } else {
944
+ rb_iv_set(node, "@structure_memoizable", Qtrue);
945
+ rb_iv_set(node, "@native_fast", Qtrue);
946
+ rb_iv_set(node, "@pub_document", document);
947
+ }
948
+ rb_iv_set(node, "@addr_reads_fast", Qtrue);
949
+ rb_iv_set(node, "@node_type", INT2FIX(kind));
950
+ rb_hash_aset(cache, key, node);
951
+ }
952
+ return node;
953
+ }
954
+
955
+ /* First element child in one C walk (text-heavy parents paid two
956
+ * FFI calls per skipped sibling). */
957
+ static VALUE nf_first_element_child(VALUE self, VALUE document,
958
+ VALUE addr)
959
+ {
960
+ void *p;
961
+
962
+ (void)self;
963
+ resolve_binding_classes();
964
+ for (p = f_first_child((void *)(uintptr_t)NUM2ULL(addr));
965
+ p != NULL; p = f_next_sibling(p)) {
966
+ if (f_node_type(p) == NT_ELEMENT)
967
+ return wrap_addr_node(document, p);
968
+ }
969
+ return Qnil;
970
+ }
971
+
972
+ /* Last element child in one C walk (the batch fetch materialized
973
+ * every child to keep one). */
974
+ static VALUE nf_last_element_child(VALUE self, VALUE document,
975
+ VALUE addr)
976
+ {
977
+ void *p, *last = NULL;
978
+
979
+ (void)self;
980
+ resolve_binding_classes();
981
+ for (p = f_first_child((void *)(uintptr_t)NUM2ULL(addr));
982
+ p != NULL; p = f_next_sibling(p)) {
983
+ if (f_node_type(p) == NT_ELEMENT)
984
+ last = p;
985
+ }
986
+ return last ? wrap_addr_node(document, last) : Qnil;
987
+ }
988
+
918
989
  /* Owned string: copy + free through the engine's seam (the
919
990
  * read_owned_string protocol). */
920
991
  static VALUE nf_fast_path(VALUE self, VALUE addr)
@@ -1737,6 +1808,48 @@ static const rb_data_type_t dh_type = {
1737
1808
  static VALUE c_doc_handle;
1738
1809
  static ID id_iv_doc_handle, id_freed_new, id_alive;
1739
1810
 
1811
+ /* Node#dup in one dispatch (TODO.perf/30): engine create +
1812
+ * lifetime handle + document wrapper + element_copy + the copy
1813
+ * wrapped-and-rooted (memo-seeded). The namespace lift stays a
1814
+ * Ruby decision (skip_adoption_lift? — the copy_of seam's
1815
+ * semantics, #696/#721/#812). */
1816
+ static VALUE nf_copy_binding_element(VALUE self, VALUE document,
1817
+ VALUE addr)
1818
+ {
1819
+ void *doc, *copy;
1820
+ VALUE new_doc, doc_addr, handle, freed, root;
1821
+ struct doc_handle *h;
1822
+
1823
+ (void)self;
1824
+ resolve_binding_classes();
1825
+ doc = f_doc_create();
1826
+ if (!doc)
1827
+ return Qnil;
1828
+ doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
1829
+ new_doc = rb_obj_alloc(c_b_document);
1830
+ rb_iv_set(new_doc, "@c_ptr",
1831
+ rb_funcall(c_ffi_pointer, id_ptr_new, 1, doc_addr));
1832
+ rb_iv_set(new_doc, "@c_address", doc_addr);
1833
+ freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
1834
+ rb_iv_set(new_doc, "@freed", freed);
1835
+ rb_iv_set(new_doc, "@readonly", Qfalse);
1836
+ rb_iv_set(new_doc, "@version", INT2FIX(0));
1837
+ handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
1838
+ &dh_type, h);
1839
+ h->doc = doc;
1840
+ rb_ivar_set(new_doc, id_iv_doc_handle, handle);
1841
+
1842
+ copy = f_elem_copy((void *)(uintptr_t)NUM2ULL(addr), doc);
1843
+ if (!copy) {
1844
+ h->doc = NULL; /* the handle releases the empty doc */
1845
+ return Qnil;
1846
+ }
1847
+ root = wrap_addr_node(new_doc, copy);
1848
+ rb_ivar_set(new_doc, rb_intern("@root"), root);
1849
+ rb_ivar_set(new_doc, rb_intern("@root_version"), INT2FIX(0));
1850
+ return new_doc;
1851
+ }
1852
+
1740
1853
  static VALUE nf_doc_handle_attach(VALUE self, VALUE document)
1741
1854
  {
1742
1855
  struct doc_handle *h;
@@ -1952,6 +2065,12 @@ void Init_native(void)
1952
2065
  nf_fast_pi_data, 1);
1953
2066
  rb_define_module_function(m_native, "fast_path",
1954
2067
  nf_fast_path, 1);
2068
+ rb_define_module_function(m_native, "first_element_child",
2069
+ nf_first_element_child, 2);
2070
+ rb_define_module_function(m_native, "last_element_child",
2071
+ nf_last_element_child, 2);
2072
+ rb_define_module_function(m_native, "copy_binding_element",
2073
+ nf_copy_binding_element, 2);
1955
2074
  rb_define_module_function(m_native, "fast_inner_xml",
1956
2075
  nf_fast_inner_xml, 1);
1957
2076
  rb_define_module_function(m_native, "doc_handle_attach",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.174.6"
4
+ VERSION = "1.9.174.7"
5
5
  end
@@ -497,9 +497,21 @@ class Leptris::XML::Element < Leptris::XML::Node
497
497
 
498
498
  # Deep copy in a NEW document via Document.copy_of (the single
499
499
  # copy seam — every child kind and namespace survives,
500
- # #696/#721/#812).
500
+ # #696/#721/#812). TODO.perf/30: one C dispatch when the ext
501
+ # is loaded — the namespace-lift decision stays here.
501
502
  def dup
502
503
  ensure_alive!
504
+ if @addr_reads_fast
505
+ doc = Leptris::XML::Native.copy_binding_element(
506
+ @document, @c_address)
507
+ unless doc.nil?
508
+ copied = doc.root
509
+ unless Leptris::XML::Element.skip_adoption_lift?(copied)
510
+ Leptris::XML::Element.lift_namespaces_for_adoption(copied, {})
511
+ end
512
+ return copied
513
+ end
514
+ end
503
515
  Leptris::XML::Document.copy_of(self)
504
516
  end
505
517
  alias_method :clone, :dup
@@ -390,6 +390,17 @@ class Leptris::XML::Node
390
390
  def first_element_child
391
391
  return @first_element_child if memo_hit?(@first_element_child_version)
392
392
  ensure_alive!
393
+ # TODO.perf/30: one C walk (the FFI scan paid two calls per
394
+ # skipped non-element sibling).
395
+ if @addr_reads_fast
396
+ result = Leptris::XML::Native.first_element_child(
397
+ @document, @c_address)
398
+ if @document
399
+ @first_element_child = result
400
+ @first_element_child_version = @document.version
401
+ end
402
+ return result
403
+ end
393
404
  # Raw pointer scan: non-element siblings are typed with one C
394
405
  # call each — never wrapped, never cached — and the found
395
406
  # element carries the ELEMENT hint into the wrap.
@@ -413,8 +424,12 @@ class Leptris::XML::Node
413
424
  end
414
425
 
415
426
  def last_element_child
416
- # Element receivers: the element-only batch fetches pointers
417
- # without wrapping any text child; only the last is wrapped.
427
+ # TODO.perf/30: one C walk — the batch fetch materialized
428
+ # every child pointer to keep one.
429
+ if @addr_reads_fast
430
+ return Leptris::XML::Native.last_element_child(
431
+ @document, @c_address)
432
+ end
418
433
  if is_a?(Leptris::XML::Element)
419
434
  kids = Leptris::XML::FFI.fetch_element_children(c_ptr)
420
435
  return nil if kids.empty?
@@ -600,6 +615,23 @@ class Leptris::XML::Node
600
615
  # non-element kinds rebuild by value — #161).
601
616
  def dup
602
617
  ensure_alive!
618
+ # TODO.perf/30: elements copy in one C dispatch (engine
619
+ # create + handle + element_copy + rooted wrap). The
620
+ # namespace lift stays a Ruby decision — the copy_of seam's
621
+ # semantics (#696/#721/#812) run below when needed.
622
+ if element? && @addr_reads_fast
623
+ doc = Leptris::XML::Native.copy_binding_element(
624
+ @document, @c_address)
625
+ unless doc.nil?
626
+ # copy_of's contract returns the copied ROOT (the last
627
+ # expression of the root= assignment), not the document.
628
+ copied = doc.root
629
+ unless Leptris::XML::Element.skip_adoption_lift?(copied)
630
+ Leptris::XML::Element.lift_namespaces_for_adoption(copied, {})
631
+ end
632
+ return copied
633
+ end
634
+ end
603
635
  Leptris::XML::Document.copy_of(self)
604
636
  end
605
637
  alias_method :clone, :dup
@@ -69,7 +69,16 @@ module Leptris::XML::Searchable
69
69
  doc_ptr, context_ptr, expr,
70
70
  Leptris::XML::Searchable.xpath_version_code(version), nil)
71
71
  elsif ns && !ns.empty?
72
- xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
72
+ # TODO.perf/29: the compiled handle is ns-independent —
73
+ # ride the cache and the cached ns set instead of the
74
+ # engine's string parse per call.
75
+ if (compiled = Leptris::XML::Searchable.compiled_expression(expr))
76
+ Leptris::XML::FFI.with_ns_set(ns) do |set|
77
+ compiled.eval_ns_ptrs(doc_ptr, context_ptr, set)
78
+ end
79
+ else
80
+ xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
81
+ end
73
82
  elsif (compiled = Leptris::XML::Searchable.compiled_expression(expr))
74
83
  compiled.eval_ptrs(doc_ptr, context_ptr)
75
84
  else
@@ -105,7 +114,16 @@ module Leptris::XML::Searchable
105
114
  doc_ptr, context_ptr, expr,
106
115
  Leptris::XML::Searchable.xpath_version_code(version), nil)
107
116
  elsif ns && !ns.empty?
108
- xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
117
+ # TODO.perf/29: the compiled handle is ns-independent —
118
+ # ride the cache and the cached ns set instead of the
119
+ # engine's string parse per call.
120
+ if (compiled = Leptris::XML::Searchable.compiled_expression(expr))
121
+ Leptris::XML::FFI.with_ns_set(ns) do |set|
122
+ compiled.eval_ns_ptrs(doc_ptr, context_ptr, set)
123
+ end
124
+ else
125
+ xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
126
+ end
109
127
  elsif (compiled = Leptris::XML::Searchable.compiled_expression(expr))
110
128
  compiled.eval_ptrs(doc_ptr, context_ptr)
111
129
  else
@@ -40,6 +40,14 @@ class Leptris::XML::XPath
40
40
  @handle, doc_ptr, context_ptr)
41
41
  end
42
42
 
43
+ # Namespace-bound evaluation against the compiled handle
44
+ # (TODO.perf/29): the handle is ns-independent — compile once,
45
+ # bind per call through the cached ns set.
46
+ def eval_ns_ptrs(doc_ptr, context_ptr, ns_set_ptr)
47
+ Leptris::XML::FFI.leptris_xpath_compiled_eval_ns(
48
+ @handle, doc_ptr, context_ptr, ns_set_ptr)
49
+ end
50
+
43
51
  def initialize(expression, handle, version_code = nil)
44
52
  @expression = expression
45
53
  @handle = handle
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.6
4
+ version: 1.9.174.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -97,6 +97,8 @@ files:
97
97
  - TODO.perf/26-post-mutation-memo-seeding.md
98
98
  - TODO.perf/27-c-yield-traversal.md
99
99
  - TODO.perf/28-address-fills.md
100
+ - TODO.perf/29-ns-xpath-compiled.md
101
+ - TODO.perf/30-copy-and-element-child-faces.md
100
102
  - TODO.restructure/01-constraint-compliance-audit.md
101
103
  - TODO.restructure/02-deep-copy-seam.md
102
104
  - TODO.restructure/03-evaluation-context-seam.md