leptris 1.9.213.0-x86_64-linux → 1.9.216.0-x86_64-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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/Rakefile +1 -1
  4. data/ext/leptris/native/native.c +22 -7
  5. data/lib/leptris/version.rb +1 -1
  6. data/lib/leptris/xml/descriptor.rb +51 -13
  7. data/lib/leptris/xml/ffi.rb +49 -2
  8. data/lib/leptris/xml/native.so +0 -0
  9. data/lib/leptris/xml/plan_value.rb +192 -24
  10. data/lib/libleptris.so +0 -0
  11. data/vendor-src/README.md +1 -1
  12. data/vendor-src/libleptris/CHANGELOG.md +61 -0
  13. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  14. data/vendor-src/libleptris/src/include/leptris/descriptor.h +58 -0
  15. data/vendor-src/libleptris/src/include/leptris.h +30 -0
  16. data/vendor-src/libleptris/src/leptris/descriptor/plan.c +447 -27
  17. data/vendor-src/libleptris/src/leptris/dom/element_query.c +28 -0
  18. data/vendor-src/libleptris/src/leptris/dom/root_doc_map.c +44 -8
  19. data/vendor-src/libleptris/src/leptris/leptris_internal.h +11 -0
  20. data/vendor-src/libleptris/src/leptris/xpath/evaluator.c +11 -4
  21. data/vendor-src/libleptris/src/leptris/xpath/evaluator_internal.h +3 -0
  22. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +422 -52
  23. data/vendor-src/libleptris/src/leptris/xpath/evaluator_path.c +69 -3
  24. data/vendor-src/libleptris/src/leptris/xpath/evaluator_types.c +48 -0
  25. data/vendor-src/libleptris/src/leptris/xpath/functions.c +20 -3
  26. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +37 -6
  27. data/vendor-src/libleptris/src/leptris/xpath/parser.c +272 -14
  28. data/vendor-src/libleptris/src/leptris/xpath/xpath_public.c +11 -3
  29. data/vendor-src/libleptris/src/leptris/xpath/xpath_variables.c +9 -0
  30. data/vendor-src/libleptris/src/leptris/xquery/xquery.c +1048 -138
  31. data/vendor-src/libleptris/src/leptris/xslt/xslt_exec.c +2 -1
  32. data/vendor-src/libleptris/test/abi/test_descriptor.cpp +294 -2
  33. data/vendor-src/libleptris/test/dom/test_dom.cpp +87 -0
  34. data/vendor-src/libleptris/test/xpath/test_xslt.cpp +1 -1
  35. data/vendor-src/libleptris/test/xquery/qt3/docs/works-mod.xml +76 -0
  36. data/vendor-src/libleptris/test/xquery/qt3/fn/function-arity.xml +280 -0
  37. data/vendor-src/libleptris/test/xquery/qt3/fn/function-name.xml +324 -0
  38. data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause/books.xml +26 -0
  39. data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause/products.xml +33 -0
  40. data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause/sales-records.xml +48 -0
  41. data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause/stores.xml +19 -0
  42. data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause.xml +749 -0
  43. data/vendor-src/libleptris/test/xquery/qt3/prod/IfExpr.xml +479 -0
  44. data/vendor-src/libleptris/test/xquery/qt3/prod/OrderByClause/orderData.xml +114 -0
  45. data/vendor-src/libleptris/test/xquery/qt3/prod/OrderByClause.xml +2491 -0
  46. data/vendor-src/libleptris/test/xquery/qt3/prod/SwitchExpr.xml +425 -0
  47. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +272 -3
  48. data/vendor-src/libleptris/vcpkg.json +1 -1
  49. metadata +13 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3091abd7691bcd52cdf61e075cbfd216f5ae5e975defc48cab57f39ff5ba10d6
4
- data.tar.gz: 390ea403a8e54c207252e794af45aa0c705ad55ff876f04da21d10787ef52d1a
3
+ metadata.gz: bd31f0587ebb9098ed318c732ffb69d6b38c4476b5e16c4dd8fc78bcc9f1eba2
4
+ data.tar.gz: cd6c4d000d2cff045c16ee809e5b359e0f344334e8c567d798d409857687b083
5
5
  SHA512:
6
- metadata.gz: 3022a4b44cb0637c64ca88b8729bed9243d28a095355b64df5ef4bd593bd5f3cce34817dd9a7ad659671deb1baa5a70fcb34bdc8f895f156bdd1b2e3b4b5d87a
7
- data.tar.gz: b37253ae40097cea6d76909b86da0697785c62d1d28d6d375e4cb2ab088888033238917d0d411e3630a96d75c808237bd01275b2c4a4976b64a9da45d42aeaf3
6
+ metadata.gz: '002850579eb358499474e6f8420046962b03d3ac2dd574251dec77928da4685c66e17bd08f0dbda8d4130cf350fb860c90813cdd3d35196e98db5bff1f45279b'
7
+ data.tar.gz: 3ccce56b40ee17ac63de60655ccc2972d463699a92f6500eb60e55d1dbf2d026604ef86ef30f205a591ba5006657035b55229f8da3d2e1134209581c9088d58a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,58 @@ 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.216.0] - 2026-09-21
9
+
10
+ ### Added — the plan-ABI consumer contract (#298 fully bound; engine 1.9.214–216)
11
+
12
+ - **#1272 attribute-predicate rows**: plan rows accept
13
+ `when: {attr => value}` (or `[[attr, value], ...]` — AND across
14
+ pairs). Same-wire-name rows with different predicates partition
15
+ the match space exclusively (first matching row wins per
16
+ occurrence); nil expected values rejected at the boundary.
17
+ - **#1273 document-order identity**: `PlanValue#node_kind` (the
18
+ source node's LEPTRIS_NODE_TYPE_*) and `#order_index` (dense
19
+ sibling rank); `flags: [:order_spine]` emits unmatched sibling
20
+ text/comment/PI runs as SCALAR values with position — ordered
21
+ hosts rebuild element_order without re-parsing.
22
+ - **#1269a in-pass type execution**: the walk parses
23
+ `type_tag ∈ {1,2,3}` scalars natively;
24
+ `PlanValue#int_value/#float_value/#bool_value` read the parsed
25
+ values (nil on soft-fail; `#string_value` stays the raw escape;
26
+ the lenient Ruby path remains the last resort).
27
+ - **#1269b fused materialize**: `Descriptor#materialize(source)`
28
+ is now ONE C call (`leptris_plan_materialize`) — parse, walk,
29
+ and free natively; PARSE failures surface as ParseError.
30
+ - **#1254 answered**: `leptris_element_attribute_pairs` attached;
31
+ the native `attribute_pairs` face re-pointed at it — two
32
+ crossings per element regardless of attribute count (was 4
33
+ accessors per attribute).
34
+ - **Crossings floor (#298.1)**: the 5k-row ISO fixture measures
35
+ **~4 FFI crossings per row** (single-child-row plans skip the
36
+ name probe; the pinned spec gates regressions).
37
+ - **CI gate removed**: the ubuntu-latest Ruby 3.4 leg returns to
38
+ blocking — engine 1.9.216 carries leptris#1277 (the TLS
39
+ root→document memo generation counter) fixing leptris-ruby#279's
40
+ 12-strike flake at the root cause. Every leg is strict again.
41
+ - Obsolete pins flipped (the engine grew the capability):
42
+ FLWOR order-by is accepted in the standalone XPath face and in
43
+ XSLT value-of (the #692 rejects-pins), and group-by returns
44
+ every tuple's binding (5 = 3+2, the tuple-ownership fix).
45
+ - Audit 342/342 (seven new symbols attached).
46
+
47
+ ## [1.9.214.0] - 2026-09-21
48
+
49
+ ### Engine sync — 1.9.214 (the QT3 lane-15 expression core)
50
+
51
+ - `switch` / `group by` / `if` expressions land in the XQuery
52
+ engine (fn-items #1268, lane-15 core #1270): bare/multi-spec
53
+ grouping variables, WHERE-after-GROUP-BY, ctor-enclosed FLWOR
54
+ hoisting, tuple-owned attribute/namespace node clones (the
55
+ group-by dangling-key fix), nested direct ctors, `prefix:*` vs
56
+ multiply disambiguation — 1640/1640 engine-side. Also ships the
57
+ typed-descriptor bulk face from 1.9.210.1 (`as_kwarg_hash`).
58
+ - No binding-facing surface changes; audit holds at 335/335.
59
+
8
60
  ## [1.9.213.0] - 2026-09-21
9
61
 
10
62
  ### Engine sync — 1.9.211–1.9.213 (perf + the QT3 date/time family)
@@ -45,6 +97,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
45
97
  on every engine. Native in-pass int/float parsing (no string
46
98
  round-trip) is the recorded engine-side headroom.
47
99
 
100
+ ## [Unreleased]
101
+
102
+ ### Added — `PlanValue#as_kwarg_hash` bulk dispatch (#298)
103
+
104
+ - One Ruby method yields the hash the consumer's hydrator wants per
105
+ node (typed attributes + typed scalar/collection/element children,
106
+ per the row type tags) — the per-field `value.attribute(name)` /
107
+ `value.children` / `value.at(i)` dispatch on the hot path
108
+ collapses. Native in-pass fusion (leptris#1269) would drop the
109
+ FFI floor further; today the documented floor on the 5k-row ISO
110
+ shape is **~5 crossings per row** (verified by
111
+ `result.crossings / 5000`).
112
+
48
113
  ## [1.9.210.0] - 2026-09-20
49
114
 
50
115
  ### Performance — engine sync (1.9.210)
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
8
8
  # Pin for `rake compile` and the platform-gem builds. Keep in lockstep
9
9
  # with .github/workflows/build.yml (which calls `rake compile`) and the
10
10
  # CHANGELOG when libleptris releases.
11
- LIBLEPTRIS_VERSION = "1.9.213"
11
+ LIBLEPTRIS_VERSION = "1.9.216"
12
12
  # Vendored alongside libleptris for fn:normalize-unicode (TODO
13
13
  # .restructure/20): built per platform with a RELOCATABLE @rpath
14
14
  # install name, loaded by ffi.rb before libleptris so the
@@ -35,6 +35,8 @@ typedef void *(*attr_first_fn)(void *);
35
35
  typedef void *(*attr_next_fn)(void *);
36
36
  typedef const char *(*attr_name_fn)(void *);
37
37
  typedef const char *(*attr_value_fn)(void *, void *);
38
+ typedef size_t (*attr_pairs_fn)(void *, const char **, const char **,
39
+ void **, size_t);
38
40
  typedef unsigned int (*node_line_fn)(void *);
39
41
  typedef size_t (*node_offset_fn)(void *);
40
42
  typedef const char *(*element_text_fn)(void *);
@@ -67,6 +69,7 @@ static attr_first_fn f_attr_first;
67
69
  static attr_next_fn f_attr_next;
68
70
  static attr_name_fn f_attr_name;
69
71
  static attr_value_fn f_attr_value;
72
+ static attr_pairs_fn f_attr_pairs;
70
73
  static node_line_fn f_node_line;
71
74
  static node_offset_fn f_node_offset;
72
75
  static element_text_fn f_element_text;
@@ -205,6 +208,7 @@ static void resolve_symbols(const char *lib_path)
205
208
  f_attr_next = (attr_next_fn)lib_sym(h, "leptris_attribute_next");
206
209
  f_attr_name = (attr_name_fn)lib_sym(h, "leptris_attribute_get_name");
207
210
  f_attr_value = (attr_value_fn)lib_sym(h, "leptris_attribute_get_value");
211
+ f_attr_pairs = (attr_pairs_fn)lib_sym(h, "leptris_element_attribute_pairs");
208
212
  f_node_line = (node_line_fn)lib_sym(h, "leptris_node_line");
209
213
  f_node_offset = (node_offset_fn)lib_sym(h, "leptris_node_byte_offset");
210
214
  f_element_text = (element_text_fn)lib_sym(h, "leptris_element_text");
@@ -1947,14 +1951,25 @@ static VALUE nf_attribute_pairs(VALUE self, VALUE document, VALUE addr)
1947
1951
  VALUE out = rb_ary_new();
1948
1952
  if (f_node_type(node) != WS_NODE_ELEMENT) return out;
1949
1953
 
1950
- for (void *a = f_attr_first(node); a; a = f_attr_next(a)) {
1951
- const char *an = f_attr_name(a);
1952
- const char *av = f_attr_value(node, a);
1953
- rb_ary_push(out, an ? rb_enc_interned_str(an, strlen(an),
1954
- rb_utf8_encoding())
1955
- : Qnil);
1956
- rb_ary_push(out, av ? rb_utf8_str_new_cstr(av) : Qnil);
1954
+ /* Engine one-call export (leptris 1.9.216, leptris-ruby#1254):
1955
+ * total caller-allocated parallel arrays → one copy pass.
1956
+ * Two crossings per element regardless of attribute count,
1957
+ * vs 4 accessors per attribute on the walk shape. */
1958
+ size_t total = f_attr_pairs(node, NULL, NULL, NULL, 0);
1959
+ if (total == 0) return out;
1960
+ const char **names = ALLOC_N(const char *, total);
1961
+ const char **values = ALLOC_N(const char *, total);
1962
+ size_t n = f_attr_pairs(node, names, values, NULL, total);
1963
+ out = rb_ary_new_capa(2 * n);
1964
+ for (size_t i = 0; i < n; i++) {
1965
+ rb_ary_push(out, names[i]
1966
+ ? rb_enc_interned_str(names[i], strlen(names[i]),
1967
+ rb_utf8_encoding())
1968
+ : Qnil);
1969
+ rb_ary_push(out, values[i] ? rb_utf8_str_new_cstr(values[i]) : Qnil);
1957
1970
  }
1971
+ xfree(names);
1972
+ xfree(values);
1958
1973
  return out;
1959
1974
  }
1960
1975
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.213.0"
4
+ VERSION = "1.9.216.0"
5
5
  end
@@ -82,6 +82,10 @@ class Leptris::XML::Descriptor
82
82
  ordered: Leptris::XML::FFI::PLAN_FLAG_ORDERED,
83
83
  cdata: Leptris::XML::FFI::PLAN_FLAG_CDATA,
84
84
  ns_lenient: Leptris::XML::FFI::PLAN_FLAG_NS_LENIENT,
85
+ # #1273: also emit unmatched sibling text runs / comments / PIs
86
+ # as SCALAR values (position + node_kind populated) so ordered
87
+ # hosts rebuild element_order without re-parsing the source.
88
+ order_spine: Leptris::XML::FFI::PLAN_FLAG_EMIT_ORDER_SPINE,
85
89
  }.freeze
86
90
  private_constant :FLAGS
87
91
 
@@ -163,6 +167,7 @@ class Leptris::XML::Descriptor
163
167
  ap[:wire_name] = anchor_string(anchors, row.fetch(:name))
164
168
  ap[:kind] = kind_code(row[:kind] || :scalar)
165
169
  ap[:type_tag] = type_tag_code(row)
170
+ pack_predicates(ap, row, anchors)
166
171
  end
167
172
  ep[:attribute_count] = attrs.size
168
173
  ep[:attribute_plans] = attr_memory
@@ -188,6 +193,7 @@ class Leptris::XML::Descriptor
188
193
  cp[:ns_uri] =
189
194
  row[:ns].is_a?(Hash) ?
190
195
  anchor_string(anchors, row[:ns].fetch(:exact)) : nil
196
+ pack_predicates(cp, row, anchors)
191
197
  end
192
198
  ep[:child_count] = children.size
193
199
  ep[:child_plans] = child_memory
@@ -229,6 +235,35 @@ class Leptris::XML::Descriptor
229
235
  end
230
236
  end
231
237
 
238
+ # #1272 attribute-predicate rows: +when:+ accepts
239
+ # {attr => value} (AND across pairs) or [[attr, value], ...].
240
+ # Same-wire-name rows with different predicates partition the
241
+ # match space exclusively (first matching row wins per
242
+ # occurrence). Nil expected values are rejected — the engine's
243
+ # predicate is string-equal only.
244
+ def self.pack_predicates(plan_struct, row, anchors)
245
+ preds = row[:when] or return
246
+ pairs = case preds
247
+ when Hash then preds.to_a
248
+ when Array then preds
249
+ else raise ArgumentError,
250
+ "when must be a Hash or an Array of [attr, value] pairs"
251
+ end
252
+ pairs.each do |(name, value)|
253
+ raise ArgumentError, "when: expected values must be non-nil" if value.nil?
254
+ end
255
+ memory = ::FFI::MemoryPointer.new(
256
+ Leptris::XML::FFI::AttrPredicate, pairs.size)
257
+ anchors << memory
258
+ pairs.each_with_index do |(name, value), k|
259
+ pr = Leptris::XML::FFI::AttrPredicate.new(memory[k])
260
+ pr[:wire_name] = anchor_string(anchors, name.to_s)
261
+ pr[:expected_value] = anchor_string(anchors, value.to_s)
262
+ end
263
+ plan_struct[:predicate_count] = pairs.size
264
+ plan_struct[:predicates] = pairs.empty? ? nil : memory
265
+ end
266
+
232
267
  def initialize(handle, plans, root_index)
233
268
  @handle = handle
234
269
  @plans = plans
@@ -252,20 +287,23 @@ class Leptris::XML::Descriptor
252
287
  owner: true, plans: @plans, plan: @plans[0])
253
288
  end
254
289
 
255
- # The fused loop (#230, pointed at the parse): source bytes →
256
- # typed rows in ONE call — parse, walk the compiled plan, free
257
- # the document; the returned PlanValue tree is standalone and
258
- # the Document never surfaces. Byte-for-byte parity with
259
- # #walk on the same source's root element.
290
+ # The fused loop (#1269b): source bytes → typed rows in ONE C
291
+ # call leptris_plan_materialize runs parse, walk, and free
292
+ # natively; the returned PlanValue tree is standalone and the
293
+ # Document never exists. Byte-parity with #walk on the source's
294
+ # root element.
260
295
  def materialize(source)
261
- document = Leptris::XML::Document.parse(source)
262
- begin
263
- root = document.root
264
- raise Leptris::XML::Error,
265
- "materialize: document has no root element" if root.nil?
266
- walk(root)
267
- ensure
268
- document.free
296
+ s = source.to_s
297
+ status = ::FFI::MemoryPointer.new(:int)
298
+ raw = Leptris::XML::FFI.leptris_plan_materialize(
299
+ s, s.bytesize, @handle, status)
300
+ if raw.null?
301
+ code = status.read_int
302
+ klass = code == Leptris::XML::FFI::LEPTRIS_ERROR_PARSE ?
303
+ Leptris::XML::ParseError : Leptris::XML::Error
304
+ raise klass, "plan materialize failed (status=#{code})"
269
305
  end
306
+ Leptris::XML::PlanValue.new(ResultHandle.new(raw),
307
+ owner: true, plans: @plans, plan: @plans[0])
270
308
  end
271
309
  end
@@ -225,10 +225,20 @@ module Leptris
225
225
  # leptris_plan_walk materializes a whole subtree in one
226
226
  # native pass. The plan/result structs mirror descriptor.h
227
227
  # — POD, ABI-frozen at v1 (leptris_plan_abi_version).
228
+ # #1272 (additive v1, trailing fields): attribute-predicate
229
+ # rows — same-wire siblings partition by (attr, expected)
230
+ # pairs, AND across pairs, exclusive per occurrence.
231
+ class AttrPredicate < ::FFI::Struct
232
+ layout :wire_name, :pointer,
233
+ :expected_value, :pointer
234
+ end
228
235
  class AttrPlan < ::FFI::Struct
229
236
  layout :wire_name, :pointer,
230
237
  :kind, :uint8,
231
- :type_tag, :uint8
238
+ :type_tag, :uint8,
239
+ :predicate_count, :uint16,
240
+ :pad_pred, :uint16,
241
+ :predicates, :pointer
232
242
  end
233
243
  class ChildPlan < ::FFI::Struct
234
244
  layout :wire_name, :pointer,
@@ -239,7 +249,11 @@ module Leptris
239
249
  # forms on ChildPlan (additive to the frozen v1 ABI).
240
250
  :ns_form, :uint8,
241
251
  :pad0, :uint8,
242
- :ns_uri, :pointer
252
+ :ns_uri, :pointer,
253
+ # #1272: element-side predicates (same semantics).
254
+ :predicate_count, :uint16,
255
+ :pad_pred, :uint16,
256
+ :predicates, :pointer
243
257
  end
244
258
  class ElementPlan < ::FFI::Struct
245
259
  layout :element_name, :pointer,
@@ -272,6 +286,9 @@ module Leptris
272
286
  PLAN_FLAG_ORDERED = 0x2
273
287
  PLAN_FLAG_CDATA = 0x4
274
288
  PLAN_FLAG_NS_LENIENT = 0x8
289
+ # #1273: walk also emits unmatched sibling text/comment/PI
290
+ # runs as SCALAR values (position + node_kind populated).
291
+ PLAN_FLAG_EMIT_ORDER_SPINE = 0x10
275
292
 
276
293
  PLAN_NS_NONE = 0
277
294
  PLAN_NS_EXACT = 1
@@ -315,6 +332,36 @@ module Leptris
315
332
  [:leptris_plan_result, :size_t], :leptris_plan_result
316
333
  attach_function :leptris_plan_value_attribute,
317
334
  [:leptris_plan_result, :string], :string
335
+ # #1254 (leptris 1.9.216): one-call flat attribute read —
336
+ # parallel name/value/handle arrays, capacity-bounded; total
337
+ # count returned (min(total, max_count) copied). Also the
338
+ # substrate for the native layer's attribute_pairs face.
339
+ attach_function :leptris_element_attribute_pairs,
340
+ [:leptris_element, :pointer, :pointer, :pointer, :size_t],
341
+ :size_t
342
+ # #1273: document-order identity. node_kind is the source
343
+ # node's LEPTRIS_NODE_TYPE_* (0 for wrappers/synthesized);
344
+ # order_index is the dense sibling rank inside the producing
345
+ # element (0 if unranked).
346
+ attach_function :leptris_plan_value_node_kind,
347
+ [:leptris_plan_result], :uint8
348
+ attach_function :leptris_plan_value_order_index,
349
+ [:leptris_plan_result], :uint32
350
+ # #1269a: in-pass type execution — the walk parsed
351
+ # type_tag ∈ {1=int, 2=float, 3=bool} values once; the
352
+ # accessor returns 0 on success, non-zero on parse failure
353
+ # (host falls back to #string).
354
+ attach_function :leptris_plan_value_int,
355
+ [:leptris_plan_result, :pointer], :int
356
+ attach_function :leptris_plan_value_float,
357
+ [:leptris_plan_result, :pointer], :int
358
+ attach_function :leptris_plan_value_bool,
359
+ [:leptris_plan_result, :pointer], :int
360
+ # #1269b: fused parse→walk→free (byte-parity with
361
+ # parse_string + plan_walk(root) + document_free).
362
+ attach_function :leptris_plan_materialize,
363
+ [:string, :size_t, :leptris_plan, :pointer],
364
+ :leptris_plan_result
318
365
 
319
366
  attach_function :leptris_version, [], :string
320
367
  attach_function :leptris_version_components, [:pointer, :pointer, :pointer], :void
Binary file
@@ -22,57 +22,151 @@ class Leptris::XML::PlanValue
22
22
  }.freeze
23
23
  private_constant :VALUE_KINDS
24
24
 
25
- def initialize(ptr, owner: nil, plans: nil, plan: nil)
25
+ def initialize(ptr, owner: nil, plans: nil, plan: nil, counter: nil)
26
26
  @ptr = ptr
27
27
  @owner = owner
28
28
  @plans = plans
29
29
  @plan = plan
30
+ @counter = counter || [0]
30
31
  end
31
32
 
32
33
  def kind
34
+ inc_crossing
33
35
  VALUE_KINDS.fetch(Leptris::XML::FFI.leptris_plan_value_kind(@ptr))
34
36
  end
35
37
 
36
38
  # wire_name of the plan row that produced this value.
37
39
  def name
40
+ inc_crossing
38
41
  Leptris::XML::FFI.leptris_plan_value_name(@ptr)
39
42
  end
40
43
 
41
44
  # Host type_tag echoed verbatim (0 when the row had none).
42
45
  def type_tag
46
+ inc_crossing
43
47
  Leptris::XML::FFI.leptris_plan_value_type_tag(@ptr)
44
48
  end
45
49
 
50
+ # #1273: LEPTRIS_NODE_TYPE_* of this value's source node (0 for
51
+ # ELEMENT/COLLECTION wrappers and synthesized text).
52
+ def node_kind
53
+ inc_crossing
54
+ Leptris::XML::FFI.leptris_plan_value_node_kind(@ptr)
55
+ end
56
+
57
+ # #1273: dense sibling rank inside the producing element (0 if
58
+ # unranked) — document-order identity without byte offsets.
59
+ def order_index
60
+ inc_crossing
61
+ Leptris::XML::FFI.leptris_plan_value_order_index(@ptr)
62
+ end
63
+
64
+ # #1269a: the walk parsed type_tag ∈ {1,2,3} values natively.
65
+ # Returns the typed value, or nil when the row's tag isn't a
66
+ # typed one or the source text didn't parse (host falls back to
67
+ # #string_value / the lenient Ruby path).
68
+ def int_value
69
+ out = ::FFI::MemoryPointer.new(:int64)
70
+ return nil unless Leptris::XML::FFI.leptris_plan_value_int(@ptr, out).zero?
71
+ out.read_int64
72
+ end
73
+
74
+ def float_value
75
+ out = ::FFI::MemoryPointer.new(:double)
76
+ return nil unless Leptris::XML::FFI.leptris_plan_value_float(@ptr, out).zero?
77
+ out.read_double
78
+ end
79
+
80
+ def bool_value
81
+ out = ::FFI::MemoryPointer.new(:int)
82
+ return nil unless Leptris::XML::FFI.leptris_plan_value_bool(@ptr, out).zero?
83
+ out.read_int != 0
84
+ end
85
+
46
86
  # SCALAR / RAW / CALLBACK string value.
47
87
  def string_value
88
+ inc_crossing
48
89
  Leptris::XML::FFI.leptris_plan_value_string(@ptr)
49
90
  end
50
91
 
51
92
  # CALLBACK: document byte offset of the source node (0 unknown).
52
93
  def position
94
+ inc_crossing
53
95
  Leptris::XML::FFI.leptris_plan_value_position(@ptr)
54
96
  end
55
97
 
56
98
  # ELEMENT: child-value count. COLLECTION: item count.
57
99
  def count
100
+ inc_crossing
58
101
  Leptris::XML::FFI.leptris_plan_value_count(@ptr)
59
102
  end
60
103
 
61
104
  # ELEMENT child value / COLLECTION item at +i+ (nil out of
62
105
  # range). Borrowed: the owning root must stay alive.
63
106
  def at(i)
107
+ inc_crossing
64
108
  ptr = Leptris::XML::FFI.leptris_plan_value_at(@ptr, i)
65
109
  return nil if ptr.null?
66
- child_name = Leptris::XML::FFI.leptris_plan_value_name(ptr)
67
- child_plan = @plan ? child_row_plan(child_name) : nil
68
- Leptris::XML::PlanValue.new(ptr, plans: @plans, plan: child_plan)
110
+ child_plan = child_plan_for_position(ptr)
111
+ Leptris::XML::PlanValue.new(ptr, plans: @plans, plan: child_plan,
112
+ counter: @counter)
69
113
  end
70
114
 
71
115
  # ELEMENT: attribute value by wire_name (nil when absent).
72
116
  def attribute(wire_name)
117
+ inc_crossing
73
118
  Leptris::XML::FFI.leptris_plan_value_attribute(@ptr, wire_name.to_s)
74
119
  end
75
120
 
121
+ # The number of FFI accessor calls this PlanValue (and its
122
+ # #as_kwarg_hash descendants, via the borrowed-#at chain) have
123
+ # counted. Reset with #reset_crossings! before the work you want
124
+ # to count. Counts FFI crossings only — engine-internal walks
125
+ # during `leptris_plan_walk` are not exposed (they belong to
126
+ # leptris/leptris#1269's headroom).
127
+ def crossings
128
+ @counter[0]
129
+ end
130
+
131
+ def reset_crossings!
132
+ @counter[0] = 0
133
+ self
134
+ end
135
+
136
+ # Bulk kwarg view (#298's binding ask): one Ruby method yields
137
+ # the hash the consumer's hydrator wants per row — no per-field
138
+ # dispatch on the hot path. Honors the row type tags (typed
139
+ # scalars and collected attributes; the nested rows recurse via
140
+ # the same face so the whole row-graph collapses into one Ruby
141
+ # method call per node).
142
+ #
143
+ # { name:, type_tag:,
144
+ # attributes: { wire_name => typed_value, ... },
145
+ # children: { wire_name => typed_value | [typed_value, ...] } }
146
+ #
147
+ # The +name+ is the element wire_name (nil on the root, by
148
+ # engine contract); +type_tag+ echoes the plan row's type tag
149
+ # (0 when absent). Collection rows appear as Arrays; callbacks
150
+ # are emitted as +{value:, position:, type_tag:}+ like #to_ruby.
151
+ def as_kwarg_hash
152
+ inc_crossing # the kind probe + name + type_tag are one logical fetch
153
+ case kind
154
+ when :element
155
+ {
156
+ name: name,
157
+ type_tag: type_tag,
158
+ attributes: kwarg_attributes,
159
+ children: kwarg_children,
160
+ }
161
+ when :collection
162
+ kwarg_collection_children
163
+ when :callback
164
+ { value: string_value, position: position, type_tag: type_tag }
165
+ else
166
+ typed_string_value
167
+ end
168
+ end
169
+
76
170
  # The eager Ruby tree:
77
171
  # :element => { kind:, name:, type_tag:, attributes: {...},
78
172
  # children: [values] }
@@ -100,36 +194,110 @@ class Leptris::XML::PlanValue
100
194
 
101
195
  private
102
196
 
103
- # SCALAR honoring the plan row's type tag (#230's typed-scalars
104
- # contract): 1=Integer, 2=Float, 3=boolean. Lenient on
105
- # unparseable input — the raw String wins (first-wins house
106
- # style; strict validation belongs to the consumer's callback
107
- # rows). #string_value stays the raw escape.
197
+ def inc_crossing
198
+ @counter[0] += 1
199
+ end
200
+
201
+ def kwarg_attributes
202
+ ((@plan && @plan[:attributes]) || []).each_with_object({}) do |row, result|
203
+ v = attribute(row[:name])
204
+ result[row[:name]] = cast_attribute(v, row) unless v.nil?
205
+ end
206
+ end
207
+
208
+ def kwarg_children
209
+ rows = ((@plan && @plan[:children]) || [])
210
+ result = {}
211
+ rows.each_with_index do |row, i|
212
+ value = at(i)
213
+ next if value.nil?
214
+ key = row[:name]
215
+ coerced = coerce_kwarg(value, row[:kind])
216
+ result[key] = if result.key?(key)
217
+ Array(result[key]) << coerced
218
+ else
219
+ coerced
220
+ end
221
+ end
222
+ result
223
+ end
224
+
225
+ def kwarg_collection_children
226
+ Array.new(count) { |i| coerce_kwarg(at(i)) }
227
+ end
228
+
229
+ # Coerce a PlanValue row to the shape the consumer's hydrator
230
+ # wants: scalars/ra→raw/callback collapse to typed values;
231
+ # :element children recurse into the kwarg hash; :collection
232
+ # children yield the array of typed items.
233
+ def coerce_kwarg(value, kind = nil)
234
+ v_kind = kind || value.kind
235
+ case v_kind
236
+ when :element then value.as_kwarg_hash
237
+ when :collection then Array.new(value.count) { |i| coerce_kwarg(value.at(i)) }
238
+ when :callback
239
+ { value: value.string_value, position: value.position,
240
+ type_tag: value.type_tag }
241
+ else
242
+ typed_string_value_for(value)
243
+ end
244
+ end
245
+
246
+ def typed_string_value_for(value)
247
+ case value.type_tag
248
+ when 1 then value.int_value || (Integer(value.string_value, 10) rescue value.string_value)
249
+ when 2 then value.float_value || (Float(value.string_value) rescue value.string_value)
250
+ when 3
251
+ v = value.bool_value
252
+ v.nil? ? typed_bool_fallback(value.string_value) : v
253
+ else value.string_value
254
+ end
255
+ end
256
+ # contract): 1=Integer, 2=Float, 3=boolean. Executed in-pass by
257
+ # the engine (#1269a); on its soft-fail (non-numeric source) the
258
+ # lenient Ruby path applies, then the raw String wins.
259
+ # #string_value stays the raw escape.
108
260
  def typed_string_value
109
261
  case type_tag
110
262
  when 1
111
- s = string_value
112
- Integer(s, 10) rescue s
263
+ int_value || (string_value && (Integer(string_value, 10) rescue string_value))
113
264
  when 2
114
- s = string_value
115
- begin
116
- Float(s)
117
- rescue ArgumentError, TypeError
118
- s
119
- end
265
+ float_value || (string_value && (Float(string_value) rescue string_value))
120
266
  when 3
121
- case string_value
122
- when "true", "1" then true
123
- when "false", "0" then false
124
- else string_value
125
- end
267
+ v = bool_value
268
+ v.nil? ? typed_bool_fallback(string_value) : v
126
269
  else
127
270
  string_value
128
271
  end
129
272
  end
130
273
 
131
- # The element plan that a child row with this wire_name recurses
132
- # into (nil for non-nested rows — their values carry no plan).
274
+ def typed_bool_fallback(s)
275
+ case s
276
+ when "true", "1" then true
277
+ when "false", "0" then false
278
+ else s
279
+ end
280
+ end
281
+
282
+ # The element plan that the child value +ptr+ recurses into (nil
283
+ # for non-nested rows — their values carry no plan). Fast path:
284
+ # with exactly ONE child row, every matched value must be that
285
+ # row — the FFI name probe is skipped (#298 crossings floor).
286
+ # Multi-row plans probe by wire_name (result positions only
287
+ # cover matched children, so positional mapping is unsafe).
288
+ def child_plan_for_position(ptr)
289
+ return nil unless @plan
290
+ rows = @plan[:children] || []
291
+ row = rows.size == 1 ? rows.first : nil
292
+ unless row
293
+ inc_crossing
294
+ child_name = Leptris::XML::FFI.leptris_plan_value_name(ptr)
295
+ row = rows.find { |r| r[:name] == child_name }
296
+ end
297
+ return nil unless row && @plans
298
+ row[:kind] == :nested ? @plans[row[:child_plan_index]] : nil
299
+ end
300
+
133
301
  def child_row_plan(child_wire_name)
134
302
  row = (@plan[:children] || []).find do |r|
135
303
  r[:kind] == :nested && r[:name] == child_wire_name
data/lib/libleptris.so CHANGED
Binary file
data/vendor-src/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Sources vendored in this gem (recompile rights; the ruby
2
2
  variant builds them at install via extconf.rb):
3
3
 
4
- - libleptris v1.9.213
4
+ - libleptris v1.9.216
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand: