leptris 1.9.381.0-arm-linux → 1.9.382.0-arm-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/Rakefile +9 -2
- data/ext/leptris/native/native.c +89 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/element.rb +11 -1
- data/lib/leptris/xml/ffi.rb +18 -0
- data/lib/leptris/xml/native.so +0 -0
- data/lib/leptris/xml/native_3_3.so +0 -0
- data/lib/leptris/xml/xsd.rb +54 -0
- data/lib/libleptris.so +0 -0
- data/vendor-src/README.md +1 -1
- data/vendor-src/libleptris/CHANGELOG.md +28 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/TODO.max-perf/00-INDEX.md +4 -0
- data/vendor-src/libleptris/TODO.max-perf/15-c-face-vs-pugixml.md +87 -0
- data/vendor-src/libleptris/src/include/leptris.h +41 -0
- data/vendor-src/libleptris/src/leptris/common/simd_text.c +44 -0
- data/vendor-src/libleptris/src/leptris/common/simd_text.h +13 -0
- data/vendor-src/libleptris/src/leptris/common/simd_text_neon.c +51 -0
- data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +54 -13
- data/vendor-src/libleptris/src/leptris/leptris.c +12 -7
- data/vendor-src/libleptris/src/leptris/leptris_internal.h +7 -0
- data/vendor-src/libleptris/src/leptris/memory/arena.c +3 -2
- data/vendor-src/libleptris/src/leptris/rng/rng_regex.c +411 -17
- data/vendor-src/libleptris/src/leptris/xpath/evaluator_path.c +116 -1
- data/vendor-src/libleptris/src/leptris/xpath/vm.c +7 -7
- data/vendor-src/libleptris/src/leptris/xsd/compile.c +105 -2
- data/vendor-src/libleptris/src/leptris/xsd/content_model.c +45 -4
- data/vendor-src/libleptris/src/leptris/xsd/validate.c +113 -42
- data/vendor-src/libleptris/src/leptris/xsd/xsd_internal.h +24 -0
- data/vendor-src/libleptris/test/parser/test_simd_count.cpp +68 -0
- data/vendor-src/libleptris/test/rng/test_rng.cpp +69 -1
- data/vendor-src/libleptris/test/xpath/test_xpath.cpp +8 -3
- data/vendor-src/libleptris/test/xsd/test_xsd_compile.cpp +220 -0
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eeb080a56ba2f644ba7219077f866627006b2d5e8ea80405a86a17cff5853737
|
|
4
|
+
data.tar.gz: 8498da423561b35ddfabe900ae828df3ee9f9963c1498ac90cbb543f73c4c58e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 750842a780c631c64e41614fa9f6b1a812bec0cffe9965fa868967a0b0375b16d9b1466279a07b990a90bbb0f14ee307c3868e9f4dd1702c1436f1af54d3d3f7
|
|
7
|
+
data.tar.gz: 16cfef2895a0d2b8ccfb5324492183111dacf30e4db0261f310418a3f2f97dbd6c68b24828858573ecd5062ba2ec9e3fa0f83c5904e8b769234dde45f9b7e86b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
### Added — 1.9.382.0 (rides libleptris 1.9.382)
|
|
4
|
+
|
|
5
|
+
- **Schema#version** (moxml#460 item 2): the effective schema
|
|
6
|
+
version — "1.1" when the root declares `@version="1.1"` (1.1
|
|
7
|
+
semantics apply: `xs:alternative` CTA, `xs:assert`), else
|
|
8
|
+
"1.0". Engine face leptris_xsd_version (leptris/leptris#1849).
|
|
9
|
+
- **Native.name_equals** (leptris/leptris#1855 ask 2): engine-side
|
|
10
|
+
name comparison — no String materialization for name-comparing
|
|
11
|
+
consumers. Soft-gated; Qnil for non-element addresses.
|
|
12
|
+
- **Soft-attached faces** (leptris-ruby#548): version-gated symbols
|
|
13
|
+
no longer break the load on older engines; the symbol audit
|
|
14
|
+
exempts them until the pin reaches their release.
|
|
15
|
+
|
|
3
16
|
### Fixed — engine ride
|
|
4
17
|
|
|
18
|
+
- **Ride libleptris 1.9.382**: XSD structured error rows
|
|
19
|
+
(leptris/leptris#1843), negative wildcards (#1850), block +
|
|
20
|
+
category escapes in the pattern regex (#1853/#1856/#1857), the
|
|
21
|
+
parse-perf campaign work (#1844/#1847/#1848/#1851), and the
|
|
22
|
+
CAS-only retain spinlock.
|
|
23
|
+
- **Ride libleptris 1.9.380**: the xsi:type conformance fix
|
|
24
|
+
(leptris/leptris#1832, lutaml/moxml#433) — an element carrying
|
|
25
|
+
`xsi:type` is assessed against the NAMED type (XSD 1.0
|
|
26
|
+
§2.6.2/§3.3.4), so valid xsi:type-derived instances validate
|
|
27
|
+
instead of being rejected against the declared type's content
|
|
28
|
+
model. Released 1.9.380.0 with the pin still at 1.9.379 (the
|
|
29
|
+
vendored dylib missed the fix); this pin bump is the real ride.
|
|
30
|
+
|
|
5
31
|
- **Ride libleptris 1.9.380**: the xsi:type conformance fix
|
|
6
32
|
(leptris/leptris#1832, lutaml/moxml#433) — an element carrying
|
|
7
33
|
`xsi:type` is assessed against the NAMED type (XSD 1.0
|
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.382"
|
|
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
|
|
@@ -401,8 +401,15 @@ namespace :audit do
|
|
|
401
401
|
.map { |n| n.sub(/\Aleptris_/, "") }.sort
|
|
402
402
|
attached = File.read("lib/leptris/xml/ffi.rb")
|
|
403
403
|
.scan(/attach_function :leptris_([a-z_0-9]+)/).flatten.sort
|
|
404
|
+
# Version-gated faces: soft-attached because engines BEFORE the
|
|
405
|
+
# named release lack the symbol (the load must survive them).
|
|
406
|
+
# They join the audit only when the engine pin reaches that
|
|
407
|
+
# release (the lockstep ride). TODO.concurrency/08-era law:
|
|
408
|
+
# never let one forward symbol poison the load.
|
|
409
|
+
version_gated = []
|
|
410
|
+
version_gated << "xsd_version" if Gem::Version.new(LIBLEPTRIS_VERSION) < Gem::Version.new("1.9.382")
|
|
404
411
|
unattached = exported - attached
|
|
405
|
-
unexported = attached - exported
|
|
412
|
+
unexported = attached - exported - version_gated
|
|
406
413
|
unless unattached.empty?
|
|
407
414
|
puts "exported but NOT attached (upstream surface drift):"
|
|
408
415
|
unattached.each { |s| puts " leptris_#{s}" }
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -3094,6 +3094,80 @@ static VALUE nf_create_element_with_attrs_wrapped(
|
|
|
3094
3094
|
return node;
|
|
3095
3095
|
}
|
|
3096
3096
|
|
|
3097
|
+
/* TODO.sota/04 (moxml build row): ONE construction crossing for the
|
|
3098
|
+
* consumer build shape — create + attrs + attach + optional text +
|
|
3099
|
+
* version bump + consumer mint, all in C off the PARENT WRAPPER's
|
|
3100
|
+
* struct (the document comes from the struct: no FFI twin, no
|
|
3101
|
+
* bridge, no per-element document climb on the Ruby side). Nokogiri's
|
|
3102
|
+
* equivalent shape is five separate crossings (Node.new, []= x2,
|
|
3103
|
+
* content=, add_child) + five wrapper allocs; this face is one
|
|
3104
|
+
* crossing + one wrapper. attrs arrives FLATTENED (name, value, ...)
|
|
3105
|
+
* like create_element_with_attrs_wrapped; text may be nil. */
|
|
3106
|
+
static VALUE nn_add_element_full(VALUE self, VALUE name, VALUE attrs,
|
|
3107
|
+
VALUE text)
|
|
3108
|
+
{
|
|
3109
|
+
struct native_node *pn;
|
|
3110
|
+
VALUE doc, ck, node;
|
|
3111
|
+
void *doc_ptr, *elem;
|
|
3112
|
+
int st;
|
|
3113
|
+
|
|
3114
|
+
resolve_binding_classes();
|
|
3115
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, pn);
|
|
3116
|
+
doc = pn->document;
|
|
3117
|
+
if (NIL_P(doc) || NIL_P(rb_ivar_get(doc, id_iv_c_address)))
|
|
3118
|
+
rb_raise(c_use_after_free_error,
|
|
3119
|
+
"owning document has been freed");
|
|
3120
|
+
if (rb_ivar_get(doc, id_iv_readonly) == Qtrue)
|
|
3121
|
+
rb_raise(c_readonly_error,
|
|
3122
|
+
"document is readonly — mutation attempted");
|
|
3123
|
+
/* Bump before the call: a failed construction merely discards
|
|
3124
|
+
* memos (the #539 discipline). */
|
|
3125
|
+
rb_ivar_set(doc, id_iv_version,
|
|
3126
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(doc, id_iv_version)) + 1));
|
|
3127
|
+
doc_ptr = doc_ptr_of(doc);
|
|
3128
|
+
elem = create_elem_core(doc_ptr, pn->ptr,
|
|
3129
|
+
StringValueCStr(name), attrs);
|
|
3130
|
+
if (!elem) return Qnil;
|
|
3131
|
+
if (!NIL_P(text)) {
|
|
3132
|
+
st = f_elem_set_text(elem, StringValueCStr(text));
|
|
3133
|
+
check_status_c(st);
|
|
3134
|
+
}
|
|
3135
|
+
ck = doc_klass_for_kind(doc, 0 /* element */);
|
|
3136
|
+
if (!NIL_P(ck)) {
|
|
3137
|
+
node = mint_consumer_node(ck, doc, elem);
|
|
3138
|
+
/* The #508 identity contract, born-primed from the parent
|
|
3139
|
+
* wrapper: @native is the node itself and the contract state
|
|
3140
|
+
* (@context/@adapter) copies off self, so the consumer's
|
|
3141
|
+
* per-child wrap prime disappears from the build row. */
|
|
3142
|
+
rb_ivar_set(node, id_iv_mx_native, node);
|
|
3143
|
+
if (NIL_P(rb_ivar_get(node, id_iv_mx_context))) {
|
|
3144
|
+
VALUE cx = rb_ivar_get(self, id_iv_mx_context);
|
|
3145
|
+
if (!NIL_P(cx)) {
|
|
3146
|
+
VALUE ad = rb_ivar_get(self, id_iv_mx_adapter);
|
|
3147
|
+
rb_ivar_set(node, id_iv_mx_context, cx);
|
|
3148
|
+
if (!NIL_P(ad)) rb_ivar_set(node, id_iv_mx_adapter, ad);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
return node;
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
node = rb_hash_aref(native_cache_of(doc),
|
|
3155
|
+
ULL2NUM((uint64_t)(uintptr_t)elem));
|
|
3156
|
+
if (!NIL_P(node)) return node;
|
|
3157
|
+
node = rb_obj_alloc(c_b_element);
|
|
3158
|
+
rb_iv_set(node, "@c_address", ULL2NUM((uint64_t)(uintptr_t)elem));
|
|
3159
|
+
rb_iv_set(node, "@document", doc);
|
|
3160
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
3161
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
3162
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
3163
|
+
rb_iv_set(node, "@pub_document", doc);
|
|
3164
|
+
rb_iv_set(node, "@addr_reads_fast", Qtrue);
|
|
3165
|
+
rb_iv_set(node, "@node_type", INT2FIX(0));
|
|
3166
|
+
rb_hash_aset(native_cache_of(doc),
|
|
3167
|
+
ULL2NUM((uint64_t)(uintptr_t)elem), node);
|
|
3168
|
+
return node;
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3097
3171
|
static VALUE nf_attribute_rows(VALUE self, VALUE document, VALUE addr)
|
|
3098
3172
|
{
|
|
3099
3173
|
(void)self;
|
|
@@ -3539,6 +3613,18 @@ static VALUE nf_name(VALUE self, VALUE addr)
|
|
|
3539
3613
|
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
3540
3614
|
}
|
|
3541
3615
|
|
|
3616
|
+
/* moxml#460/#1855 ask 2: engine-side name comparison — no String
|
|
3617
|
+
* materialization for name-comparing consumers. Qtrue/Qfalse;
|
|
3618
|
+
* Qnil when the address is not an element (mirrors nf_name). */
|
|
3619
|
+
static VALUE nf_name_equals(VALUE self, VALUE addr, VALUE name)
|
|
3620
|
+
{
|
|
3621
|
+
const char *s;
|
|
3622
|
+
(void)self;
|
|
3623
|
+
s = f_elem_name((void *)(uintptr_t)NUM2ULL(addr));
|
|
3624
|
+
if (!s) return Qnil;
|
|
3625
|
+
return strcmp(s, StringValueCStr(name)) == 0 ? Qtrue : Qfalse;
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3542
3628
|
static VALUE nf_element_text(VALUE self, VALUE addr)
|
|
3543
3629
|
{
|
|
3544
3630
|
const char *s;
|
|
@@ -3965,6 +4051,8 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
|
|
|
3965
4051
|
rb_define_alias(c_native_node, "text_read", "content");
|
|
3966
4052
|
rb_define_alias(c_native_node, "name_read", "name");
|
|
3967
4053
|
rb_define_method(c_native_node, "children", nn_children, 0);
|
|
4054
|
+
rb_define_method(c_native_node, "add_element_full",
|
|
4055
|
+
nn_add_element_full, 3);
|
|
3968
4056
|
rb_define_alias(c_native_node, "children_read", "children");
|
|
3969
4057
|
/* Unshadowed write face (the add_child_native pattern): the
|
|
3970
4058
|
* registered Identity klasses shadow content= with the
|
|
@@ -3981,6 +4069,7 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
|
|
|
3981
4069
|
m_native = rb_define_module_under(m_xml, "Native");
|
|
3982
4070
|
m_native_sentinel = m_native;
|
|
3983
4071
|
rb_define_module_function(m_native, "fast_name", nf_name, 1);
|
|
4072
|
+
rb_define_module_function(m_native, "name_equals", nf_name_equals, 2);
|
|
3984
4073
|
id_sax_start_document = rb_intern("start_document");
|
|
3985
4074
|
id_sax_end_document = rb_intern("end_document");
|
|
3986
4075
|
id_sax_start_element = rb_intern("start_element");
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -548,11 +548,17 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
548
548
|
# Cross-document and docless (#376): full path first (lift +
|
|
549
549
|
# engine copy), then the source original is removed; returns
|
|
550
550
|
# the installed copy (self's new next sibling).
|
|
551
|
+
# #543: the raw-FFI lanes are wrapper-visible mutations too —
|
|
552
|
+
# they must advance the document version like the C-bound face,
|
|
553
|
+
# or every #530 walk-read memo warmed before the insert serves
|
|
554
|
+
# stale children afterwards (bump before the splice: a failed
|
|
555
|
+
# mutation merely discards memos).
|
|
551
556
|
if Leptris::XML::Element.foreign_to?(node, @document)
|
|
552
557
|
ensure_writable!
|
|
553
558
|
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
554
559
|
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces_for_sibling)
|
|
555
560
|
end
|
|
561
|
+
@document&.advance_version
|
|
556
562
|
Leptris::XML::FFI.check_status(
|
|
557
563
|
Leptris::XML::FFI.leptris_element_insert_after(c_ptr, node.c_ptr))
|
|
558
564
|
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
@@ -569,6 +575,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
569
575
|
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
570
576
|
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces_for_sibling)
|
|
571
577
|
end
|
|
578
|
+
@document&.advance_version
|
|
572
579
|
Leptris::XML::FFI.check_status(
|
|
573
580
|
Leptris::XML::FFI.leptris_element_insert_after(c_ptr, node.c_ptr))
|
|
574
581
|
node
|
|
@@ -580,12 +587,14 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
580
587
|
# the namespace lift — the full path below handles it.
|
|
581
588
|
# Cross-document and docless (#376): full path first (lift +
|
|
582
589
|
# engine copy), then the source original is removed; returns
|
|
583
|
-
# the installed copy (self's new previous sibling).
|
|
590
|
+
# the installed copy (self's new previous sibling). #543: the
|
|
591
|
+
# raw-FFI lanes advance the version like the C-bound face.
|
|
584
592
|
if Leptris::XML::Element.foreign_to?(node, @document)
|
|
585
593
|
ensure_writable!
|
|
586
594
|
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
587
595
|
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces_for_sibling)
|
|
588
596
|
end
|
|
597
|
+
@document&.advance_version
|
|
589
598
|
Leptris::XML::FFI.check_status(
|
|
590
599
|
Leptris::XML::FFI.leptris_element_insert_before(c_ptr, node.c_ptr))
|
|
591
600
|
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
@@ -602,6 +611,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
602
611
|
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
603
612
|
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces_for_sibling)
|
|
604
613
|
end
|
|
614
|
+
@document&.advance_version
|
|
605
615
|
Leptris::XML::FFI.check_status(
|
|
606
616
|
Leptris::XML::FFI.leptris_element_insert_before(c_ptr, node.c_ptr))
|
|
607
617
|
node
|
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -1397,6 +1397,24 @@ attach_function :leptris_parse_string,
|
|
|
1397
1397
|
[:leptris_xsd_schema], :size_t
|
|
1398
1398
|
attach_function :leptris_xsd_error,
|
|
1399
1399
|
[:leptris_xsd_schema], UTF8_STRING
|
|
1400
|
+
# libleptris 1.9.382 (moxml#460/#1841): the structured report —
|
|
1401
|
+
# rows of {kind, component, line, column, message}; schema-
|
|
1402
|
+
# owned memory, valid until the next validate. out receives
|
|
1403
|
+
# the record array pointer; the return is the row count.
|
|
1404
|
+
attach_function :leptris_xsd_error_report,
|
|
1405
|
+
[:leptris_xsd_schema, :pointer], :size_t
|
|
1406
|
+
# libleptris 1.9.382 (moxml#460 item 2): the schema's
|
|
1407
|
+
# effective version — "1.1" when the root declares
|
|
1408
|
+
# @version="1.1" (1.1 semantics apply), else "1.0". SOFT
|
|
1409
|
+
# attach: engines before 1.9.382 lack the symbol, and a hard
|
|
1410
|
+
# attach would break every face at load for the whole gem —
|
|
1411
|
+
# callers gate on FFI.respond_to?(:leptris_xsd_version).
|
|
1412
|
+
begin
|
|
1413
|
+
attach_function :leptris_xsd_version,
|
|
1414
|
+
[:leptris_xsd_schema], UTF8_STRING
|
|
1415
|
+
rescue ::FFI::NotFoundError
|
|
1416
|
+
nil
|
|
1417
|
+
end
|
|
1400
1418
|
# Slice 2 (libleptris 1.9.319, #1075): lexical validation.
|
|
1401
1419
|
# Built-ins take the "xs:NAME" reference spelling; -1 = not
|
|
1402
1420
|
# in the tier-1 table. simple_valid resolves a user
|
data/lib/leptris/xml/native.so
CHANGED
|
Binary file
|
|
Binary file
|
data/lib/leptris/xml/xsd.rb
CHANGED
|
@@ -108,6 +108,20 @@ module Leptris::XML::XSD
|
|
|
108
108
|
Leptris::XML::FFI.leptris_xsd_declaration_count(@handle)
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
# The schema's effective version (libleptris 1.9.382, moxml#460
|
|
112
|
+
# item 2): "1.1" when the root carries @version="1.1" — the
|
|
113
|
+
# engine then applies 1.1 semantics (xs:alternative CTA,
|
|
114
|
+
# xs:assert) — else "1.0". Consumers scope 1.0-only checks
|
|
115
|
+
# against it.
|
|
116
|
+
def version
|
|
117
|
+
check_released!
|
|
118
|
+
unless Leptris::XML::FFI.respond_to?(:leptris_xsd_version)
|
|
119
|
+
raise Leptris::XML::Error,
|
|
120
|
+
'Schema#version requires libleptris 1.9.382+ ' '(the leptris_xsd_version face)'
|
|
121
|
+
end
|
|
122
|
+
Leptris::XML::FFI.leptris_xsd_version(@handle) || '1.0'
|
|
123
|
+
end
|
|
124
|
+
|
|
111
125
|
# The engine's schema-level error message, or nil when the
|
|
112
126
|
# schema compiled cleanly.
|
|
113
127
|
def error
|
|
@@ -150,6 +164,46 @@ module Leptris::XML::XSD
|
|
|
150
164
|
end
|
|
151
165
|
end
|
|
152
166
|
|
|
167
|
+
# The structured report (libleptris 1.9.382, moxml#460/#1841):
|
|
168
|
+
# one hash per finding — kind (element-decl / attribute-use /
|
|
169
|
+
# content-model / simpleType-facet / type-resolution /
|
|
170
|
+
# assertion / identity-constraint), component (declaring
|
|
171
|
+
# schema component), line, column, message (the same text
|
|
172
|
+
# validate_errors carries). Requires the 1.9.382 engine.
|
|
173
|
+
StructuredError = Struct.new(:kind, :component, :line, :column,
|
|
174
|
+
:message)
|
|
175
|
+
|
|
176
|
+
def structured_errors(document)
|
|
177
|
+
check_released!
|
|
178
|
+
valid?(document)
|
|
179
|
+
unless Leptris::XML::FFI.respond_to?(:leptris_xsd_error_report)
|
|
180
|
+
raise Leptris::XML::Error,
|
|
181
|
+
'structured_errors requires libleptris 1.9.382+ ' \
|
|
182
|
+
'(the leptris_xsd_error_report face)'
|
|
183
|
+
end
|
|
184
|
+
out = ::FFI::MemoryPointer.new(:pointer)
|
|
185
|
+
n = Leptris::XML::FFI.leptris_xsd_error_report(@handle, out)
|
|
186
|
+
rows = out.get_pointer(0)
|
|
187
|
+
return [] if n.zero? || rows.null?
|
|
188
|
+
|
|
189
|
+
# C layout: kind* / component* / uint line / uint column /
|
|
190
|
+
# message* — 32 bytes per record with tail padding.
|
|
191
|
+
(0...n).map do |i|
|
|
192
|
+
ptr = rows + (i * 32)
|
|
193
|
+
kind = ptr.get_pointer(0)
|
|
194
|
+
component = ptr.get_pointer(8)
|
|
195
|
+
line = ptr.get_uint32(16)
|
|
196
|
+
column = ptr.get_uint32(20)
|
|
197
|
+
message = ptr.get_pointer(24)
|
|
198
|
+
StructuredError.new(
|
|
199
|
+
kind.nil? ? nil : kind.read_string,
|
|
200
|
+
component.nil? ? nil : component.read_string,
|
|
201
|
+
line, column,
|
|
202
|
+
message.nil? ? nil : message.read_string
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
153
207
|
# Slice 3 direct face: a child SEQUENCE against a top-level
|
|
154
208
|
# element declaration's compiled content model (occurrence
|
|
155
209
|
# bounds, xs:any wildcards). +children+ are child Nodes (name
|
data/lib/libleptris.so
CHANGED
|
Binary file
|
data/vendor-src/README.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.9.382] - 2026-10-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- category escapes — \p{L}, \p{Nd}, families (moxml#464) (xsd)
|
|
8
|
+
- codepoint-wide \p{Is...} blocks — the full F.1 table (moxml#464) (xsd)
|
|
9
|
+
- \p{Is...} block escapes in the pattern-facet regex (moxml#464) (xsd)
|
|
10
|
+
- XSD 1.1 negative wildcards — notQName on xs:any and xs:anyAttribute (moxml#460) (xsd)
|
|
11
|
+
- leptris_xsd_version — effective version reporting (moxml#460) (xsd)
|
|
12
|
+
- structured error rows — {kind, component, line, column, message} (xsd)
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- gate the category table on LEPTRIS_HAS_UTF8PROC (simd)
|
|
17
|
+
- CAS-only retain spinlock — the plain spin read raced the atomics (memory)
|
|
18
|
+
- dispatch_init fully initializes the count3_high slot on every arch (simd)
|
|
19
|
+
- declared non-UTF-8 encodings are EXEMPT from the fused gate (parse)
|
|
20
|
+
|
|
21
|
+
### Performance
|
|
22
|
+
|
|
23
|
+
- parse_string adopts the conversion buffer — the copy-in chain and the pristine carve drop out (#1846)
|
|
24
|
+
- raise the element-index build threshold to 8 axis queries (xpath)
|
|
25
|
+
- fuse the UTF-8 fast gate into the count3 sizing pass (parse)
|
|
26
|
+
- rank unmutated documents by parse-time byte offset (xpath)
|
|
27
|
+
- pre-size the element-position journal from the count3 estimate (parse)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
3
31
|
## [1.9.381] - 2026-10-22
|
|
4
32
|
|
|
5
33
|
The XSLT transform engine is no longer slower than libxslt on the
|
|
@@ -28,3 +28,7 @@ TODO.max-perf/node-layout-fork.md — 5 slices, addresses #1222 +
|
|
|
28
28
|
The interleaved-parse rewrite (owner-directed 2026-09-20):
|
|
29
29
|
TODO.max-perf/interleaved-parse.md — the #1222 step-change; slices 0-4,
|
|
30
30
|
each CI-benchmark-gated.
|
|
31
|
+
|
|
32
|
+
The C-face vs pugixml campaign (serialbench fair-flags wave, 2026-10-24):
|
|
33
|
+
TODO.max-perf/15-c-face-vs-pugixml.md — parse 2.81x / generation 2.23x /
|
|
34
|
+
xpath 3.48x; three tracks, CI-bench-gated. Track A1 shipped (PR #1844).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# 15 — C-face vs pugixml: close the fair-flags gap (#1842)
|
|
2
|
+
|
|
3
|
+
Serialbench's fair-flags wave (pugixml `parse_ws_pcdata`, run
|
|
4
|
+
40066278528, posted to #1842): the gap is real — **parse 2.81x**,
|
|
5
|
+
**generation 2.23x**, **xpath 3.48x** median across 13 CI legs —
|
|
6
|
+
and mingw is outright parity (772 vs 770 ips), so the compiler/arch
|
|
7
|
+
dimension matters as much as the library. macos-26-intel is our
|
|
8
|
+
weakest runner every wave and exaggerates every ratio.
|
|
9
|
+
|
|
10
|
+
Leads on XSLT (341 vs libxslt 287 ips) — not part of this campaign.
|
|
11
|
+
|
|
12
|
+
## Track A — parse (2.81x)
|
|
13
|
+
|
|
14
|
+
Profiled (8s sample, no-LTO build, text-shape fixture; see the
|
|
15
|
+
#1842 comment): scanner loop 68% | memchr 8% | dp_wire_child 6.4% |
|
|
16
|
+
whole-buffer copies ~5% | count3 sizing pass 4.4% | SWAR utf8
|
|
17
|
+
validation 2.8% | elem_pos journal ~2.5%.
|
|
18
|
+
|
|
19
|
+
- [x] **A1** journal pre-size from the count3 estimate — PR #1844.
|
|
20
|
+
- [ ] **A2** fold the UTF-8 validation into the count3 pass
|
|
21
|
+
(`count3_high`: ASCII => valid; high bytes => the existing
|
|
22
|
+
DFA). Saves one full memory pass (~2.8%).
|
|
23
|
+
- [ ] **A3** lazy pristine copy. `doc->xml_buffer` is load-bearing
|
|
24
|
+
for lazy LINE resolution; the pristine copy exists because
|
|
25
|
+
`dp_nul` may overwrite a `\n` token terminator. Audit which
|
|
26
|
+
terminators can be `\n`; either protect those or build the
|
|
27
|
+
line table from the scratch. Saves ~2.5%.
|
|
28
|
+
- [ ] **A4** (sprint 2, the actual gap) scanner-loop surgery in
|
|
29
|
+
`direct_parse_internal`: per-tag branch layout, edge-encode
|
|
30
|
+
elimination on the hot path, NUL-write discipline audit.
|
|
31
|
+
pugixml-class work; the loop is 68% of text-shape parse.
|
|
32
|
+
|
|
33
|
+
## Track B — generation (2.23x)
|
|
34
|
+
|
|
35
|
+
- [ ] **B1** profile `leptris_document_serialize` on the serialbench
|
|
36
|
+
fixture (same method: no-LTO sample, MB/s A/B vs pugixml's
|
|
37
|
+
`xml_document::save` to buffer).
|
|
38
|
+
- [ ] **B2** levers from B1: expected candidates — escape-table
|
|
39
|
+
scans (the common no-escape fast path), buffer growth
|
|
40
|
+
strategy, per-node virtual dispatch on the writer.
|
|
41
|
+
|
|
42
|
+
## Track C — xpath (3.48x — the widest)
|
|
43
|
+
|
|
44
|
+
**C0 (measured, 2026-10-24):** the serialbench xpath ROW re-parses
|
|
45
|
+
inside the timed loop (bench.c: `leptris_parse_string` + 3 evals per
|
|
46
|
+
iteration) — the row inherits the parse gap. On the exact ops
|
|
47
|
+
(306KB canonical medium.xml), the dominant NON-parse cost is the
|
|
48
|
+
document-order rank-table build per fresh document:
|
|
49
|
+
`doc_rank_table_put` 337 + `index_walk` 78 + zeroing 29 of ~900
|
|
50
|
+
non-parse samples — every query on every fresh doc re-walks the
|
|
51
|
+
whole tree to assign ranks (#485 table).
|
|
52
|
+
|
|
53
|
+
- [x] **C1** exact-ops profile (scratch_sb_repro vs serialbench-c
|
|
54
|
+
bench.c BENCH bodies; fixture = serialbench/fixtures
|
|
55
|
+
medium.xml).
|
|
56
|
+
- [ ] **C2** offset fast path (DESIGNED, NOT LANDED — see below):
|
|
57
|
+
unmutated parse-created documents (mutation_version == 0)
|
|
58
|
+
rank by parse-time byte offset `(base.line << 2 | kind_bias)`
|
|
59
|
+
— byte offsets ARE XPath document order (attrs sit inside the
|
|
60
|
+
start tag; ns/attr/xpath-text biases 1/2/3 on the owner's
|
|
61
|
+
offset). No walk, no hash. A first implementation reproduced
|
|
62
|
+
EXACTLY (doc node must rank to the outside-the-tree tail like
|
|
63
|
+
the table path) but 8 XPathResults/SubtreeIndex/Ledger tests
|
|
64
|
+
still failed: the eval ITSELF produced different node sets
|
|
65
|
+
when the sort skipped the table build — some step-dedup or
|
|
66
|
+
ordering consumer depends on the table's side effect
|
|
67
|
+
(doc_order_index population), not just the ranks. The
|
|
68
|
+
reproduce-in-one-file probe: parse kSubtree (test_xpath.cpp),
|
|
69
|
+
eval //item then //section//item — fast mode duplicates the
|
|
70
|
+
nested item 5x. NEXT SESSION: find the consumer (candidates:
|
|
71
|
+
the vm's descendant chain dedup assumptions, or a hidden
|
|
72
|
+
doc_order_index reader behind a macro), or gate the fast path
|
|
73
|
+
to the FINAL result sort only.
|
|
74
|
+
- [ ] **C3** mingw/Windows parity investigation: we are AT parity
|
|
75
|
+
there — diff the codegen assumptions (branch-heavy loops?
|
|
76
|
+
LTO behavior on gcc-win vs clang-mac?) and port whatever
|
|
77
|
+
wins back to the other legs.
|
|
78
|
+
|
|
79
|
+
## Laws
|
|
80
|
+
|
|
81
|
+
- CI benchmark legs (quiet runners) gate every number; local A/B
|
|
82
|
+
under a loaded machine is noise.
|
|
83
|
+
- Profile with a separate `-DLEPTRIS_ENABLE_LTO=OFF` build dir; LTO
|
|
84
|
+
flattens samples into one frame.
|
|
85
|
+
- #1218: arena estimates only ever grow.
|
|
86
|
+
|
|
87
|
+
Refs #1842, serialbench-c#8.
|
|
@@ -952,9 +952,50 @@ LEPTRIS_API int leptris_xsd_content_valid(
|
|
|
952
952
|
* the schema). */
|
|
953
953
|
LEPTRIS_API int leptris_xsd_validate(LeptrisXsdSchema schema,
|
|
954
954
|
LeptrisDocument doc);
|
|
955
|
+
/**
|
|
956
|
+
* Effective XSD version of a compiled schema ("1.1" or "1.0").
|
|
957
|
+
*
|
|
958
|
+
* "1.1" when the schema root carries @version="1.1" — the engine
|
|
959
|
+
* then applies 1.1 semantics (xs:alternative conditional type
|
|
960
|
+
* assignment, xs:assert evaluation). Any other root version, or no
|
|
961
|
+
* version attribute, is "1.0". The returned string is static and
|
|
962
|
+
* schema-independent; NULL for a NULL schema (moxml#460).
|
|
963
|
+
*/
|
|
964
|
+
LEPTRIS_API const char* leptris_xsd_version(LeptrisXsdSchema schema);
|
|
955
965
|
LEPTRIS_API size_t leptris_xsd_error_count(LeptrisXsdSchema schema);
|
|
956
966
|
LEPTRIS_API const char* leptris_xsd_error_at(LeptrisXsdSchema schema,
|
|
957
967
|
size_t i);
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* One XSD validation finding, in the structured report shape
|
|
971
|
+
* (#1841 — mirrors LeptrisRngErrorRecord). Returned as a
|
|
972
|
+
* whole-list report via leptris_xsd_error_report(); no
|
|
973
|
+
* per-index stitching. All strings are schema-owned and live
|
|
974
|
+
* until the next validate call or leptris_xsd_free.
|
|
975
|
+
*/
|
|
976
|
+
typedef struct {
|
|
977
|
+
const char* kind; /* failure class, stable name:
|
|
978
|
+
* "element-decl", "attribute-use",
|
|
979
|
+
* "content-model", "simpleType-facet",
|
|
980
|
+
* "type-resolution", "assertion",
|
|
981
|
+
* "identity-constraint" */
|
|
982
|
+
const char* component; /* declaring schema component's name
|
|
983
|
+
* (qualified where namespaced), or NULL */
|
|
984
|
+
unsigned line; /* instance source line, 0 when unknown */
|
|
985
|
+
unsigned column; /* instance source column, 0 when unknown */
|
|
986
|
+
const char* message; /* unchanged leptris_xsd_error_at text */
|
|
987
|
+
} LeptrisXsdErrorRecord;
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Get the full XSD validation report in one call (#1841).
|
|
991
|
+
*
|
|
992
|
+
* @param schema schema handle after a leptris_xsd_validate call
|
|
993
|
+
* @param out receives the schema-owned record array (NULL when
|
|
994
|
+
* the count is 0; left untouched on a NULL schema)
|
|
995
|
+
* @return record count; 0 for a valid document
|
|
996
|
+
*/
|
|
997
|
+
LEPTRIS_API size_t leptris_xsd_error_report(
|
|
998
|
+
LeptrisXsdSchema schema, const LeptrisXsdErrorRecord** out);
|
|
958
999
|
/* Validate an instance document against the parsed schema
|
|
959
1000
|
* (phase-2 core subset: element/attribute/text/data/value/
|
|
960
1001
|
* choice/group/interleave/repeats/ref). Returns 1 valid; on 0,
|
|
@@ -58,6 +58,9 @@ static ptrdiff_t (*g_find3_fn)(const char*, size_t, char, char, char) = NULL;
|
|
|
58
58
|
static size_t (*g_count_fn)(const char*, size_t, char) = NULL;
|
|
59
59
|
static void (*g_count3_fn)(const char*, size_t, char, char, char,
|
|
60
60
|
size_t*, size_t*, size_t*) = NULL;
|
|
61
|
+
static void (*g_count3_high_fn)(const char*, size_t, char, char, char,
|
|
62
|
+
size_t*, size_t*, size_t*,
|
|
63
|
+
int*) = NULL;
|
|
61
64
|
static void (*g_copy3_fn)(char*, const char*, size_t, char, char, char,
|
|
62
65
|
size_t*, size_t*, size_t*) = NULL;
|
|
63
66
|
|
|
@@ -79,6 +82,10 @@ static size_t count_scalar(const char* s, size_t len, char c) {
|
|
|
79
82
|
* dispatch_init()" pattern raced when two threads hit the same
|
|
80
83
|
* entry point on first call. */
|
|
81
84
|
static void dispatch_init(void);
|
|
85
|
+
static void count3_high_scalar(const char* s, size_t len,
|
|
86
|
+
char c0, char c1, char c2,
|
|
87
|
+
size_t* n0, size_t* n1, size_t* n2,
|
|
88
|
+
int* saw_high);
|
|
82
89
|
|
|
83
90
|
#if defined(_MSC_VER)
|
|
84
91
|
# pragma section(".CRT$XCU", read)
|
|
@@ -100,6 +107,7 @@ static void dispatch_init(void) {
|
|
|
100
107
|
g_contains_fn = leptris_text_contains_avx2;
|
|
101
108
|
g_find3_fn = leptris_text_find3_avx2;
|
|
102
109
|
g_count_fn = leptris_text_count_char_avx2;
|
|
110
|
+
g_count3_high_fn = count3_high_scalar;
|
|
103
111
|
return;
|
|
104
112
|
}
|
|
105
113
|
#endif
|
|
@@ -109,6 +117,7 @@ static void dispatch_init(void) {
|
|
|
109
117
|
g_find3_fn = leptris_text_find3_neon;
|
|
110
118
|
g_count_fn = leptris_text_count_char_neon;
|
|
111
119
|
g_count3_fn = leptris_text_count3_neon;
|
|
120
|
+
g_count3_high_fn = leptris_text_count3_high_neon;
|
|
112
121
|
g_copy3_fn = leptris_copy_count3_neon;
|
|
113
122
|
return;
|
|
114
123
|
}
|
|
@@ -117,6 +126,41 @@ static void dispatch_init(void) {
|
|
|
117
126
|
g_contains_fn = contains_scalar;
|
|
118
127
|
g_find3_fn = find3_scalar;
|
|
119
128
|
g_count_fn = count_scalar;
|
|
129
|
+
g_count3_high_fn = count3_high_scalar;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Scalar kernel: one byte loop counts all three chars and detects
|
|
133
|
+
* high bytes (host shapes without a count3 SIMD kernel — x86/AVX2
|
|
134
|
+
* builds have none today). Assigned by dispatch_init so the
|
|
135
|
+
* constructor fully initializes the slot on every arch: the lazy
|
|
136
|
+
* if-NULL re-dispatch raced under TSan when two threads hit the
|
|
137
|
+
* same entry on first call (TODO.concurrency/08 discipline). */
|
|
138
|
+
static void count3_high_scalar(const char* s, size_t len,
|
|
139
|
+
char c0, char c1, char c2,
|
|
140
|
+
size_t* n0, size_t* n1, size_t* n2,
|
|
141
|
+
int* saw_high) {
|
|
142
|
+
size_t a = 0, b = 0, c = 0;
|
|
143
|
+
int h = 0;
|
|
144
|
+
for (size_t i = 0; i < len; i++) {
|
|
145
|
+
unsigned char ch = (unsigned char)s[i];
|
|
146
|
+
if (ch == (unsigned char)c0) a++;
|
|
147
|
+
if (ch == (unsigned char)c1) b++;
|
|
148
|
+
if (ch == (unsigned char)c2) c++;
|
|
149
|
+
if (ch & 0x80) h = 1;
|
|
150
|
+
}
|
|
151
|
+
*n0 = a;
|
|
152
|
+
*n1 = b;
|
|
153
|
+
*n2 = c;
|
|
154
|
+
*saw_high = h;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
void leptris_text_count3_high(const char* s, size_t len,
|
|
158
|
+
char c0, char c1, char c2,
|
|
159
|
+
size_t* n0, size_t* n1, size_t* n2,
|
|
160
|
+
int* saw_high) {
|
|
161
|
+
if (!g_count3_high_fn) dispatch_init();
|
|
162
|
+
if (!g_count3_high_fn) g_count3_high_fn = count3_high_scalar;
|
|
163
|
+
g_count3_high_fn(s, len, c0, c1, c2, n0, n1, n2, saw_high);
|
|
120
164
|
}
|
|
121
165
|
|
|
122
166
|
int leptris_text_contains(const char* s, size_t len, char c) {
|
|
@@ -48,6 +48,15 @@ void leptris_text_count3(const char* s, size_t len,
|
|
|
48
48
|
char c0, char c1, char c2,
|
|
49
49
|
size_t* n0, size_t* n1, size_t* n2);
|
|
50
50
|
|
|
51
|
+
/* count3 + "any byte >= 0x80" in one pass (#1842): the parser's
|
|
52
|
+
* sizing pre-scan doubles as the UTF-8 fast gate — pure-ASCII input
|
|
53
|
+
* is valid UTF-8 by construction. Falls back to the scalar loop
|
|
54
|
+
* where no SIMD path is compiled. */
|
|
55
|
+
void leptris_text_count3_high(const char* s, size_t len,
|
|
56
|
+
char c0, char c1, char c2,
|
|
57
|
+
size_t* n0, size_t* n1, size_t* n2,
|
|
58
|
+
int* saw_high);
|
|
59
|
+
|
|
51
60
|
/* Copy [src, src+len) to dst while counting c0/c1/c2 occurrences —
|
|
52
61
|
* one fused memory pass (falls back to memcpy + count3 where no
|
|
53
62
|
* SIMD path is compiled). The parser's owns-copy path gets its
|
|
@@ -103,6 +112,10 @@ size_t leptris_text_count_char_neon(const char* s, size_t len, char c);
|
|
|
103
112
|
void leptris_text_count3_neon(const char* s, size_t len,
|
|
104
113
|
char a, char b, char c,
|
|
105
114
|
size_t* ca, size_t* cb, size_t* cc);
|
|
115
|
+
void leptris_text_count3_high_neon(const char* s, size_t len,
|
|
116
|
+
char a, char b, char c,
|
|
117
|
+
size_t* ca, size_t* cb, size_t* cc,
|
|
118
|
+
int* saw_high);
|
|
106
119
|
void leptris_copy_count3_neon(char* dst, const char* src, size_t len,
|
|
107
120
|
char a, char b, char c,
|
|
108
121
|
size_t* ca, size_t* cb, size_t* cc);
|