leptris 1.9.213.0 → 1.9.216.0
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 +4 -4
- data/CHANGELOG.md +65 -0
- data/Rakefile +1 -1
- data/ext/leptris/native/native.c +22 -7
- data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/s390x-linux-musl/libleptris.so +0 -0
- data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
- data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/descriptor.rb +51 -13
- data/lib/leptris/xml/ffi.rb +49 -2
- data/lib/leptris/xml/plan_value.rb +192 -24
- data/vendor-src/README.md +1 -1
- data/vendor-src/libleptris/CHANGELOG.md +61 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/src/include/leptris/descriptor.h +58 -0
- data/vendor-src/libleptris/src/include/leptris.h +30 -0
- data/vendor-src/libleptris/src/leptris/descriptor/plan.c +447 -27
- data/vendor-src/libleptris/src/leptris/dom/element_query.c +28 -0
- data/vendor-src/libleptris/src/leptris/dom/root_doc_map.c +44 -8
- data/vendor-src/libleptris/src/leptris/leptris_internal.h +11 -0
- data/vendor-src/libleptris/src/leptris/xpath/evaluator.c +11 -4
- data/vendor-src/libleptris/src/leptris/xpath/evaluator_internal.h +3 -0
- data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +422 -52
- data/vendor-src/libleptris/src/leptris/xpath/evaluator_path.c +69 -3
- data/vendor-src/libleptris/src/leptris/xpath/evaluator_types.c +48 -0
- data/vendor-src/libleptris/src/leptris/xpath/functions.c +20 -3
- data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +37 -6
- data/vendor-src/libleptris/src/leptris/xpath/parser.c +272 -14
- data/vendor-src/libleptris/src/leptris/xpath/xpath_public.c +11 -3
- data/vendor-src/libleptris/src/leptris/xpath/xpath_variables.c +9 -0
- data/vendor-src/libleptris/src/leptris/xquery/xquery.c +1048 -138
- data/vendor-src/libleptris/src/leptris/xslt/xslt_exec.c +2 -1
- data/vendor-src/libleptris/test/abi/test_descriptor.cpp +294 -2
- data/vendor-src/libleptris/test/dom/test_dom.cpp +87 -0
- data/vendor-src/libleptris/test/xpath/test_xslt.cpp +1 -1
- data/vendor-src/libleptris/test/xquery/qt3/docs/works-mod.xml +76 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/function-arity.xml +280 -0
- data/vendor-src/libleptris/test/xquery/qt3/fn/function-name.xml +324 -0
- data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause/books.xml +26 -0
- data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause/products.xml +33 -0
- data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause/sales-records.xml +48 -0
- data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause/stores.xml +19 -0
- data/vendor-src/libleptris/test/xquery/qt3/prod/GroupByClause.xml +749 -0
- data/vendor-src/libleptris/test/xquery/qt3/prod/IfExpr.xml +479 -0
- data/vendor-src/libleptris/test/xquery/qt3/prod/OrderByClause/orderData.xml +114 -0
- data/vendor-src/libleptris/test/xquery/qt3/prod/OrderByClause.xml +2491 -0
- data/vendor-src/libleptris/test/xquery/qt3/prod/SwitchExpr.xml +425 -0
- data/vendor-src/libleptris/test/xquery/test_qt3.cpp +272 -3
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 71e488b3d701d539538391a6023b3831e3e1e52ac68a55890e8a580a4816834e
|
|
4
|
+
data.tar.gz: c6e32e3507bc45f4694a465bf03a48da95083b6c06fd58e546b020a2dbb53a9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0471386305b59ee214963a745efcdaedd37affe0608abab3698e3aa16f5e62453da8a1723fbac74ed45d9c69366432a080408030d30846b9ad22fb223e7dd00b'
|
|
7
|
+
data.tar.gz: 942534081f51c0b36c967cd1de6037584aaf717f5d2168c5c9a9e17c57d053dfd4256894dd049f96a9a92301a9f0f42beb5eae0a6f087bd44a0044c67052d7bc
|
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.
|
|
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
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -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
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
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
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/leptris/version.rb
CHANGED
|
@@ -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 (#
|
|
256
|
-
#
|
|
257
|
-
#
|
|
258
|
-
#
|
|
259
|
-
#
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -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
|
|
@@ -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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
Integer(s, 10) rescue s
|
|
263
|
+
int_value || (string_value && (Integer(string_value, 10) rescue string_value))
|
|
113
264
|
when 2
|
|
114
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|