leptris 1.9.379.2-aarch64-linux → 1.9.379.3-aarch64-linux

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: b235f58e7de7780990dc11862d1bce77ee33220cb3f0ee61e9124ded829f080d
4
- data.tar.gz: be5340e4be9d94703638eb74f13f80ccef66f5a8eac3ddb28f9fd9857ef1db95
3
+ metadata.gz: 1eb86fa70776ca573401c3ce7ce1527c99cb53f77de2ac5cbeeb885b411eca1f
4
+ data.tar.gz: e646310bc3c27e611d3a214ca2066c25333a8af911b005a6776c6c813661a6ec
5
5
  SHA512:
6
- metadata.gz: 1423256a4aa9d071f1441beb8cd48bbc0a5733d8d4bf7363f7135f9872dba50edae6b30e1ddc4353665fa06502087d07e5451a69e8be130cfd05327a33b42bc6
7
- data.tar.gz: c59457e85190260d802ecf401d088c0f789235549c20350310d1dfd013dd370d556719507c16673818b2cbf07d2f532e4df4471125090ca1c4bcaa172cfa8d7d
6
+ metadata.gz: c3cc002aa4ebd2122e636e1e6aff445ffbcab4acf5af84272e0e0d0d1082d6f8b665cd76291383aed71581eeedf5f47bebc71b04ab1c42ded249c7c6e63ff2e9
7
+ data.tar.gz: 22e6b9b866b6652786edb988295dddd97c7d7ab7b9e41c67cb51f07e9853f29c5315c2f88239536fc9ff57ccb22d8637c443939df66862549049d8bdbe0fb7e7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## [1.9.379.3] - 2026-10-22
2
+
3
+ ### Fixed
4
+
5
+ - **attribute_nodes desync after interleaved bare-name writes
6
+ (leptris/leptris-ruby#526, PR #533)**: a cold bare-name `[]` read
7
+ nil'd the attribute_nodes/keys/values memos but restamped only the
8
+ attributes version, so the sibling faces served a stamped nil.
9
+ Dropped faces now drop their stamps; a `key?` partial-fill hole
10
+ found by the same 2000-trial sweep is fixed too.
11
+
12
+ ### Added — native layer
13
+
14
+ - **Bulk XPath materialization (PR #531)**:
15
+ `xpath_result_rows_bulk` returns the whole node-set as registered
16
+ NativeNodes in one C crossing.
17
+
1
18
  ## [1.9.379.2] - 2026-10-21
2
19
 
3
20
  ### Added — native layer
@@ -1925,6 +1925,89 @@ static VALUE nf_at_xpath_first(VALUE self, VALUE document,
1925
1925
  return node;
1926
1926
  }
1927
1927
 
1928
+ /* ---- Bulk XPath rows as registered NativeNodes (#529) ----------
1929
+ * The consumer's iterate-every-row lane pays ONE FFI crossing and
1930
+ * one Ruby wrapper mint PER result row. This face materializes the
1931
+ * whole nodeset in a single call: every real tree node mints
1932
+ * through the document's registered wrapper klasses (#340) and the
1933
+ * native identity cache — the same mint the walk and create faces
1934
+ * use, cache-checked so one address is one Ruby object — so rows
1935
+ * arrive already carrying the consumer's contract, with no
1936
+ * Ruby-visible per-row call. Entries the consumer-klass path can't
1937
+ * represent (synthetic attribute rows, sequence-carrier text,
1938
+ * namespaces, unregistered documents) keep the shared per-entry
1939
+ * binding materializer, exactly what the eager path minted before.
1940
+ * Nodesets free the handle here (TODO.perf/22 eager discipline);
1941
+ * non-nodeset results return Qnil UNFREED — the caller keeps the
1942
+ * scalar path, whose wrap owns the free. */
1943
+ static VALUE nf_xpath_result_rows_bulk(VALUE self, VALUE document,
1944
+ VALUE result_addr)
1945
+ {
1946
+ void *result = (void *)(uintptr_t)NUM2ULL(result_addr);
1947
+ void **buf;
1948
+ int *kinds;
1949
+ size_t scount;
1950
+ long i, n;
1951
+ VALUE cache, out;
1952
+
1953
+ (void)self;
1954
+ resolve_binding_classes();
1955
+ if (f_xp_type(result) != 0) /* XPATH_NODESET only */
1956
+ return Qnil;
1957
+
1958
+ scount = f_xp_count(result);
1959
+ if (scount == 0) {
1960
+ f_xp_free(result);
1961
+ return rb_ary_new2(0);
1962
+ }
1963
+ buf = ruby_xmalloc(scount * sizeof(void *));
1964
+ kinds = ruby_xmalloc(scount * sizeof(int));
1965
+ /* The batch accessor compacts (invalid pointers skip) — n is
1966
+ * the row count AND the index space the name/value accessors
1967
+ * answer on. */
1968
+ scount = f_xp_nodes_ex(result, buf, kinds, scount);
1969
+ n = (long)scount;
1970
+ out = rb_ary_new_capa(n);
1971
+ cache = binding_cache_of(document);
1972
+ for (i = 0; i < n; i++) {
1973
+ void *ptr = buf[i];
1974
+ int tag;
1975
+ VALUE node, key;
1976
+
1977
+ if (ptr == NULL)
1978
+ continue;
1979
+ /* The unified tag space (#477) classifies every entry from
1980
+ * the node word itself: 0..5 real tree kinds — the same
1981
+ * ints the doc-klass map is indexed by — 6/7/8 synthetic
1982
+ * attribute/namespace/text. */
1983
+ tag = (int)f_node_type(ptr);
1984
+ key = ULL2NUM((uint64_t)(uintptr_t)ptr);
1985
+ if (tag >= WS_NODE_ELEMENT && tag <= WS_NODE_DOCTYPE) {
1986
+ VALUE mk = doc_klass_for_kind(document, tag);
1987
+ if (!NIL_P(mk)) {
1988
+ node = rb_hash_aref(native_cache_of(document), key);
1989
+ if (NIL_P(node))
1990
+ node = mint_consumer_node(mk, document, ptr);
1991
+ rb_ary_push(out, node);
1992
+ continue;
1993
+ }
1994
+ }
1995
+ node = materialize_xp_entry(document, cache, result, ptr,
1996
+ kinds[i], (int)i);
1997
+ if (node == m_native_sentinel) {
1998
+ /* Rare kind the entry materializer defers (namespace
1999
+ * rows): mint the plain native twin instead of leaking
2000
+ * the module sentinel into consumer-visible rows. */
2001
+ node = wrap_new(document, ptr);
2002
+ }
2003
+ rb_ary_push(out, node);
2004
+ }
2005
+ ruby_xfree(buf);
2006
+ ruby_xfree(kinds);
2007
+ f_xp_free(result);
2008
+ return out;
2009
+ }
2010
+
1928
2011
  /* ---- Bulk attribute materialization (TODO.perf/10) ---------------
1929
2012
  * Element#attributes / #keys / #values walk the attribute list
1930
2013
  * one FFI call per row (name + value). One C pass builds the
@@ -2444,6 +2527,92 @@ static VALUE nf_snapshot_rows(VALUE self, VALUE document, VALUE addr)
2444
2527
  return out;
2445
2528
  }
2446
2529
 
2530
+ /* ---- Columnar preorder walk (leptris-ruby#527) -------------------
2531
+ * The moxml cold walk crossed the boundary once per element
2532
+ * (children_read + name_read per node, ~23% of cold-walk
2533
+ * samples). walk_columnar returns a WHOLE subtree's walk data in
2534
+ * ONE crossing: [names, handles, depths] — three parallel arrays,
2535
+ * preorder, the addressed node at depth 0.
2536
+ *
2537
+ * names: interned (frozen, shared) fstrings for elements, nil for
2538
+ * the child kinds with no name (text/comment/cdata/pi) — the same
2539
+ * discipline as nn_name, so repeat names allocate zero.
2540
+ * handles: NativeNodes (TypedData path) minted through the
2541
+ * doc-klass map (#340) when the document registered wrapper
2542
+ * klasses, else the kind-resolved NativeNode subclasses — every
2543
+ * mint lands in the document's native-cache identity, so handles
2544
+ * here ARE the children_read/visit wrappers and re-walks return
2545
+ * identical objects.
2546
+ * depths: root-relative ints; document/doctype receivers walk
2547
+ * their children at depth 0 (same entry rule as snapshot_rows). */
2548
+ static VALUE columnar_node_for(VALUE document, VALUE owner_klass,
2549
+ void *ptr, int kind)
2550
+ {
2551
+ VALUE key = ULL2NUM((uint64_t)(uintptr_t)ptr);
2552
+ VALUE cache = native_cache_of(document);
2553
+ VALUE node = rb_hash_aref(cache, key);
2554
+ struct native_node *n;
2555
+
2556
+ if (!NIL_P(node)) return node;
2557
+ VALUE mk = doc_klass_for_kind(document, kind);
2558
+ if (!NIL_P(mk))
2559
+ return mint_consumer_node(mk, document, ptr);
2560
+ node = nn_allocate(klass_for_kind(owner_klass, kind));
2561
+ TypedData_Get_Struct(node, struct native_node, &nn_type, n);
2562
+ n->ptr = ptr;
2563
+ n->document = document;
2564
+ rb_hash_aset(cache, key, node);
2565
+ return node;
2566
+ }
2567
+
2568
+ static void walk_columnar_impl(VALUE document, VALUE owner_klass,
2569
+ void *node, int depth, VALUE names,
2570
+ VALUE handles, VALUE depths)
2571
+ {
2572
+ int kind = f_node_type(node);
2573
+ void *c;
2574
+
2575
+ if (kind == WS_NODE_ELEMENT) {
2576
+ const char *name = f_elem_name(node);
2577
+ rb_ary_push(names, name
2578
+ ? rb_enc_interned_str(name, strlen(name),
2579
+ rb_utf8_encoding())
2580
+ : Qnil);
2581
+ rb_ary_push(handles, columnar_node_for(document, owner_klass,
2582
+ node, kind));
2583
+ rb_ary_push(depths, INT2NUM(depth));
2584
+ for (c = f_first_child(node); c; c = f_next_sibling(c)) {
2585
+ walk_columnar_impl(document, owner_klass, c, depth + 1,
2586
+ names, handles, depths);
2587
+ }
2588
+ } else if (kind == WS_NODE_TEXT || kind == WS_NODE_COMMENT ||
2589
+ kind == WS_NODE_CDATA || kind == WS_NODE_PI) {
2590
+ rb_ary_push(names, Qnil);
2591
+ rb_ary_push(handles, columnar_node_for(document, owner_klass,
2592
+ node, kind));
2593
+ rb_ary_push(depths, INT2NUM(depth));
2594
+ } else if (kind == WS_NODE_DOCTYPE || kind == 9 /* DOCUMENT */) {
2595
+ for (c = f_first_child(node); c; c = f_next_sibling(c)) {
2596
+ walk_columnar_impl(document, owner_klass, c, depth,
2597
+ names, handles, depths);
2598
+ }
2599
+ }
2600
+ }
2601
+
2602
+ static VALUE nf_walk_columnar(VALUE self, VALUE root)
2603
+ {
2604
+ struct native_node *n;
2605
+ VALUE names, handles, depths;
2606
+ (void)self;
2607
+ TypedData_Get_Struct(root, struct native_node, &nn_type, n);
2608
+ names = rb_ary_new();
2609
+ handles = rb_ary_new();
2610
+ depths = rb_ary_new();
2611
+ walk_columnar_impl(n->document, rb_obj_class(root), n->ptr, 0,
2612
+ names, handles, depths);
2613
+ return rb_ary_new3(3, names, handles, depths);
2614
+ }
2615
+
2447
2616
  /* ---- Struct plan executor (moxml "miles faster" round) ------------
2448
2617
  * The consumer's shape compiles to spec: name (interned String) ->
2449
2618
  * [Struct class, attrs Hash (name -> slot Symbol), text slot Symbol
@@ -3653,6 +3822,8 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
3653
3822
  nf_at_xpath_first, 2);
3654
3823
  rb_define_module_function(m_native, "materialize_xpath",
3655
3824
  nf_materialize_xpath, 2);
3825
+ rb_define_module_function(m_native, "xpath_result_rows_bulk",
3826
+ nf_xpath_result_rows_bulk, 2);
3656
3827
  rb_define_module_function(m_native, "set_binding_name",
3657
3828
  nf_set_binding_name, 3);
3658
3829
  rb_define_module_function(m_native, "set_binding_text",
@@ -3697,6 +3868,8 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
3697
3868
  nf_snapshot_subtree, 2);
3698
3869
  rb_define_module_function(m_native, "snapshot_rows",
3699
3870
  nf_snapshot_rows, 2);
3871
+ rb_define_module_function(m_native, "walk_columnar",
3872
+ nf_walk_columnar, 1);
3700
3873
  rb_define_module_function(m_native, "attribute_pairs",
3701
3874
  nf_attribute_pairs, 2);
3702
3875
  rb_define_module_function(m_native, "plan_structs",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.379.2"
4
+ VERSION = "1.9.379.3"
5
5
  end
@@ -117,6 +117,14 @@ class Leptris::XML::Element < Leptris::XML::Node
117
117
  # Drop any full-face memos from a prior version — otherwise a
118
118
  # post-mutation cold [] would stamp the new version onto a
119
119
  # stale @attributes hash and attributes would serve it.
120
+ # ruby#526: only @attributes shares @attributes_version with
121
+ # @attr_values (both restamped below), so only it needs the
122
+ # drop; @attribute_nodes/@keys/@values carry their OWN stamps.
123
+ # Niling those WITHOUT restamping left a nil memo whose stamp
124
+ # still matched the (unadvanced) document version — the next
125
+ # attribute_nodes/keys/values call memo-hit the nil and served
126
+ # it (consumers reading `attribute_nodes || []` saw []).
127
+ # Dropping a face means dropping both halves.
120
128
  ensure_alive!
121
129
  v = if @addr_reads_fast
122
130
  Leptris::XML::Native.fast_attribute(@c_address, name)
@@ -126,8 +134,11 @@ class Leptris::XML::Element < Leptris::XML::Node
126
134
  @attr_values = { name => v }
127
135
  @attributes = nil
128
136
  @attribute_nodes = nil
137
+ @attribute_nodes_version = nil
129
138
  @keys = nil
139
+ @keys_version = nil
130
140
  @values = nil
141
+ @values_version = nil
131
142
  @attributes_version = @document.version
132
143
  return v
133
144
  end
@@ -188,10 +199,14 @@ class Leptris::XML::Element < Leptris::XML::Node
188
199
  # Versioned attribute memo first (TODO.perf/21): a hit proves
189
200
  # presence without the engine round-trip; the memo cannot
190
201
  # prove absence for a partial fill, so misses fall through.
202
+ # ruby#526 sibling: a PARTIAL fill's key proves only "queried"
203
+ # — the cold [] caches queried-absent names as nil, so a nil
204
+ # entry without the full face is ambiguous exactly as in #[]'
205
+ # version-matched read and must fall through to the engine.
191
206
  n = name.to_s
192
207
  values = @attr_values
193
208
  if @document && values && @attributes_version == @document.version &&
194
- values.key?(n)
209
+ values.key?(n) && (!values[n].nil? || @attributes)
195
210
  return true
196
211
  end
197
212
  return true if Leptris::XML::FFI.leptris_element_has_attribute(
Binary file
Binary file
@@ -109,6 +109,21 @@ class Leptris::XML::Document
109
109
  Leptris::XML::Native.register_doc_klasses(self, map)
110
110
  end
111
111
 
112
+ # leptris-ruby#529: the whole XPath node-set result as an Array of
113
+ # NativeNodes in ONE C call — the iterate-every-row lane's bulk
114
+ # face. Rows mint through the registered wrapper klasses (#340)
115
+ # with native-cache identity; synthetic attribute/sequence rows
116
+ # keep the ResultAttr/ResultText wrappers the eager path minted.
117
+ # Takes the raw result pointer eval_ptrs returned and CONSUMES it
118
+ # (nodesets free the handle in C); a non-nodeset result answers
119
+ # nil and leaves the pointer with the caller — route it through
120
+ # Searchable.wrap_xpath_result for the scalar faces.
121
+ def xpath_result_rows_bulk(result_ptr)
122
+ Leptris::XML::Native.xpath_result_rows_bulk(
123
+ self, result_ptr.respond_to?(:address) ? result_ptr.address
124
+ : result_ptr)
125
+ end
126
+
112
127
  # Builder factories that return NativeNodes (#149): one C call
113
128
  # and a TypedData wrap — no FFI::Pointer, no wrap_fresh path.
114
129
  # (context, adapter) pre-writes the consumer's contract ivars in
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.379.2
4
+ version: 1.9.379.3
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.