leptris 1.9.380.1 → 1.9.381.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 +15 -0
- data/Rakefile +1 -1
- data/ext/leptris/native/native.c +219 -21
- 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/native_layer.rb +20 -2
- data/vendor-src/README.md +1 -1
- data/vendor-src/libleptris/CHANGELOG.md +42 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/TODO.SOTA.md +18 -2
- data/vendor-src/libleptris/docs/MISSION-PLAN.md +1 -1
- data/vendor-src/libleptris/src/leptris/dom/element.c +31 -39
- data/vendor-src/libleptris/src/leptris/dom/element_modify.c +30 -10
- data/vendor-src/libleptris/src/leptris/xpath/evaluator_path.c +18 -3
- data/vendor-src/libleptris/src/leptris/xsd/validate.c +18 -1
- data/vendor-src/libleptris/src/leptris/xslt/xslt_exec.c +42 -3
- data/vendor-src/libleptris/src/leptris/xslt/xslt_pattern.c +16 -9
- data/vendor-src/libleptris/test/dom/test_dom.cpp +42 -0
- data/vendor-src/libleptris/test/xsd/test_xsd_compile.cpp +90 -0
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 600a02350159ac29319d4e4df445bade18bc26489ea6ad3f28dfe118948b366e
|
|
4
|
+
data.tar.gz: dd081df19fbe1a3ae8b1af6dad80395e2ddbd93aa2ecd488cbee6b059324e910
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b5c65806365acbe0bc0aebe358c5267519c71e3e922acecc1ca13fa44a826965f9feba45258286fb10885cb1ad13d77f2f97bead9d027f92ad0f60569086d02a
|
|
7
|
+
data.tar.gz: a3bdb6971f8e5a16db92ba01cf6d4d55f580e923250b616b9b8b02d3d755485717bc11f8a36b852e4c1526d40002d4117f025325e8672be3a91097ed66b5ec28
|
data/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,21 @@
|
|
|
10
10
|
model. Released 1.9.380.0 with the pin still at 1.9.379 (the
|
|
11
11
|
vendored dylib missed the fix); this pin bump is the real ride.
|
|
12
12
|
|
|
13
|
+
### Fixed — engine ride (1.9.380.2)
|
|
14
|
+
|
|
15
|
+
- **Bulk XPath priming face rides (leptris/leptris-ruby#537)**:
|
|
16
|
+
`xpath_result_rows_bulk` gains the mx_context/mx_adapter birth-prime
|
|
17
|
+
parameters and marks all-native results; 1.9.380.1 shipped before
|
|
18
|
+
the PR merged, so the face rides in 1.9.380.2.
|
|
19
|
+
|
|
20
|
+
## [1.9.381.0] - 2026-10-22
|
|
21
|
+
|
|
22
|
+
Ride: libleptris 1.9.381 — the XSLT identity/copy-of quadratic fix
|
|
23
|
+
(#1839): identity transforms 13.0 s -> 57 ms on a 20k-book catalog;
|
|
24
|
+
every CLI row now beats xsltproc end-to-end. The Rakefile pin moves
|
|
25
|
+
1.9.380 -> 1.9.381 in the same commit so the platform gems vendor
|
|
26
|
+
this core.
|
|
27
|
+
|
|
13
28
|
## [1.9.379.3] - 2026-10-22
|
|
14
29
|
|
|
15
30
|
### Fixed
|
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.381"
|
|
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
|
@@ -203,6 +203,10 @@ static VALUE binding_klass_for(int kind);
|
|
|
203
203
|
static VALUE c_iteration_scope;
|
|
204
204
|
static VALUE c_leptris_error;
|
|
205
205
|
static VALUE c_b_document, c_b_freed, c_ffi_pointer;
|
|
206
|
+
/* BulkIdentityRows resolves lazily like the binding classes: the
|
|
207
|
+
* module lives in native_layer.rb, which optional consumers load. */
|
|
208
|
+
static VALUE c_bulk_rows_mark = Qundef;
|
|
209
|
+
static ID id_bulk_rows_mark;
|
|
206
210
|
static ID id_ptr_new, id_freed_new, id_alive;
|
|
207
211
|
static ID id_iv_c_address, id_iv_document, id_iv_parent;
|
|
208
212
|
static ID id_iv_mx_context, id_iv_mx_adapter, id_iv_mx_native;
|
|
@@ -221,8 +225,23 @@ struct native_node {
|
|
|
221
225
|
* Slot beats the hash memo on repeat reads of one name. */
|
|
222
226
|
VALUE attr_key, attr_val, attr_ver;
|
|
223
227
|
VALUE content_val, content_ver;
|
|
228
|
+
/* Walk-read memos (leptris-ruby#530): the element name and the
|
|
229
|
+
* children list, the two reads a tree walk pays per node. The
|
|
230
|
+
* children memo is what the subtree prime (#527 machinery)
|
|
231
|
+
* fills; the version stamp retires both on any mutation. The
|
|
232
|
+
* children list is SHARED with callers until the version moves
|
|
233
|
+
* (the mutation that would invalidate it also retires the
|
|
234
|
+
* memo) — treat it as read-only, like an interned String. */
|
|
235
|
+
VALUE name_val, name_ver;
|
|
236
|
+
VALUE children_val, children_ver;
|
|
224
237
|
};
|
|
225
238
|
|
|
239
|
+
static VALUE columnar_node_for(VALUE document, VALUE owner_klass,
|
|
240
|
+
void *ptr, int kind);
|
|
241
|
+
static VALUE doc_klass_for_kind(VALUE document, int kind);
|
|
242
|
+
static VALUE nn_prime_children_of(VALUE document, VALUE owner_klass,
|
|
243
|
+
void *node, VALUE ver);
|
|
244
|
+
|
|
226
245
|
static void nn_mark(void *p)
|
|
227
246
|
{
|
|
228
247
|
struct native_node *n = p;
|
|
@@ -234,6 +253,10 @@ static void nn_mark(void *p)
|
|
|
234
253
|
rb_gc_mark(n->attr_val);
|
|
235
254
|
if (n->content_val != Qnil)
|
|
236
255
|
rb_gc_mark(n->content_val);
|
|
256
|
+
if (n->name_val != Qnil)
|
|
257
|
+
rb_gc_mark(n->name_val);
|
|
258
|
+
if (n->children_val != Qnil)
|
|
259
|
+
rb_gc_mark(n->children_val);
|
|
237
260
|
}
|
|
238
261
|
|
|
239
262
|
static size_t nn_size(const void *p) { (void)p; return sizeof(struct native_node); }
|
|
@@ -367,7 +390,19 @@ static VALUE nn_name(VALUE self)
|
|
|
367
390
|
{
|
|
368
391
|
struct native_node *n;
|
|
369
392
|
const char *s;
|
|
393
|
+
VALUE doc, ver;
|
|
394
|
+
|
|
370
395
|
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
396
|
+
/* Walk-memo hit (leptris-ruby#530): a repeat name read is two
|
|
397
|
+
* struct reads and a Fixnum compare — the subtree prime fills
|
|
398
|
+
* this slot ahead of the walk, so walked names never recompute
|
|
399
|
+
* (or re-intern) until the next mutation. */
|
|
400
|
+
doc = n->document;
|
|
401
|
+
if (doc != Qnil) {
|
|
402
|
+
ver = rb_ivar_get(doc, id_iv_version);
|
|
403
|
+
if (n->name_ver == ver)
|
|
404
|
+
return n->name_val;
|
|
405
|
+
}
|
|
371
406
|
s = f_elem_name(n->ptr);
|
|
372
407
|
/* Interned (shared, frozen) fstring: element names repeat
|
|
373
408
|
* massively across a document — one RString per distinct name
|
|
@@ -375,7 +410,13 @@ static VALUE nn_name(VALUE self)
|
|
|
375
410
|
* ~18% of consumer materialize allocations were fresh name
|
|
376
411
|
* strings). Names are identifiers; nothing downstream mutates
|
|
377
412
|
* them in place (renames mint new strings). */
|
|
378
|
-
|
|
413
|
+
VALUE name = s ? rb_enc_interned_str(s, strlen(s), rb_utf8_encoding())
|
|
414
|
+
: Qnil;
|
|
415
|
+
if (doc != Qnil) {
|
|
416
|
+
n->name_val = name;
|
|
417
|
+
n->name_ver = ver;
|
|
418
|
+
}
|
|
419
|
+
return name;
|
|
379
420
|
}
|
|
380
421
|
|
|
381
422
|
static VALUE nn_content(VALUE self)
|
|
@@ -563,14 +604,43 @@ static VALUE nn_children(VALUE self)
|
|
|
563
604
|
void **buf;
|
|
564
605
|
int *kinds;
|
|
565
606
|
int count, i;
|
|
566
|
-
VALUE cache, out, owner_klass;
|
|
607
|
+
VALUE cache, out, owner_klass, doc, ver;
|
|
567
608
|
|
|
568
609
|
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
610
|
+
/* Walk-memo hit (leptris-ruby#530): the subtree prime (or an
|
|
611
|
+
* earlier read) filled this node's list against the current
|
|
612
|
+
* mutation version — serve it, engine walk skipped. */
|
|
613
|
+
doc = n->document;
|
|
614
|
+
if (doc != Qnil) {
|
|
615
|
+
ver = rb_ivar_get(doc, id_iv_version);
|
|
616
|
+
if (n->children_ver == ver)
|
|
617
|
+
return n->children_val;
|
|
618
|
+
/* Registered-consumer documents (#340 doc-klass map = the
|
|
619
|
+
* moxml mint registration) walk whole subtrees top-down:
|
|
620
|
+
* the FIRST cold children_read trades itself for ONE
|
|
621
|
+
* subtree crossing that fills every node's name + children
|
|
622
|
+
* memo (walk_columnar machinery, native-cache identity —
|
|
623
|
+
* the primed wrappers ARE the answers later reads give).
|
|
624
|
+
* Unregistered documents keep the per-node fetch; both
|
|
625
|
+
* paths memo-store below. */
|
|
626
|
+
if (!NIL_P(doc_klass_for_kind(doc, NT_ELEMENT))) {
|
|
627
|
+
out = nn_prime_children_of(doc, rb_obj_class(self), n->ptr,
|
|
628
|
+
ver);
|
|
629
|
+
n->children_val = out;
|
|
630
|
+
n->children_ver = ver;
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
569
634
|
count = fetch_children_two_call(n->ptr, &buf, &kinds);
|
|
570
635
|
if (count <= 0) {
|
|
571
636
|
ruby_xfree(buf);
|
|
572
637
|
ruby_xfree(kinds);
|
|
573
|
-
|
|
638
|
+
out = rb_ary_new2(0);
|
|
639
|
+
if (doc != Qnil) {
|
|
640
|
+
n->children_val = out;
|
|
641
|
+
n->children_ver = ver;
|
|
642
|
+
}
|
|
643
|
+
return out;
|
|
574
644
|
}
|
|
575
645
|
|
|
576
646
|
cache = native_cache_of(n->document);
|
|
@@ -592,6 +662,10 @@ static VALUE nn_children(VALUE self)
|
|
|
592
662
|
}
|
|
593
663
|
ruby_xfree(buf);
|
|
594
664
|
ruby_xfree(kinds);
|
|
665
|
+
if (doc != Qnil) {
|
|
666
|
+
n->children_val = out;
|
|
667
|
+
n->children_ver = ver;
|
|
668
|
+
}
|
|
595
669
|
return out;
|
|
596
670
|
}
|
|
597
671
|
|
|
@@ -1940,18 +2014,28 @@ static VALUE nf_at_xpath_first(VALUE self, VALUE document,
|
|
|
1940
2014
|
* Nodesets free the handle here (TODO.perf/22 eager discipline);
|
|
1941
2015
|
* non-nodeset results return Qnil UNFREED — the caller keeps the
|
|
1942
2016
|
* scalar path, whose wrap owns the free. */
|
|
1943
|
-
static VALUE nf_xpath_result_rows_bulk(VALUE
|
|
1944
|
-
VALUE result_addr)
|
|
2017
|
+
static VALUE nf_xpath_result_rows_bulk(int argc, VALUE *argv, VALUE self)
|
|
1945
2018
|
{
|
|
1946
|
-
|
|
2019
|
+
VALUE document, result_addr, mx_context, mx_adapter;
|
|
2020
|
+
void *result;
|
|
1947
2021
|
void **buf;
|
|
1948
2022
|
int *kinds;
|
|
1949
2023
|
size_t scount;
|
|
1950
2024
|
long i, n;
|
|
1951
2025
|
VALUE cache, out;
|
|
2026
|
+
int all_native;
|
|
1952
2027
|
|
|
1953
2028
|
(void)self;
|
|
1954
2029
|
resolve_binding_classes();
|
|
2030
|
+
/* Optional (context, adapter): when the consumer passes its
|
|
2031
|
+
* contract state, every registered-klass row is born primed
|
|
2032
|
+
* (@native is the node itself, @context/@adapter written here)
|
|
2033
|
+
* and an all-native result extends with BulkIdentityRows —
|
|
2034
|
+
* the consumer's per-row wrap prime disappears from the
|
|
2035
|
+
* iterate-every-result lane (same triple as nn_create_element). */
|
|
2036
|
+
rb_scan_args(argc, argv, "22", &document, &result_addr,
|
|
2037
|
+
&mx_context, &mx_adapter);
|
|
2038
|
+
result = (void *)(uintptr_t)NUM2ULL(result_addr);
|
|
1955
2039
|
if (f_xp_type(result) != 0) /* XPATH_NODESET only */
|
|
1956
2040
|
return Qnil;
|
|
1957
2041
|
|
|
@@ -1969,6 +2053,7 @@ static VALUE nf_xpath_result_rows_bulk(VALUE self, VALUE document,
|
|
|
1969
2053
|
n = (long)scount;
|
|
1970
2054
|
out = rb_ary_new_capa(n);
|
|
1971
2055
|
cache = binding_cache_of(document);
|
|
2056
|
+
all_native = !NIL_P(mx_context);
|
|
1972
2057
|
for (i = 0; i < n; i++) {
|
|
1973
2058
|
void *ptr = buf[i];
|
|
1974
2059
|
int tag;
|
|
@@ -1988,10 +2073,17 @@ static VALUE nf_xpath_result_rows_bulk(VALUE self, VALUE document,
|
|
|
1988
2073
|
node = rb_hash_aref(native_cache_of(document), key);
|
|
1989
2074
|
if (NIL_P(node))
|
|
1990
2075
|
node = mint_consumer_node(mk, document, ptr);
|
|
2076
|
+
if (all_native &&
|
|
2077
|
+
!rb_ivar_defined(node, id_iv_mx_context)) {
|
|
2078
|
+
rb_ivar_set(node, id_iv_mx_native, node);
|
|
2079
|
+
rb_ivar_set(node, id_iv_mx_context, mx_context);
|
|
2080
|
+
rb_ivar_set(node, id_iv_mx_adapter, mx_adapter);
|
|
2081
|
+
}
|
|
1991
2082
|
rb_ary_push(out, node);
|
|
1992
2083
|
continue;
|
|
1993
2084
|
}
|
|
1994
2085
|
}
|
|
2086
|
+
all_native = 0;
|
|
1995
2087
|
node = materialize_xp_entry(document, cache, result, ptr,
|
|
1996
2088
|
kinds[i], (int)i);
|
|
1997
2089
|
if (node == m_native_sentinel) {
|
|
@@ -2005,6 +2097,22 @@ static VALUE nf_xpath_result_rows_bulk(VALUE self, VALUE document,
|
|
|
2005
2097
|
ruby_xfree(buf);
|
|
2006
2098
|
ruby_xfree(kinds);
|
|
2007
2099
|
f_xp_free(result);
|
|
2100
|
+
if (all_native) {
|
|
2101
|
+
/* Certificate for the consumer's fast row loop: every row
|
|
2102
|
+
* is a born-primed, registered-klass identity native. The
|
|
2103
|
+
* module lives in the Ruby layer (native_layer.rb) and may
|
|
2104
|
+
* not be loaded on faces used without it — an unresolved
|
|
2105
|
+
* mark degrades to the unmarked Array, never an error. */
|
|
2106
|
+
if (c_bulk_rows_mark == Qundef) {
|
|
2107
|
+
VALUE m_xml = rb_path2class("Leptris::XML");
|
|
2108
|
+
c_bulk_rows_mark =
|
|
2109
|
+
rb_const_defined_at(m_xml, id_bulk_rows_mark)
|
|
2110
|
+
? rb_const_get_at(m_xml, id_bulk_rows_mark)
|
|
2111
|
+
: Qnil;
|
|
2112
|
+
}
|
|
2113
|
+
if (c_bulk_rows_mark != Qnil)
|
|
2114
|
+
rb_extend_object(out, c_bulk_rows_mark);
|
|
2115
|
+
}
|
|
2008
2116
|
return out;
|
|
2009
2117
|
}
|
|
2010
2118
|
|
|
@@ -2599,6 +2707,58 @@ static void walk_columnar_impl(VALUE document, VALUE owner_klass,
|
|
|
2599
2707
|
}
|
|
2600
2708
|
}
|
|
2601
2709
|
|
|
2710
|
+
/* ---- Subtree memo priming (leptris-ruby#530) ---------------------
|
|
2711
|
+
* Registered-consumer documents (the #340 doc-klass map — moxml's
|
|
2712
|
+
* mint registration) walk whole subtrees top-down, so the FIRST
|
|
2713
|
+
* cold children_read fills every ELEMENT's name + children read
|
|
2714
|
+
* memos in ONE crossing, reusing the walk_columnar machinery and
|
|
2715
|
+
* minting through columnar_node_for (native-cache identity — the
|
|
2716
|
+
* primed wrappers ARE the answers later reads hand back). Memos
|
|
2717
|
+
* are version-stamped like the content memo: any mutation bumps
|
|
2718
|
+
* @version and retires them subtree-wide.
|
|
2719
|
+
*
|
|
2720
|
+
* Conservative by construction: only ELEMENT descendants get their
|
|
2721
|
+
* memos filled here (the one kind whose f_first_child chain IS its
|
|
2722
|
+
* children list, the #527 contract); leaf and shell kinds keep the
|
|
2723
|
+
* per-node fetch, which memo-stores the same way. The receiver's
|
|
2724
|
+
* own name memo is left to nn_name (one node, once). */
|
|
2725
|
+
static VALUE nn_prime_children_of(VALUE document, VALUE owner_klass,
|
|
2726
|
+
void *node, VALUE ver)
|
|
2727
|
+
{
|
|
2728
|
+
VALUE out = rb_ary_new();
|
|
2729
|
+
void *c;
|
|
2730
|
+
|
|
2731
|
+
for (c = f_first_child(node); c; c = f_next_sibling(c)) {
|
|
2732
|
+
int kind = f_node_type(c);
|
|
2733
|
+
VALUE wrapper = columnar_node_for(document, owner_klass, c, kind);
|
|
2734
|
+
struct native_node *nn;
|
|
2735
|
+
|
|
2736
|
+
TypedData_Get_Struct(wrapper, struct native_node, &nn_type, nn);
|
|
2737
|
+
if (kind == WS_NODE_ELEMENT) {
|
|
2738
|
+
const char *name = f_elem_name(c);
|
|
2739
|
+
nn->name_val = name ? rb_enc_interned_str(name, strlen(name),
|
|
2740
|
+
rb_utf8_encoding())
|
|
2741
|
+
: Qnil;
|
|
2742
|
+
nn->name_ver = ver;
|
|
2743
|
+
nn->children_val = nn_prime_children_of(document, owner_klass,
|
|
2744
|
+
c, ver);
|
|
2745
|
+
nn->children_ver = ver;
|
|
2746
|
+
} else if (kind != WS_NODE_DOCTYPE && kind != 9 /* DOCUMENT */) {
|
|
2747
|
+
/* leaf kinds: memo the name exactly as nn_name computes
|
|
2748
|
+
* it (f_elem_name answers "" for nameless kinds); their
|
|
2749
|
+
* (empty) children keep the fetch path, which
|
|
2750
|
+
* memo-stores the same way. */
|
|
2751
|
+
const char *name = f_elem_name(c);
|
|
2752
|
+
nn->name_val = name ? rb_enc_interned_str(name, strlen(name),
|
|
2753
|
+
rb_utf8_encoding())
|
|
2754
|
+
: Qnil;
|
|
2755
|
+
nn->name_ver = ver;
|
|
2756
|
+
}
|
|
2757
|
+
rb_ary_push(out, wrapper);
|
|
2758
|
+
}
|
|
2759
|
+
return out;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2602
2762
|
static VALUE nf_walk_columnar(VALUE self, VALUE root)
|
|
2603
2763
|
{
|
|
2604
2764
|
struct native_node *n;
|
|
@@ -2839,18 +2999,41 @@ static void *create_elem_core(void *doc_ptr, void *parent,
|
|
|
2839
2999
|
return elem;
|
|
2840
3000
|
}
|
|
2841
3001
|
|
|
3002
|
+
/* leptris-ruby#539: the ATTACHED construction faces are
|
|
3003
|
+
* wrapper-visible mutations — they splice a built child under a
|
|
3004
|
+
* parent — so they run the same freed/readonly gates and version
|
|
3005
|
+
* bump as every other write face (bump before the call: a failed
|
|
3006
|
+
* mutation merely discards memos). The raw-address face keeps the
|
|
3007
|
+
* legacy Integer-document contract (moxml's pre-#340 callers pass
|
|
3008
|
+
* doc.c_address): a bare engine address cannot reach the Document
|
|
3009
|
+
* object, so the gate runs only when the caller names it. */
|
|
2842
3010
|
static VALUE nf_create_element_with_attrs(VALUE self, VALUE document,
|
|
2843
3011
|
VALUE parent_addr, VALUE name,
|
|
2844
3012
|
VALUE attrs)
|
|
2845
3013
|
{
|
|
2846
|
-
void *doc_ptr
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
3014
|
+
void *doc_ptr;
|
|
3015
|
+
void *parent;
|
|
3016
|
+
void *elem;
|
|
3017
|
+
|
|
3018
|
+
parent = NIL_P(parent_addr)
|
|
3019
|
+
? NULL
|
|
3020
|
+
: (void *)(uintptr_t)NUM2ULL(parent_addr);
|
|
3021
|
+
if (!NIL_P(document) && !RB_INTEGER_TYPE_P(document)) {
|
|
3022
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
3023
|
+
rb_raise(c_use_after_free_error,
|
|
3024
|
+
"owning document has been freed");
|
|
3025
|
+
/* The bump rides the ATTACH: a free-floating create mutates
|
|
3026
|
+
* no tree (detached-create discipline, no bump). */
|
|
3027
|
+
if (parent)
|
|
3028
|
+
document_advance_version(document);
|
|
3029
|
+
doc_ptr = doc_ptr_of(document);
|
|
3030
|
+
} else {
|
|
3031
|
+
doc_ptr = NIL_P(document)
|
|
3032
|
+
? NULL
|
|
3033
|
+
: (void *)(uintptr_t)NUM2ULL(document);
|
|
3034
|
+
}
|
|
3035
|
+
elem = create_elem_core(doc_ptr, parent,
|
|
3036
|
+
StringValueCStr(name), attrs);
|
|
2854
3037
|
return elem ? ULL2NUM((uintptr_t)elem) : Qnil;
|
|
2855
3038
|
}
|
|
2856
3039
|
|
|
@@ -2865,15 +3048,29 @@ static VALUE nf_create_element_with_attrs_wrapped(
|
|
|
2865
3048
|
VALUE self, VALUE document, VALUE parent_addr, VALUE name,
|
|
2866
3049
|
VALUE attrs)
|
|
2867
3050
|
{
|
|
2868
|
-
void *doc_ptr
|
|
2869
|
-
void *parent
|
|
2870
|
-
|
|
2871
|
-
: (void *)(uintptr_t)NUM2ULL(parent_addr);
|
|
2872
|
-
void *elem = create_elem_core(doc_ptr, parent,
|
|
2873
|
-
StringValueCStr(name), attrs);
|
|
3051
|
+
void *doc_ptr;
|
|
3052
|
+
void *parent;
|
|
3053
|
+
void *elem;
|
|
2874
3054
|
VALUE ck, key, node, cache;
|
|
2875
3055
|
|
|
2876
3056
|
(void)self;
|
|
3057
|
+
parent = NIL_P(parent_addr)
|
|
3058
|
+
? NULL
|
|
3059
|
+
: (void *)(uintptr_t)NUM2ULL(parent_addr);
|
|
3060
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
3061
|
+
rb_raise(c_use_after_free_error,
|
|
3062
|
+
"owning document has been freed");
|
|
3063
|
+
/* #539: attached construction bumps the version — the same
|
|
3064
|
+
* discipline as append_binding_child (bump before the call; a
|
|
3065
|
+
* failed mutation merely discards memos) — so version-stamped
|
|
3066
|
+
* memos (C content, walk-reads) retire across the splice.
|
|
3067
|
+
* Free-floating creates (parent_addr 0) mutate no tree and
|
|
3068
|
+
* stay bump-free, like the detached create faces. */
|
|
3069
|
+
if (parent)
|
|
3070
|
+
document_advance_version(document);
|
|
3071
|
+
doc_ptr = doc_ptr_of(document);
|
|
3072
|
+
elem = create_elem_core(doc_ptr, parent,
|
|
3073
|
+
StringValueCStr(name), attrs);
|
|
2877
3074
|
if (!elem) return Qnil;
|
|
2878
3075
|
resolve_binding_classes();
|
|
2879
3076
|
ck = doc_klass_for_kind(document, 0 /* element */);
|
|
@@ -3715,6 +3912,7 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
|
|
|
3715
3912
|
id_iv_mx_context = rb_intern("@context");
|
|
3716
3913
|
id_iv_mx_native = rb_intern("@native");
|
|
3717
3914
|
id_iv_mx_adapter = rb_intern("@adapter");
|
|
3915
|
+
id_bulk_rows_mark = rb_intern("BulkIdentityRows");
|
|
3718
3916
|
id_iv_parent = rb_intern("@parent");
|
|
3719
3917
|
id_iv_structure_memoizable = rb_intern("@structure_memoizable");
|
|
3720
3918
|
id_iv_native_fast = rb_intern("@native_fast");
|
|
@@ -3823,7 +4021,7 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
|
|
|
3823
4021
|
rb_define_module_function(m_native, "materialize_xpath",
|
|
3824
4022
|
nf_materialize_xpath, 2);
|
|
3825
4023
|
rb_define_module_function(m_native, "xpath_result_rows_bulk",
|
|
3826
|
-
nf_xpath_result_rows_bulk,
|
|
4024
|
+
nf_xpath_result_rows_bulk, -1);
|
|
3827
4025
|
rb_define_module_function(m_native, "set_binding_name",
|
|
3828
4026
|
nf_set_binding_name, 3);
|
|
3829
4027
|
rb_define_module_function(m_native, "set_binding_text",
|
|
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
|
@@ -59,6 +59,19 @@ rescue LoadError => e
|
|
|
59
59
|
MSG
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Certificate module for the bulk XPath rows face: an Array extended
|
|
63
|
+
# with this carries ONLY born-primed registered-klass identity natives
|
|
64
|
+
# (xpath_result_rows_bulk answered with a consumer context+adapter, so
|
|
65
|
+
# @native is the node itself and @context/@adapter are already set).
|
|
66
|
+
# Consumers with a per-row wrap in their result loop can key a fast
|
|
67
|
+
# path on it (moxml's NodeSet#each); one object per native address
|
|
68
|
+
# remains the binding's native-cache contract (#529) either way.
|
|
69
|
+
module Leptris::XML::BulkIdentityRows
|
|
70
|
+
def bulk_identity_rows?
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
62
75
|
module Leptris::XML::Native
|
|
63
76
|
# FFI.libleptris_candidates is the single source of truth — the
|
|
64
77
|
# native layer MUST dlsym the exact image the FFI layer loaded,
|
|
@@ -118,10 +131,15 @@ class Leptris::XML::Document
|
|
|
118
131
|
# (nodesets free the handle in C); a non-nodeset result answers
|
|
119
132
|
# nil and leaves the pointer with the caller — route it through
|
|
120
133
|
# Searchable.wrap_xpath_result for the scalar faces.
|
|
121
|
-
|
|
134
|
+
# Optional (context, adapter): when the consumer passes its
|
|
135
|
+
# contract state, every registered-klass row is born primed and an
|
|
136
|
+
# all-native result extends with BulkIdentityRows — the consumer's
|
|
137
|
+
# per-row wrap prime disappears from the iterate-every-result lane.
|
|
138
|
+
def xpath_result_rows_bulk(result_ptr, mx_context = nil, mx_adapter = nil)
|
|
122
139
|
Leptris::XML::Native.xpath_result_rows_bulk(
|
|
123
140
|
self, result_ptr.respond_to?(:address) ? result_ptr.address
|
|
124
|
-
: result_ptr
|
|
141
|
+
: result_ptr,
|
|
142
|
+
mx_context, mx_adapter)
|
|
125
143
|
end
|
|
126
144
|
|
|
127
145
|
# Builder factories that return NativeNodes (#149): one C call
|
data/vendor-src/README.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.9.381] - 2026-10-22
|
|
4
|
+
|
|
5
|
+
The XSLT transform engine is no longer slower than libxslt on the
|
|
6
|
+
workload users actually run. The identity transform
|
|
7
|
+
(`match="@*|node()"` + `xsl:copy`) was quadratic: 13.0 s where
|
|
8
|
+
xsltproc takes 65 ms on a 20k-book catalog. Three independent causes
|
|
9
|
+
went in PR #1839, and every measured CLI row now beats xsltproc
|
|
10
|
+
end-to-end:
|
|
11
|
+
|
|
12
|
+
| stylesheet (20k-book catalog) | xsltproc 1.1.35 | 1.9.380 | 1.9.381 |
|
|
13
|
+
| --- | --- | --- | --- |
|
|
14
|
+
| identity `@*|node()` | 64.5 ms | 13,002 ms | **56.9 ms** |
|
|
15
|
+
| `copy-of //book` | 37.2 ms | 58.8 ms | **25.3 ms** |
|
|
16
|
+
| dispatch (apply-templates) | 63.8 ms | 23.5 ms | **23.6 ms** |
|
|
17
|
+
| 1,000-template sheet | 293.8 ms | 18.7 ms | **17.6 ms** |
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- apply xsi:type at every depth in whole-instance validation: an
|
|
22
|
+
instance document may pin a derived type on any element via
|
|
23
|
+
`xsi:type`; validation previously honored it only at the root, so
|
|
24
|
+
deeper particles validated against the declared type (#1838)
|
|
25
|
+
|
|
26
|
+
### Performance
|
|
27
|
+
|
|
28
|
+
- **xslt: identity transforms and copy-of were quadratic (#1839).**
|
|
29
|
+
Three fixes: (1) the pattern fast path never compiled node-type
|
|
30
|
+
tests — `node(`/`text(`/`processing-instruction(` parsed as NCName
|
|
31
|
+
and failed on the paren, sending every match through the quadratic
|
|
32
|
+
ancestor ladder (and `processing-instruction(` compared 21 of its
|
|
33
|
+
23 prefix characters); (2) the document-order rank table hashed
|
|
34
|
+
pointers by low bits, which degenerates on fixed-stride pool
|
|
35
|
+
allocations (872 ms to build for a 500k-node transform); (3) the
|
|
36
|
+
append-tail pair pointed at the deepest node a body built, so wide
|
|
37
|
+
parents re-walked their whole child chain on every append after an
|
|
38
|
+
interleaved child. Full suite 2103/2103, libxslt-205 output parity
|
|
39
|
+
included.
|
|
40
|
+
- dom: lane-18 append/create micro-levers from a fresh profile
|
|
41
|
+
(#1836)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
3
45
|
## [1.9.380] - 2026-10-22
|
|
4
46
|
|
|
5
47
|
### Fixed
|
|
@@ -106,7 +106,11 @@ Harness `wt/460-harness/build_bench.rb`; A/B pairs alternate within
|
|
|
106
106
|
ONE window (drift ±20%). Rejected within noise: fast_attach,
|
|
107
107
|
native-cache deletion, doc_klass memo, identity-probe deletion,
|
|
108
108
|
add_child alias. Residual: wrapper allocation (owner-level node
|
|
109
|
-
layout).
|
|
109
|
+
layout). **Allocation census 2026-10-22: AT FLOOR** — moxml
|
|
110
|
+
0.06 minor GC/build vs nokogiri 0.39 (6.5x fewer), 3420 objects vs
|
|
111
|
+
9907; every named suspect eliminated by the levers; the non-floor
|
|
112
|
+
remainder is 17 objects/build (~0.1% CPU). No lever remains — the
|
|
113
|
+
gap is the wrapper-count/identity-contract decision.
|
|
110
114
|
- [ ] per-element object budget: one Ruby object per created node
|
|
111
115
|
(drop the shell entirely post-create-priming; the mint cache
|
|
112
116
|
insert policy; the context registration)
|
|
@@ -138,7 +142,19 @@ three interleaved-median runs (`wt/460-harness/pipeline_bench.rb`,
|
|
|
138
142
|
build, serialize medians, fail on >5% regression)
|
|
139
143
|
|
|
140
144
|
### B5 remaining red rows
|
|
141
|
-
- [
|
|
145
|
+
- [x] pugixml lane-18 append/attr-parse — **re-dated 2026-10-22: the
|
|
146
|
+
2.0x/5.9x rows no longer exist.** The named levers shipped in
|
|
147
|
+
the prior 22-round campaign (dup-check hash PR #1413, FNV-fused
|
|
148
|
+
lookup PR #1425, append parity PR #1533). Attr-lookup
|
|
149
|
+
~parity (0.99x; caveat: in-process rigs let leptris's teardown
|
|
150
|
+
depress the adjacent pugixml row — self-warmed rigs are the
|
|
151
|
+
honest numbers). Append: **1.44x on self-warmed rigs; 1.37x
|
|
152
|
+
after PR leptris#1836** (4 behavior-identical levers: childless-
|
|
153
|
+
proof ancestor-walk skip, fused type chains, bounded name scan,
|
|
154
|
+
create_pooled double-copy fix; −3.6%, 9/10 pairs). Residual
|
|
155
|
+
~1.37x parse gap = conformance features pugixml doesn't do
|
|
156
|
+
(dup detection, error positions, NUL writes) — the measured
|
|
157
|
+
floor; owner decision in #1436.
|
|
142
158
|
- [ ] zero-copy text lever (#1218) — adopt if it wins any row
|
|
143
159
|
|
|
144
160
|
## Gate C — Safety: trust is auditable
|
|
@@ -58,7 +58,7 @@ from stale build dirs, so the harness protocol is part of the gate.
|
|
|
58
58
|
| attr listing | ≥ all | 99x — ahead |
|
|
59
59
|
| build | ≥ all | 0.87x — ahead |
|
|
60
60
|
| walk+read (moxml) | ≥ 0.95x nokogiri | **done 2026-10-19** — ~195ns name / ~193ns content, 0.90–0.95x |
|
|
61
|
-
| append / attr-parse (pugixml lane 18) | ≥ parity | **
|
|
61
|
+
| append / attr-parse (pugixml lane 18) | ≥ parity | lookup ~parity (rig-bias caveat); **append 1.37x after PR leptris#1836** (was 1.44x self-warmed) — residual = conformance floor (#1436) |
|
|
62
62
|
| DOM mutation rows | ≥ parity | RED — node-layout redesign, owner decision pending |
|
|
63
63
|
| zero-copy text (#1218 lever) | adopt if it wins any row | lever named, unmeasured |
|
|
64
64
|
|
|
@@ -59,6 +59,7 @@ LeptrisElement leptris_element_create_with_view(
|
|
|
59
59
|
memset(elem, 0, sizeof(struct leptris_element));
|
|
60
60
|
elem->base.type = LEPTRIS_NODE_TYPE_ELEMENT;
|
|
61
61
|
char* nm = leptris_sv_to_cstr_pooled(&name_view, pool);
|
|
62
|
+
if (!nm) return NULL; /* pool OOM — do not store a NULL name */
|
|
62
63
|
elem_set_name(elem, nm);
|
|
63
64
|
elem->name_hash = leptris_name_hash_compute(nm);
|
|
64
65
|
elem->name_len = (name_view.length > 254)
|
|
@@ -107,13 +108,16 @@ LeptrisElement leptris_element_create_zero_copy(LeptrisMemoryPool* pool) {
|
|
|
107
108
|
LeptrisElement leptris_element_create_pooled(const char* name, LeptrisMemoryPool* pool) {
|
|
108
109
|
if (!name || !pool) return NULL;
|
|
109
110
|
|
|
110
|
-
/*
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
*
|
|
116
|
-
|
|
111
|
+
/* Lane 18: ONE strlen + ONE pooled copy. The old route strdup'ed
|
|
112
|
+
* the name into the pool and then handed that copy to the inplace
|
|
113
|
+
* path, whose with_view call copies the SAME bytes into the pool
|
|
114
|
+
* again via sv_to_cstr_pooled — the strdup result was orphaned
|
|
115
|
+
* (never stored on the element), so every fallback-path create
|
|
116
|
+
* paid a dead strlen + pool alloc + memcpy. Building the view
|
|
117
|
+
* over the caller's buffer lands on the identical final name
|
|
118
|
+
* pointer (same pooled copy, same interning behavior). */
|
|
119
|
+
return leptris_element_create_with_view(leptris_sv_from_cstr(name),
|
|
120
|
+
pool);
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
/* Free element - pool allocated, so this is a no-op */
|
|
@@ -771,22 +775,16 @@ void leptris_element_append_child_hint_doc(LeptrisElement elem,
|
|
|
771
775
|
LeptrisNode* hint) {
|
|
772
776
|
if (!elem || !child) return;
|
|
773
777
|
|
|
774
|
-
/*
|
|
775
|
-
*
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
child->type != LEPTRIS_NODE_TYPE_TEXT &&
|
|
785
|
-
child->type != LEPTRIS_NODE_TYPE_CDATA &&
|
|
786
|
-
child->type != LEPTRIS_NODE_TYPE_COMMENT &&
|
|
787
|
-
child->type != LEPTRIS_NODE_TYPE_PI &&
|
|
788
|
-
child->type != LEPTRIS_NODE_TYPE_ENTITY_REF) {
|
|
789
|
-
return; /* Only allow these node types as children */
|
|
778
|
+
/* Type gate, fused (lane 18): the appendable set is
|
|
779
|
+
* {ELEMENT..PI} ∪ {ENTITY_REF}; the old two-chain form (a range
|
|
780
|
+
* guard followed by a six-way membership walk) admitted exactly
|
|
781
|
+
* the same set in ~8 compare/branch pairs. One load + two
|
|
782
|
+
* compares decide it. */
|
|
783
|
+
{
|
|
784
|
+
unsigned t = (unsigned)child->type;
|
|
785
|
+
if (!(t <= (unsigned)LEPTRIS_NODE_TYPE_PI ||
|
|
786
|
+
t == (unsigned)LEPTRIS_NODE_TYPE_ENTITY_REF))
|
|
787
|
+
return; /* corrupted or non-appendable node type */
|
|
790
788
|
}
|
|
791
789
|
|
|
792
790
|
/* Issue #217: if child is already attached to a parent, unlink
|
|
@@ -909,22 +907,16 @@ void leptris_element_append_child_hint_doc(LeptrisElement elem,
|
|
|
909
907
|
void leptris_element_prepend_child_internal(LeptrisElement elem, LeptrisNode* child) {
|
|
910
908
|
if (!elem || !child) return;
|
|
911
909
|
|
|
912
|
-
/*
|
|
913
|
-
*
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
child->type != LEPTRIS_NODE_TYPE_TEXT &&
|
|
923
|
-
child->type != LEPTRIS_NODE_TYPE_CDATA &&
|
|
924
|
-
child->type != LEPTRIS_NODE_TYPE_COMMENT &&
|
|
925
|
-
child->type != LEPTRIS_NODE_TYPE_PI &&
|
|
926
|
-
child->type != LEPTRIS_NODE_TYPE_ENTITY_REF) {
|
|
927
|
-
return; /* Only allow these node types as children */
|
|
910
|
+
/* Type gate, fused (lane 18): the appendable set is
|
|
911
|
+
* {ELEMENT..PI} ∪ {ENTITY_REF}; the old two-chain form (a range
|
|
912
|
+
* guard followed by a six-way membership walk) admitted exactly
|
|
913
|
+
* the same set in ~8 compare/branch pairs. One load + two
|
|
914
|
+
* compares decide it. */
|
|
915
|
+
{
|
|
916
|
+
unsigned t = (unsigned)child->type;
|
|
917
|
+
if (!(t <= (unsigned)LEPTRIS_NODE_TYPE_PI ||
|
|
918
|
+
t == (unsigned)LEPTRIS_NODE_TYPE_ENTITY_REF))
|
|
919
|
+
return; /* corrupted or non-appendable node type */
|
|
928
920
|
}
|
|
929
921
|
|
|
930
922
|
/* Issue #217: if child is already attached to a parent, unlink
|
|
@@ -183,7 +183,12 @@ LeptrisElement leptris_element_create(LeptrisDocument doc, const char* name) {
|
|
|
183
183
|
* Same init contract as leptris_element_create_with_view
|
|
184
184
|
* (memset-zero + type/name/name_hash/name_len). Falls back
|
|
185
185
|
* to the plain pool path when a block can't be allocated. */
|
|
186
|
-
|
|
186
|
+
/* Lane 18: element names are almost always tiny — a bounded
|
|
187
|
+
* byte scan resolves the length without the strlen call;
|
|
188
|
+
* anything 8+ bytes pays the same strlen as before. */
|
|
189
|
+
size_t name_len = 0;
|
|
190
|
+
while (name_len < 8 && name[name_len]) name_len++;
|
|
191
|
+
if (name_len == 8 && name[8]) name_len = strlen(name);
|
|
187
192
|
elem = mut_elem_carve(doc);
|
|
188
193
|
if (elem) {
|
|
189
194
|
char* name_copy = mut_name_carve(doc, name, name_len);
|
|
@@ -469,6 +474,11 @@ LeptrisStatus leptris_element_add_child_addr_installed(
|
|
|
469
474
|
LeptrisStatus leptris_element_append_child(LeptrisElement parent, LeptrisElement child) {
|
|
470
475
|
if (!parent || !child) return LEPTRIS_ERROR_NULL_ARG;
|
|
471
476
|
|
|
477
|
+
/* Single document resolution per mutation (TODO 195c): the
|
|
478
|
+
* root walk + map lookup was paid three times per append
|
|
479
|
+
* (twice here, once inside the internal for the tail cache). */
|
|
480
|
+
struct leptris_document* doc = leptris_element_get_document(parent);
|
|
481
|
+
|
|
472
482
|
/* #1636: appending a node into its own descendant subtree
|
|
473
483
|
* builds a parent cycle — every tree walker (serializer
|
|
474
484
|
* first) then recurses without bound and the process dies
|
|
@@ -477,9 +487,24 @@ LeptrisStatus leptris_element_append_child(LeptrisElement parent, LeptrisElement
|
|
|
477
487
|
* ancestor edge: walk child's parent chain for parent. The
|
|
478
488
|
* same-document fast path below never sees foreign copies as
|
|
479
489
|
* ancestors, so the walk runs on the caller's node — O(depth),
|
|
480
|
-
* depth bounded by the parse cap.
|
|
481
|
-
|
|
482
|
-
|
|
490
|
+
* depth bounded by the parse cap.
|
|
491
|
+
*
|
|
492
|
+
* Lane 18: the freshness probe below runs FIRST. A child is
|
|
493
|
+
* provably childless — and therefore cannot be an ancestor of
|
|
494
|
+
* parent (nor equal any node that has descendants) — only when
|
|
495
|
+
* BOTH hold: it was carved from the current mut block (recent
|
|
496
|
+
* create) AND its child list is still empty. The extent alone is
|
|
497
|
+
* NOT enough: a block-extent element may have been given
|
|
498
|
+
* children since its create, and appending an ancestor under its
|
|
499
|
+
* own descendant must keep rejecting. parent == child stays
|
|
500
|
+
* unconditional and the walk still sees the ORIGINAL child
|
|
501
|
+
* before any #1528 adoption, so accept/reject decisions are
|
|
502
|
+
* identical to the unconditioned guard. */
|
|
503
|
+
int child_fresh = node_in_current_mut_block(doc, child) &&
|
|
504
|
+
(leptris_elem_first_child(child) == NULL);
|
|
505
|
+
|
|
506
|
+
if (parent == child) return LEPTRIS_ERROR_INVALID_ARG;
|
|
507
|
+
if (!child_fresh) {
|
|
483
508
|
LeptrisElement p = leptris_element_parent(parent);
|
|
484
509
|
int guard = 0;
|
|
485
510
|
while (p && guard++ < 8192) {
|
|
@@ -488,11 +513,6 @@ LeptrisStatus leptris_element_append_child(LeptrisElement parent, LeptrisElement
|
|
|
488
513
|
}
|
|
489
514
|
}
|
|
490
515
|
|
|
491
|
-
/* Single document resolution per mutation (TODO 195c): the
|
|
492
|
-
* root walk + map lookup was paid three times per append
|
|
493
|
-
* (twice here, once inside the internal for the tail cache). */
|
|
494
|
-
struct leptris_document* doc = leptris_element_get_document(parent);
|
|
495
|
-
|
|
496
516
|
/* #1528: a foreign document's node is adopted by copy, never
|
|
497
517
|
* spliced raw. The freshly-created-then-appended shape proves
|
|
498
518
|
* same-doc in two hot loads: a child inside the CURRENT mut
|
|
@@ -500,7 +520,7 @@ LeptrisStatus leptris_element_append_child(LeptrisElement parent, LeptrisElement
|
|
|
500
520
|
* THIS document's block by the create call just executed — no
|
|
501
521
|
* foreign allocation can live inside it. Everything else takes
|
|
502
522
|
* the full check (Lane 18 append bar). */
|
|
503
|
-
if (!
|
|
523
|
+
if (!child_fresh) {
|
|
504
524
|
child = (LeptrisElement)adopt_for_splice((LeptrisNode*)child,
|
|
505
525
|
doc);
|
|
506
526
|
if (!child) return LEPTRIS_ERROR_MEMORY;
|
|
@@ -508,6 +508,21 @@ static void doc_rank_table_free(DocOrderRankTable* t) {
|
|
|
508
508
|
free(t->ranks);
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
/* Mix the pointer before masking. Pool nodes are allocated at a
|
|
512
|
+
* fixed stride, so the old (ptr >> 4) & mask sent every key to the
|
|
513
|
+
* same residue class: with a power-of-two capacity and 64/96-byte
|
|
514
|
+
* strides, only a fraction of the slots were ever reachable and
|
|
515
|
+
* linear probing degenerated. A 500k-node identity transform spent
|
|
516
|
+
* 872 ms in the rank build; mixing makes the same build O(1) per
|
|
517
|
+
* insert. */
|
|
518
|
+
static size_t doc_rank_slot(const void* key, size_t mask) {
|
|
519
|
+
uint64_t x = (uint64_t)(uintptr_t)key;
|
|
520
|
+
x ^= x >> 33;
|
|
521
|
+
x *= 0xff51afd7ed558ccdULL;
|
|
522
|
+
x ^= x >> 33;
|
|
523
|
+
return (size_t)x & mask;
|
|
524
|
+
}
|
|
525
|
+
|
|
511
526
|
/* Grow at 70% load so probe chains stay short. */
|
|
512
527
|
static int doc_rank_table_grow(DocOrderRankTable* t) {
|
|
513
528
|
size_t new_cap = (t->mask + 1) << 1;
|
|
@@ -521,7 +536,7 @@ static int doc_rank_table_grow(DocOrderRankTable* t) {
|
|
|
521
536
|
size_t new_mask = new_cap - 1;
|
|
522
537
|
for (size_t i = 0; i <= t->mask; i++) {
|
|
523
538
|
if (!t->keys[i]) continue;
|
|
524
|
-
size_t j = (
|
|
539
|
+
size_t j = doc_rank_slot(t->keys[i], new_mask);
|
|
525
540
|
while (new_keys[j]) j = (j + 1) & new_mask;
|
|
526
541
|
new_keys[j] = t->keys[i];
|
|
527
542
|
new_ranks[j] = t->ranks[i];
|
|
@@ -538,7 +553,7 @@ static int doc_rank_table_put(DocOrderRankTable* t, void* key, int64_t rank) {
|
|
|
538
553
|
if ((t->count + 1) * 10 >= (t->mask + 1) * 7) {
|
|
539
554
|
if (!doc_rank_table_grow(t)) return 0;
|
|
540
555
|
}
|
|
541
|
-
size_t i = (
|
|
556
|
+
size_t i = doc_rank_slot(key, t->mask);
|
|
542
557
|
while (t->keys[i]) {
|
|
543
558
|
if (t->keys[i] == key) return 1; /* first visit wins */
|
|
544
559
|
i = (i + 1) & t->mask;
|
|
@@ -550,7 +565,7 @@ static int doc_rank_table_put(DocOrderRankTable* t, void* key, int64_t rank) {
|
|
|
550
565
|
}
|
|
551
566
|
|
|
552
567
|
static int64_t doc_rank_table_get(const DocOrderRankTable* t, void* key) {
|
|
553
|
-
size_t i = (
|
|
568
|
+
size_t i = doc_rank_slot(key, t->mask);
|
|
554
569
|
while (t->keys[i]) {
|
|
555
570
|
if (t->keys[i] == key) return t->ranks[i];
|
|
556
571
|
i = (i + 1) & t->mask;
|
|
@@ -423,8 +423,25 @@ static void validate_children(struct xsd_validator* v,
|
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
425
|
}
|
|
426
|
-
|
|
426
|
+
/* leptris-ruby#535 (XSD 1.0 2.6.2): xsi:type applies at every
|
|
427
|
+
* assessed depth -- a child typed by the parent particle is
|
|
428
|
+
* substituted exactly like a globally declared one (which
|
|
429
|
+
* validate_element above already handles). */
|
|
430
|
+
const char* cxsi = leptris_element_attribute_ns(
|
|
431
|
+
ce, "http://www.w3.org/2001/XMLSchema-instance", "type");
|
|
432
|
+
if (cxsi && cxsi[0] && ptype) {
|
|
433
|
+
char xuri[512], xlocal[256];
|
|
434
|
+
xsd_resolve_qname_inst(ce, cxsi, xuri, sizeof(xuri),
|
|
435
|
+
xlocal, sizeof(xlocal));
|
|
436
|
+
if (!xlocal[0]) {
|
|
437
|
+
verrf_at(v, ce, "element", cl,
|
|
438
|
+
"xsi:type prefix is not bound in the instance");
|
|
439
|
+
} else {
|
|
440
|
+
validate_local_uri(v, s, ce, cl, xlocal, xuri, cxsi);
|
|
441
|
+
}
|
|
442
|
+
} else if (ptype) {
|
|
427
443
|
validate_local(v, s, ce, cl, ptype);
|
|
444
|
+
}
|
|
428
445
|
}
|
|
429
446
|
}
|
|
430
447
|
|
|
@@ -1011,6 +1011,15 @@ static int op_result_elem(XsltExec* ex, const XsltInstr* in,
|
|
|
1011
1011
|
}
|
|
1012
1012
|
}
|
|
1013
1013
|
ex->pending_parent = parent;
|
|
1014
|
+
/* The content ran only inside e (append-only, strictly nested),
|
|
1015
|
+
* so e is `parent`'s newest child — hand it back as the next
|
|
1016
|
+
* append's tail hint (the single-entry pair otherwise points at
|
|
1017
|
+
* the deepest node the body built, and each append to a wide
|
|
1018
|
+
* parent then walks its whole child chain, #682). */
|
|
1019
|
+
if (parent) {
|
|
1020
|
+
ex->last_append_parent = parent;
|
|
1021
|
+
ex->last_append_child = (LeptrisNodeRef)e;
|
|
1022
|
+
}
|
|
1014
1023
|
return rc;
|
|
1015
1024
|
}
|
|
1016
1025
|
|
|
@@ -2691,6 +2700,16 @@ static int op_copy(XsltExec* ex, const XsltInstr* in, LeptrisElement node) {
|
|
|
2691
2700
|
ex->pending_parent = e;
|
|
2692
2701
|
int rc = xslt_exec_instrs(ex, in->child, node);
|
|
2693
2702
|
ex->pending_parent = saved;
|
|
2703
|
+
/* Result-tree construction is append-only and strictly nested,
|
|
2704
|
+
* so `e` is `saved`'s newest child when the body returns — hand
|
|
2705
|
+
* it back as the next append's tail hint. The single-entry pair
|
|
2706
|
+
* otherwise points at the deepest node the body built, and each
|
|
2707
|
+
* append to a wide parent then walks that parent's whole child
|
|
2708
|
+
* chain (quadratic identity transforms, #682). */
|
|
2709
|
+
if (saved) {
|
|
2710
|
+
ex->last_append_parent = saved;
|
|
2711
|
+
ex->last_append_child = (LeptrisNodeRef)e;
|
|
2712
|
+
}
|
|
2694
2713
|
return rc;
|
|
2695
2714
|
}
|
|
2696
2715
|
|
|
@@ -2763,21 +2782,27 @@ static int copy_node_deep(XsltExec* ex, LeptrisElement node,
|
|
|
2763
2782
|
LeptrisNodeRef cc = (LeptrisNodeRef)leptris_cdata_create(
|
|
2764
2783
|
t, t ? strlen(t) : 0,
|
|
2765
2784
|
((struct leptris_document*)ex->result)->pool);
|
|
2766
|
-
if (cc)
|
|
2785
|
+
if (cc) out_append_child(ex, e, (void*)cc);
|
|
2767
2786
|
} else if (ty == LEPTRIS_NODE_TYPE_COMMENT) {
|
|
2768
2787
|
LeptrisNodeRef cm = leptris_comment_node_create(
|
|
2769
2788
|
ex->result, leptris_comment_node_get_content(c));
|
|
2770
|
-
if (cm)
|
|
2789
|
+
if (cm) out_append_child(ex, e, (void*)cm);
|
|
2771
2790
|
} else if (ty == LEPTRIS_NODE_TYPE_PI) {
|
|
2772
2791
|
LeptrisNodeRef pi = leptris_pi_node_create(
|
|
2773
2792
|
ex->result, leptris_pi_node_get_target(c),
|
|
2774
2793
|
leptris_pi_node_get_data(c));
|
|
2775
|
-
if (pi)
|
|
2794
|
+
if (pi) out_append_child(ex, e, (void*)pi);
|
|
2776
2795
|
} else if (ty == LEPTRIS_NODE_TYPE_TEXT) {
|
|
2777
2796
|
const char* t = leptris_text_get_content((LeptrisTextNode*)c);
|
|
2778
2797
|
if (t && t[0]) out_append_text(ex, e, t);
|
|
2779
2798
|
}
|
|
2780
2799
|
}
|
|
2800
|
+
/* The caller's newest child is e — hand it back as the tail
|
|
2801
|
+
* hint so sibling copies append O(1) (see op_copy). */
|
|
2802
|
+
if (parent) {
|
|
2803
|
+
ex->last_append_parent = parent;
|
|
2804
|
+
ex->last_append_child = (LeptrisNodeRef)e;
|
|
2805
|
+
}
|
|
2781
2806
|
return 0;
|
|
2782
2807
|
}
|
|
2783
2808
|
|
|
@@ -3515,6 +3540,10 @@ static int builtin_no_match(XsltExec* ex, const char* mode,
|
|
|
3515
3540
|
&(XsltInstr){ .kind = XSLT_INSTR_APPLY_TEMPLATES, .name = mode },
|
|
3516
3541
|
node);
|
|
3517
3542
|
ex->pending_parent = saved_parent;
|
|
3543
|
+
if (saved_parent && e) {
|
|
3544
|
+
ex->last_append_parent = saved_parent;
|
|
3545
|
+
ex->last_append_child = (LeptrisNodeRef)e;
|
|
3546
|
+
}
|
|
3518
3547
|
return rc;
|
|
3519
3548
|
}
|
|
3520
3549
|
|
|
@@ -3876,6 +3905,16 @@ static int op_element(XsltExec* ex, const XsltInstr* in,
|
|
|
3876
3905
|
ex->pending_parent = e;
|
|
3877
3906
|
int rc = xslt_exec_instrs(ex, in->child, node);
|
|
3878
3907
|
ex->pending_parent = saved;
|
|
3908
|
+
/* Result-tree construction is append-only and strictly nested,
|
|
3909
|
+
* so `e` is `saved`'s newest child when the body returns — hand
|
|
3910
|
+
* it back as the next append's tail hint. The single-entry pair
|
|
3911
|
+
* otherwise points at the deepest node the body built, and each
|
|
3912
|
+
* append to a wide parent then walks that parent's whole child
|
|
3913
|
+
* chain (quadratic identity transforms, #682). */
|
|
3914
|
+
if (saved) {
|
|
3915
|
+
ex->last_append_parent = saved;
|
|
3916
|
+
ex->last_append_child = (LeptrisNodeRef)e;
|
|
3917
|
+
}
|
|
3879
3918
|
return rc;
|
|
3880
3919
|
}
|
|
3881
3920
|
|
|
@@ -59,6 +59,22 @@ static int parse_pat_step(const char** pp, const char* end,
|
|
|
59
59
|
if (*p == '@') { s->is_attr = 1; p++; }
|
|
60
60
|
if (p < end && *p == '*') {
|
|
61
61
|
p++;
|
|
62
|
+
/* Node-type tests FIRST: "node(" also starts with an NCName, so
|
|
63
|
+
* the name branch below would swallow the keyword and then fail
|
|
64
|
+
* on its '(' — leaving steps_valid=0 and sending every match of
|
|
65
|
+
* node()/text()/comment()/processing-instruction() into the
|
|
66
|
+
* ancestor ladder (O(width) per node: the identity transform's
|
|
67
|
+
* @*|node() went quadratic). The bare names "node"/"text" still
|
|
68
|
+
* parse as name tests: the keyword check requires the '('. */
|
|
69
|
+
} else if (p + 5 <= end && strncmp(p, "node(", 5) == 0) {
|
|
70
|
+
s->kind = 1; p += 5;
|
|
71
|
+
} else if (p + 5 <= end && strncmp(p, "text(", 5) == 0) {
|
|
72
|
+
s->kind = 2; p += 5;
|
|
73
|
+
} else if (p + 8 <= end && strncmp(p, "comment(", 8) == 0) {
|
|
74
|
+
s->kind = 3; p += 8;
|
|
75
|
+
} else if (p + 23 <= end &&
|
|
76
|
+
strncmp(p, "processing-instruction(", 23) == 0) {
|
|
77
|
+
s->kind = 4; p += 23;
|
|
62
78
|
} else if (p < end && ncname_char(*p) && *p != '-') {
|
|
63
79
|
const char* st = p;
|
|
64
80
|
while (p < end && ncname_char(*p)) p++;
|
|
@@ -72,15 +88,6 @@ static int parse_pat_step(const char** pp, const char* end,
|
|
|
72
88
|
if (!s->name) return 0;
|
|
73
89
|
memcpy(s->name, st, n);
|
|
74
90
|
s->name[n] = 0;
|
|
75
|
-
} else if (p + 5 <= end && strncmp(p, "node(", 5) == 0) {
|
|
76
|
-
s->kind = 1; p += 5;
|
|
77
|
-
} else if (p + 5 <= end && strncmp(p, "text(", 5) == 0) {
|
|
78
|
-
s->kind = 2; p += 5;
|
|
79
|
-
} else if (p + 8 <= end && strncmp(p, "comment(", 8) == 0) {
|
|
80
|
-
s->kind = 3; p += 8;
|
|
81
|
-
} else if (p + 21 <= end &&
|
|
82
|
-
strncmp(p, "processing-instruction(", 23 - 2) == 0) {
|
|
83
|
-
s->kind = 4; p += 21;
|
|
84
91
|
} else {
|
|
85
92
|
return 0;
|
|
86
93
|
}
|
|
@@ -2966,6 +2966,48 @@ TEST(DomCycleGuard, AncestorAppendRejected) {
|
|
|
2966
2966
|
leptris_document_free(d);
|
|
2967
2967
|
}
|
|
2968
2968
|
|
|
2969
|
+
/* Lane 18: the append path skips the #1636 ancestor walk when the
|
|
2970
|
+
* child is provably childless (fresh mut-block carve AND empty
|
|
2971
|
+
* child list). An element still sitting inside the mut-block
|
|
2972
|
+
* extent is NOT automatically childless — it may have been given
|
|
2973
|
+
* children since its create — so the walk must still run for it.
|
|
2974
|
+
* Regression pin for the first cut, which keyed the skip on the
|
|
2975
|
+
* extent alone and ACCEPTED root-under-b for mutation-built
|
|
2976
|
+
* trees (parse-built trees never enter the mut block, which is
|
|
2977
|
+
* why the parse-only specs above could not see it). */
|
|
2978
|
+
TEST(DomCycleGuard, AncestorAppendRejectedInMutBlockExtent) {
|
|
2979
|
+
LeptrisDocument d = leptris_document_create();
|
|
2980
|
+
ASSERT_NE(d, nullptr);
|
|
2981
|
+
LeptrisElement root = leptris_element_create(d, "root");
|
|
2982
|
+
ASSERT_NE(root, nullptr);
|
|
2983
|
+
ASSERT_EQ(leptris_document_set_root(d, root), LEPTRIS_OK);
|
|
2984
|
+
LeptrisElement a = leptris_element_create(d, "a");
|
|
2985
|
+
LeptrisElement b = leptris_element_create(d, "b");
|
|
2986
|
+
ASSERT_NE(a, nullptr);
|
|
2987
|
+
ASSERT_NE(b, nullptr);
|
|
2988
|
+
ASSERT_EQ(leptris_element_append_child(root, a), LEPTRIS_OK);
|
|
2989
|
+
ASSERT_EQ(leptris_element_append_child(a, b), LEPTRIS_OK);
|
|
2990
|
+
|
|
2991
|
+
/* all three still sit in the current mut-block extent here —
|
|
2992
|
+
* the guard must not trust the extent as childlessness */
|
|
2993
|
+
EXPECT_EQ(leptris_element_append_child(b, root),
|
|
2994
|
+
LEPTRIS_ERROR_INVALID_ARG);
|
|
2995
|
+
EXPECT_EQ(leptris_element_append_child(a, a),
|
|
2996
|
+
LEPTRIS_ERROR_INVALID_ARG);
|
|
2997
|
+
EXPECT_EQ(leptris_element_prepend_child(b, root),
|
|
2998
|
+
LEPTRIS_ERROR_INVALID_ARG);
|
|
2999
|
+
|
|
3000
|
+
/* fresh create->append (the fast-path shape) still links */
|
|
3001
|
+
LeptrisElement c = leptris_element_create(d, "c");
|
|
3002
|
+
ASSERT_NE(c, nullptr);
|
|
3003
|
+
EXPECT_EQ(leptris_element_append_child(b, c), LEPTRIS_OK);
|
|
3004
|
+
char* out = leptris_document_serialize(d, nullptr);
|
|
3005
|
+
ASSERT_NE(out, nullptr);
|
|
3006
|
+
EXPECT_NE(strstr(out, "<b><c/></b>"), nullptr);
|
|
3007
|
+
leptris_free_string(out);
|
|
3008
|
+
leptris_document_free(d);
|
|
3009
|
+
}
|
|
3010
|
+
|
|
2969
3011
|
TEST(DomCycleGuard, InsertIntoOwnSubtreeRejected) {
|
|
2970
3012
|
LeptrisStatus st = LEPTRIS_OK;
|
|
2971
3013
|
LeptrisDocument d =
|
|
@@ -2855,4 +2855,94 @@ TEST(XsdConformanceClauses, XsiTypeUnknownTypeIsLoud) {
|
|
|
2855
2855
|
leptris_xsd_free(s);
|
|
2856
2856
|
}
|
|
2857
2857
|
|
|
2858
|
+
TEST(XsdConformanceClauses, XsiTypeAtDepthGlobalChild) {
|
|
2859
|
+
const char schema[] =
|
|
2860
|
+
"<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\""
|
|
2861
|
+
" targetNamespace=\"urn:t\" xmlns=\"urn:t\""
|
|
2862
|
+
" elementFormDefault=\"qualified\">"
|
|
2863
|
+
" <xs:complexType name=\"Base\">"
|
|
2864
|
+
" <xs:sequence><xs:element name=\"a\" type=\"xs:string\"/></xs:sequence>"
|
|
2865
|
+
" </xs:complexType>"
|
|
2866
|
+
" <xs:complexType name=\"Derived\">"
|
|
2867
|
+
" <xs:complexContent>"
|
|
2868
|
+
" <xs:extension base=\"Base\">"
|
|
2869
|
+
" <xs:sequence><xs:element name=\"b\" type=\"xs:string\"/></xs:sequence>"
|
|
2870
|
+
" </xs:extension>"
|
|
2871
|
+
" </xs:complexContent>"
|
|
2872
|
+
" </xs:complexType>"
|
|
2873
|
+
" <xs:element name=\"doc\" type=\"Base\"/>"
|
|
2874
|
+
" <xs:element name=\"wrap\">"
|
|
2875
|
+
" <xs:complexType>"
|
|
2876
|
+
" <xs:sequence><xs:element ref=\"doc\"/></xs:sequence>"
|
|
2877
|
+
" </xs:complexType>"
|
|
2878
|
+
" </xs:element>"
|
|
2879
|
+
"</xs:schema>";
|
|
2880
|
+
LeptrisXsdSchema s = compile_string(schema);
|
|
2881
|
+
ASSERT_NE(s, (LeptrisXsdSchema)0);
|
|
2882
|
+
/* leptris-ruby#535 depth case, global-declaration child: the
|
|
2883
|
+
* child walk re-enters validate_element, which reads xsi:type.
|
|
2884
|
+
* The prefix resolves through the ROOT's declaration -- in
|
|
2885
|
+
* scope, not just the bearing element's own. */
|
|
2886
|
+
EXPECT_EQ(validate_compiled(
|
|
2887
|
+
s,
|
|
2888
|
+
"<wrap xmlns=\"urn:t\" xmlns:i=\"urn:t\""
|
|
2889
|
+
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
|
|
2890
|
+
"<doc xsi:type=\"i:Derived\"><a>x</a><b>y</b></doc></wrap>"),
|
|
2891
|
+
1)
|
|
2892
|
+
<< "xsi:type must be honored on a global child below the root";
|
|
2893
|
+
/* the declared type governs without xsi:type: b is out of Base. */
|
|
2894
|
+
EXPECT_EQ(validate_compiled(
|
|
2895
|
+
s, "<wrap xmlns=\"urn:t\"><doc><a>x</a><b>y</b></doc></wrap>"),
|
|
2896
|
+
0);
|
|
2897
|
+
leptris_xsd_free(s);
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
TEST(XsdConformanceClauses, XsiTypeAtDepthParticleTypedChild) {
|
|
2901
|
+
const char schema[] =
|
|
2902
|
+
"<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\""
|
|
2903
|
+
" targetNamespace=\"urn:t\" xmlns=\"urn:t\""
|
|
2904
|
+
" elementFormDefault=\"qualified\">"
|
|
2905
|
+
" <xs:complexType name=\"Base\">"
|
|
2906
|
+
" <xs:sequence><xs:element name=\"a\" type=\"xs:string\"/></xs:sequence>"
|
|
2907
|
+
" </xs:complexType>"
|
|
2908
|
+
" <xs:complexType name=\"Derived\">"
|
|
2909
|
+
" <xs:complexContent>"
|
|
2910
|
+
" <xs:extension base=\"Base\">"
|
|
2911
|
+
" <xs:sequence><xs:element name=\"b\" type=\"xs:string\"/></xs:sequence>"
|
|
2912
|
+
" </xs:extension>"
|
|
2913
|
+
" </xs:complexContent>"
|
|
2914
|
+
" </xs:complexType>"
|
|
2915
|
+
" <xs:element name=\"wrap\">"
|
|
2916
|
+
" <xs:complexType>"
|
|
2917
|
+
" <xs:sequence><xs:element name=\"doc\" type=\"Base\"/></xs:sequence>"
|
|
2918
|
+
" </xs:complexType>"
|
|
2919
|
+
" </xs:element>"
|
|
2920
|
+
"</xs:schema>";
|
|
2921
|
+
LeptrisXsdSchema s = compile_string(schema);
|
|
2922
|
+
ASSERT_NE(s, (LeptrisXsdSchema)0);
|
|
2923
|
+
/* leptris-ruby#535 depth case, local particle: no global `doc`
|
|
2924
|
+
* exists, so the child is typed by wrap's particle -- xsi:type
|
|
2925
|
+
* must still substitute Derived's model at that depth. */
|
|
2926
|
+
EXPECT_EQ(validate_compiled(
|
|
2927
|
+
s,
|
|
2928
|
+
"<wrap xmlns=\"urn:t\" xmlns:i=\"urn:t\""
|
|
2929
|
+
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
|
|
2930
|
+
"<doc xsi:type=\"i:Derived\"><a>x</a><b>y</b></doc></wrap>"),
|
|
2931
|
+
1)
|
|
2932
|
+
<< "xsi:type must substitute on a particle-typed child too";
|
|
2933
|
+
/* the named type's own constraint applies: Derived needs b. */
|
|
2934
|
+
EXPECT_EQ(validate_compiled(
|
|
2935
|
+
s,
|
|
2936
|
+
"<wrap xmlns=\"urn:t\" xmlns:i=\"urn:t\""
|
|
2937
|
+
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
|
|
2938
|
+
"<doc xsi:type=\"i:Derived\"><a>x</a></doc></wrap>"),
|
|
2939
|
+
0);
|
|
2940
|
+
/* without xsi:type the declared particle type governs: b is out
|
|
2941
|
+
* of Base's model. */
|
|
2942
|
+
EXPECT_EQ(validate_compiled(
|
|
2943
|
+
s, "<wrap xmlns=\"urn:t\"><doc><a>x</a><b>y</b></doc></wrap>"),
|
|
2944
|
+
0);
|
|
2945
|
+
leptris_xsd_free(s);
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2858
2948
|
} // namespace
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json",
|
|
3
3
|
"name": "leptris",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.381",
|
|
5
5
|
"description": "High-performance XML parser and XPath 1.0 engine in C (libleptris)",
|
|
6
6
|
"homepage": "https://github.com/leptris/leptris",
|
|
7
7
|
"license": "MIT",
|