leptris 1.9.381.0 → 1.9.382.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/Rakefile +9 -2
  4. data/ext/leptris/native/native.c +89 -0
  5. data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
  6. data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
  7. data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
  8. data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
  9. data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
  10. data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
  11. data/lib/leptris/vendor/s390x-linux-musl/libleptris.so +0 -0
  12. data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
  13. data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
  14. data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
  15. data/lib/leptris/version.rb +1 -1
  16. data/lib/leptris/xml/element.rb +11 -1
  17. data/lib/leptris/xml/ffi.rb +18 -0
  18. data/lib/leptris/xml/xsd.rb +54 -0
  19. data/vendor-src/README.md +1 -1
  20. data/vendor-src/libleptris/CHANGELOG.md +28 -0
  21. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  22. data/vendor-src/libleptris/TODO.max-perf/00-INDEX.md +4 -0
  23. data/vendor-src/libleptris/TODO.max-perf/15-c-face-vs-pugixml.md +87 -0
  24. data/vendor-src/libleptris/src/include/leptris.h +41 -0
  25. data/vendor-src/libleptris/src/leptris/common/simd_text.c +44 -0
  26. data/vendor-src/libleptris/src/leptris/common/simd_text.h +13 -0
  27. data/vendor-src/libleptris/src/leptris/common/simd_text_neon.c +51 -0
  28. data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +54 -13
  29. data/vendor-src/libleptris/src/leptris/leptris.c +12 -7
  30. data/vendor-src/libleptris/src/leptris/leptris_internal.h +7 -0
  31. data/vendor-src/libleptris/src/leptris/memory/arena.c +3 -2
  32. data/vendor-src/libleptris/src/leptris/rng/rng_regex.c +411 -17
  33. data/vendor-src/libleptris/src/leptris/xpath/evaluator_path.c +116 -1
  34. data/vendor-src/libleptris/src/leptris/xpath/vm.c +7 -7
  35. data/vendor-src/libleptris/src/leptris/xsd/compile.c +105 -2
  36. data/vendor-src/libleptris/src/leptris/xsd/content_model.c +45 -4
  37. data/vendor-src/libleptris/src/leptris/xsd/validate.c +113 -42
  38. data/vendor-src/libleptris/src/leptris/xsd/xsd_internal.h +24 -0
  39. data/vendor-src/libleptris/test/parser/test_simd_count.cpp +68 -0
  40. data/vendor-src/libleptris/test/rng/test_rng.cpp +69 -1
  41. data/vendor-src/libleptris/test/xpath/test_xpath.cpp +8 -3
  42. data/vendor-src/libleptris/test/xsd/test_xsd_compile.cpp +220 -0
  43. data/vendor-src/libleptris/vcpkg.json +1 -1
  44. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 600a02350159ac29319d4e4df445bade18bc26489ea6ad3f28dfe118948b366e
4
- data.tar.gz: dd081df19fbe1a3ae8b1af6dad80395e2ddbd93aa2ecd488cbee6b059324e910
3
+ metadata.gz: 7a54646ad6fd027ed1f7963ef24f56f663e8e66b49fd98cc7409585b3272ec92
4
+ data.tar.gz: 50a525c470549ea2fc8ad8bd08c1f8c55af4d1d5cbc05507c448693f77a7c85c
5
5
  SHA512:
6
- metadata.gz: b5c65806365acbe0bc0aebe358c5267519c71e3e922acecc1ca13fa44a826965f9feba45258286fb10885cb1ad13d77f2f97bead9d027f92ad0f60569086d02a
7
- data.tar.gz: a3bdb6971f8e5a16db92ba01cf6d4d55f580e923250b616b9b8b02d3d755485717bc11f8a36b852e4c1526d40002d4117f025325e8672be3a91097ed66b5ec28
6
+ metadata.gz: 19181d95c1f8d45ef7b72c5f2b970be16897097ff2c553a0f13ec0361e6773701abd9ebe108712c1d06a471a0384d07bcc0288d5d8ec2c1a5f76afa15701e5d6
7
+ data.tar.gz: e283781e48733b014409d707732f135bcb132734f6d2d26f252f45ee76991e656dd49882a3fae7d7e81cbb76e826344890d4be472cafc87b0574fc384ce00507
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.381"
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}" }
@@ -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");
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.381.0"
4
+ VERSION = "1.9.382.0"
5
5
  end
@@ -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
@@ -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
@@ -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/vendor-src/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Sources vendored in this gem (recompile rights; the ruby
2
2
  variant builds them at install via extconf.rb):
3
3
 
4
- - libleptris v1.9.381
4
+ - libleptris v1.9.382
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -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
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.381
6
+ VERSION 1.9.382
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -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) {