leptris 1.9.394.0-ppc64le-linux → 1.9.396.0-ppc64le-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e3208fdd19dc20eb05cdeaff72a587f053ec9e9b338b863afcc18065f12ba50
4
- data.tar.gz: cbb3a90c5c594faee31b9fd845b127edb7dadf2d3952f9e6f02cc1c8718de675
3
+ metadata.gz: 948d64de60780abbd35c5eb75ecbb69609396b681b219047ef0ac7775b9383c3
4
+ data.tar.gz: 69236732f06743bd08714bbf46697cadd63febc486e324ffeee46798c5182fce
5
5
  SHA512:
6
- metadata.gz: f4d47ec0dd4ca188ffd7e04048b7c4efbdc708d60ce2a93a546a6eeba020c28f9fe8a22ca0cc7821729200357d0fece1c1ab4667fa95c4ad8ede11aeb5ce161b
7
- data.tar.gz: c3f0b734f40bcda5aa98df85fd2b0a0c93239f986c23d4035b58ec205183082b07594bf5b3baff1dc9655a7649fbf6ebb19e9d9facdc1014a378b03a83e309f7
6
+ metadata.gz: d3a2ee8d49e508f8d27bbf36610c2d9cd1975c5ec8fdf6f8f752d81c1154f22c630fc5883ab2fd5af5afcafa920eebb8342a7e8777af3dd82fef88d6acf7957b
7
+ data.tar.gz: 955dccf5c781bd9e1906bade2ae4182fa50d28c15dc899950f0edb340cb77c8deacc307a9132f9e7ba23348398ddd71e911d1082105c07940576f8fb9600956c
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.394"
11
+ LIBLEPTRIS_VERSION = "1.9.396"
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,14 @@ namespace :audit do
401
401
  .map { |n| n.sub(/\A_/, "") }
402
402
  .select { |n| n.start_with?("leptris_") }
403
403
  .map { |n| n.sub(/\Aleptris_/, "") }.sort
404
- attached = File.read("lib/leptris/xml/ffi.rb")
405
- .scan(/attach_function :leptris_([a-z_0-9]+)/).flatten.sort
404
+ attached = (
405
+ File.read("lib/leptris/xml/ffi.rb")
406
+ .scan(/attach_function :leptris_([a-z_0-9]+)/).flatten +
407
+ # The native shim's C->C dlsym dispatches (#572: struct-value
408
+ # returns libffi can't carry) are bound surface too.
409
+ File.read("ext/leptris/native/native.c")
410
+ .scan(/lib_sym\((?:h|handle), "leptris_([a-z_0-9]+)"\)/).flatten
411
+ ).sort
406
412
  # Version-gated faces: soft-attached because engines BEFORE the
407
413
  # named release lack the symbol (the load must survive them).
408
414
  # They join the audit only when the engine pin reaches that
@@ -278,6 +278,14 @@ static void *lib_open(const char *path) { return dlopen(path, RTLD_NOW); }
278
278
  static void *lib_sym(void *h, const char *name) { return dlsym(h, name); }
279
279
  #endif
280
280
 
281
+ /* leptris-ruby#572: the engine's capability descriptor (1.9.392+). */
282
+ typedef struct {
283
+ unsigned long long caps;
284
+ unsigned descriptor_version;
285
+ } rb_caps_t;
286
+ typedef rb_caps_t (*capabilities_fn)(void);
287
+ static capabilities_fn f_capabilities;
288
+
281
289
  static void resolve_symbols(const char *lib_path)
282
290
  {
283
291
  void *h = lib_open(lib_path);
@@ -314,6 +322,7 @@ static void resolve_symbols(const char *lib_path)
314
322
  /* Single-crossing element construction (libleptris 1.9.237,
315
323
  * #1344): NULL in older engine pins; the create path falls
316
324
  * back to create + per-pair sets. */
325
+ f_capabilities = (capabilities_fn)lib_sym(h, "leptris_capabilities");
317
326
  f_elem_new_with_attrs =
318
327
  (elem_new_attrs_fn)lib_sym(h, "leptris_element_new_with_attributes");
319
328
  f_text_create = (text_create_fn)lib_sym(h, "leptris_text_node_create");
@@ -3965,6 +3974,25 @@ static VALUE nf_sax_parser_free(VALUE self, VALUE obj)
3965
3974
  return Qnil;
3966
3975
  }
3967
3976
 
3977
+ /* leptris-ruby#572: the engine's capability descriptor as a plain
3978
+ * hash — libffi's small-struct value return proved unreliable across
3979
+ * ABIs, so the shim reads the C struct and returns the two fields
3980
+ * (raw bitset + descriptor version; bits append per #1905). */
3981
+ static VALUE nf_capabilities(VALUE module) {
3982
+ VALUE hash = rb_hash_new();
3983
+ if (!f_capabilities) {
3984
+ rb_hash_aset(hash, ID2SYM(rb_intern("caps")), Qnil);
3985
+ rb_hash_aset(hash, ID2SYM(rb_intern("descriptor_version")), Qnil);
3986
+ return hash;
3987
+ }
3988
+ rb_caps_t c = f_capabilities();
3989
+ rb_hash_aset(hash, ID2SYM(rb_intern("caps")),
3990
+ ULL2NUM(c.caps));
3991
+ rb_hash_aset(hash, ID2SYM(rb_intern("descriptor_version")),
3992
+ UINT2NUM(c.descriptor_version));
3993
+ return hash;
3994
+ }
3995
+
3968
3996
  LEPTRIS_INIT_EXPORT void Init_native(void)
3969
3997
  {
3970
3998
  VALUE m_leptris, m_xml, m_native;
@@ -4084,6 +4112,8 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
4084
4112
  c_sax_parser = rb_define_class_under(m_native, "SaxParser",
4085
4113
  c_object);
4086
4114
  rb_define_module_function(m_native, "sax_parse", nf_sax_parse, 3);
4115
+ rb_define_module_function(m_native, "capabilities",
4116
+ nf_capabilities, 0);
4087
4117
  rb_define_module_function(m_native, "sax_parser_create",
4088
4118
  nf_sax_parser_create, 2);
4089
4119
  rb_define_module_function(m_native, "sax_parser_feed",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.394.0"
4
+ VERSION = "1.9.396.0"
5
5
  end
@@ -566,6 +566,11 @@ module Leptris
566
566
  [:string, :size_t, :leptris_plan, :pointer],
567
567
  :leptris_plan_result
568
568
 
569
+ # leptris-ruby#572: the engine's capability descriptor is read
570
+ # through the native shim's Native.capabilities (libffi's
571
+ # small-struct value return proved ABI-unreliable); the shim
572
+ # exposes the raw bitset + descriptor version and re-encodes
573
+ # nothing (bits append per contract).
569
574
  attach_function :leptris_version, [], UTF8_STRING
570
575
  attach_function :leptris_version_components, [:pointer, :pointer, :pointer], :void
571
576
 
Binary file
Binary file
data/lib/leptris/xml.rb CHANGED
@@ -43,6 +43,31 @@ module Leptris
43
43
 
44
44
  # Nokogiri-style top-level entry points (Nokogiri::XML(...) /
45
45
  # Nokogiri::XML.parse). Delegates to Document.parse.
46
+ # leptris-ruby#572: the engine's capability descriptor — the
47
+ # raw bitset plus descriptor version (leptris/leptris#1905).
48
+ # Frozen and memoized: the engine's answers are fixed per binary.
49
+ # Consumers (moxml#504) OR this with wrapper knowledge; bits
50
+ # append per the engine's contract — never reorder.
51
+ def self.capabilities
52
+ @capabilities ||=
53
+ begin
54
+ raw =
55
+ begin
56
+ Native.capabilities
57
+ rescue NameError
58
+ # the native layer loads with `require "leptris"`;
59
+ # a direct `require "leptris/xml"` has not loaded it
60
+ # yet — the explicit entry is the parallel-API door.
61
+ require "leptris"
62
+ Native.capabilities
63
+ end
64
+ {
65
+ caps: raw[:caps],
66
+ descriptor_version: raw[:descriptor_version]
67
+ }.freeze
68
+ end
69
+ end
70
+
46
71
  def self.parse(xml_or_io, options: nil, readonly: false,
47
72
  recover: true, strict: false)
48
73
  recover = false if strict
data/lib/libleptris.so CHANGED
Binary file
data/vendor-src/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Sources vendored in this gem (recompile rights; the ruby
2
2
  variant builds them at install via extconf.rb):
3
3
 
4
- - libleptris v1.9.394
4
+ - libleptris v1.9.396
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,74 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.396] - 2026-10-31
4
+
5
+ ### Fixed
6
+
7
+ - **The AVX2 UTF-8 validator rejected valid multi-byte input
8
+ (leptris#1842 A5 follow-up).** Three defects shipped with the
9
+ 1.9.395 SIMD gate, all invisible on arm64: the ascii lane's
10
+ polarity was inverted (`cmplt(cur, zero)` is the NOT-ascii half,
11
+ so every ASCII byte tripped the error term), both
12
+ continuation-range bounds compared in the wrong direction
13
+ (`cont_ok` was unsatisfiable — NEON's direct `vcge/vcle` pair
14
+ became a swapped saturating-subtraction idiom), and the tail
15
+ anchor walked back only over continuation runs, letting an
16
+ opener in the final block lane escape validation (both cores).
17
+ The leptris-py UTF-16 BOM lane caught it (`malformed input` on
18
+ `café`); x86 CI failed 25+ tests. Now verified by a 400k-case
19
+ lane-math simulation against the scalar oracle plus boundary
20
+ shapes seeded into the randomized cross-check.
21
+
22
+ - **`leptris_parse_string_inplace` no longer writes past the
23
+ buffer.** The NUL sentinel at `buf[len]` exceeded the documented
24
+ contract (terminators INSIDE the buffer); an exactly-sized
25
+ caller tripped ASAN. The scanner takes `len` and never read it.
26
+
27
+ - **`xs:redefine` bodies were dropped (leptris#1926).** A
28
+ `redefine`d complexType whose body declared new particles (or
29
+ attributes) lost them: the extension derived from the base type
30
+ captured its model against the BASE body, and the redefinition was
31
+ never folded in. Bodies are now captured after the base resolves —
32
+ the same mechanism `xs:override` already uses (PR #1913) — so the
33
+ ladder resolves head-first through the redefinition and the
34
+ extension folds against the redefined content. `xs:group`/attr
35
+ refs inside redefine bodies are collected like override bodies.
36
+
37
+
38
+
39
+ ## [1.9.395] - 2026-10-31
40
+
41
+ ### Added
42
+
43
+ - **Boundary-vector UTF-8 validation (leptris#1842 A5, PRs #1922/#1924).**
44
+ `leptris_utf8_valid` — AVX2/NEON cores over the existing dispatch
45
+ layer, the scalar DFA as the fallback and oracle — pinned by an
46
+ adversarial matrix plus a 4000-case randomized cross-check. The
47
+ parser's UTF-8 gate now routes through it: **parse of UTF-8-dense
48
+ documents is +32%** (3.70ms -> 2.81ms on a 2.2MB catalog; the
49
+ validator alone ~3.7 GB/s on NEON). Unblocked line-level profiling
50
+ first: the no-LTO `-g` build resolves file:line through
51
+ dsymutil/atos (ThinLTO bitcode was the blocker).
52
+
53
+ ### Fixed
54
+
55
+ - **PI data-scan bounds (fuzzer class).** Consuming the PI target's
56
+ separator could leave `p.pos` one past `p.end`; `p.end - p.pos`
57
+ underflowed to SIZE_MAX and the data `memchr` walked off the
58
+ allocation (the nightly-fuzz witness segfaulted under the no-LTO
59
+ build — allocator luck hid it on LTO).
60
+ - **ctE019 regression (leptris#1923, PR #1927).** A chameleon-included
61
+ attribute referenced through a restriction base was false-rejected
62
+ as "not declared (strict)" — the 1.9.391 pending-ref fix made the
63
+ forward-referenced rows visible, surfacing that only the derived
64
+ type's rows were consulted. The oracle (xmllint) validates ctE019:
65
+ both derivation spellings now record the restriction base and the
66
+ strict-attribute admission walks the chain (admission only;
67
+ required/fixed/type checks stay derived-only).
68
+ - drop the const-path pristine carve (leptris#1842 A3) (parse)
69
+
70
+
71
+
3
72
  ## [1.9.394] - 2026-10-30
4
73
 
5
74
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.394
6
+ VERSION 1.9.396
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -300,16 +300,23 @@ target_compile_definitions(leptris_objects PUBLIC _POSIX_C_SOURCE=200809L)
300
300
  # them all into the same library, dispatch at runtime via cpu.c. The
301
301
  # scalar TU (simd_text.c) is always built; these add accelerated paths.
302
302
  if(CMAKE_SYSTEM_PROCESSOR MATCHES "(x86_64|AMD64|i[3-6]86)")
303
- check_c_compiler_flag("-mavx2" LEPTRIS_HAVE_AVX2_FLAG)
303
+ # MSVC has no -mavx2; /arch:AVX2 enables the whole AVX set for the
304
+ # TU (the windows lane shipped scalar-only and the capability bit
305
+ # disagreed with test_capabilities — 2026-10-31).
306
+ if(MSVC)
307
+ set(LEPTRIS_AVX2_FLAG "/arch:AVX2")
308
+ else()
309
+ set(LEPTRIS_AVX2_FLAG "-mavx2")
310
+ endif()
311
+ check_c_compiler_flag("${LEPTRIS_AVX2_FLAG}" LEPTRIS_HAVE_AVX2_FLAG)
304
312
  if(LEPTRIS_HAVE_AVX2_FLAG)
305
313
  target_sources(leptris_objects PRIVATE leptris/common/simd_text_avx2.c)
306
314
  set_source_files_properties(leptris/common/simd_text_avx2.c
307
- PROPERTIES COMPILE_FLAGS "-mavx2")
315
+ PROPERTIES COMPILE_FLAGS "${LEPTRIS_AVX2_FLAG}")
308
316
  # Consumers (simd_text.c dispatch) reference the AVX2 symbol only
309
- # when this define is present — MSVC rejects -mavx2 (uses /arch:),
310
- # so the TU is skipped there and the dispatch falls back to scalar.
317
+ # when this define is present.
311
318
  target_compile_definitions(leptris_objects PRIVATE LEPTRIS_HAS_AVX2_BUILD=1)
312
- message(STATUS "AOT SIMD: AVX2 text scan ENABLED")
319
+ message(STATUS "AOT SIMD: AVX2 text scan ENABLED (${LEPTRIS_AVX2_FLAG})")
313
320
  else()
314
321
  message(STATUS "AOT SIMD: AVX2 unavailable (flag rejected)")
315
322
  endif()
@@ -49,6 +49,54 @@ static ptrdiff_t find3_scalar(const char* s, size_t len,
49
49
  return -1;
50
50
  }
51
51
 
52
+ /* Scalar UTF-8 DFA — the always-available fallback (and the oracle
53
+ * the SIMD cores are pinned against). Moved from the parser's
54
+ * dp_utf8_valid (#1842 A5). */
55
+ int leptris_utf8_valid_scalar(const char* s_, size_t len) {
56
+ const unsigned char* p = (const unsigned char*)s_;
57
+ size_t i = 0;
58
+ /* ASCII fast path: 8 bytes per iteration. The DFA below runs
59
+ * only when a high byte is present; ordinary documents pay one
60
+ * memory-bandwidth pass. */
61
+ while (i + 8 <= len) {
62
+ uint64_t w;
63
+ memcpy(&w, p + i, 8);
64
+ if (w & UINT64_C(0x8080808080808080)) break;
65
+ i += 8;
66
+ }
67
+ while (i < len && p[i] < 0x80) i++;
68
+ while (i < len) {
69
+ unsigned c = p[i];
70
+ if (c < 0x80) {
71
+ i++;
72
+ continue;
73
+ }
74
+ size_t n;
75
+ unsigned lo = 0x80;
76
+ unsigned hi = 0xBF;
77
+ if (c >= 0xC2 && c <= 0xDF) {
78
+ n = 1;
79
+ } else if (c >= 0xE0 && c <= 0xEF) {
80
+ n = 2;
81
+ if (c == 0xE0) lo = 0xA0;
82
+ else if (c == 0xED) hi = 0x9F;
83
+ } else if (c >= 0xF0 && c <= 0xF4) {
84
+ n = 3;
85
+ if (c == 0xF0) lo = 0x90;
86
+ else if (c == 0xF4) hi = 0x8F;
87
+ } else {
88
+ return 0;
89
+ }
90
+ if (len - i <= n) return 0;
91
+ if (p[i + 1] < lo || p[i + 1] > hi) return 0;
92
+ for (size_t k = 2; k <= n; k++) {
93
+ if (p[i + k] < 0x80 || p[i + k] > 0xBF) return 0;
94
+ }
95
+ i += n + 1;
96
+ }
97
+ return 1;
98
+ }
99
+
52
100
  /* ---- Dispatch ---------------------------------------------------------- */
53
101
 
54
102
  /* Resolved pointer — initialized on first use (thread-safe enough:
@@ -63,6 +111,7 @@ static void (*g_count3_high_fn)(const char*, size_t, char, char, char,
63
111
  int*) = NULL;
64
112
  static void (*g_copy3_fn)(char*, const char*, size_t, char, char, char,
65
113
  size_t*, size_t*, size_t*) = NULL;
114
+ static int (*g_utf8_fn)(const char*, size_t) = NULL;
66
115
  static ptrdiff_t (*g_scan_run_fn)(const char*, size_t, char, char, char,
67
116
  int*, int*) = NULL;
68
117
 
@@ -105,6 +154,9 @@ static size_t count_scalar(const char* s, size_t len, char c) {
105
154
  * dispatch_init()" pattern raced when two threads hit the same
106
155
  * entry point on first call. */
107
156
  static void dispatch_init(void);
157
+ int leptris_utf8_valid_scalar(const char* s, size_t len);
158
+ int leptris_utf8_valid_avx2(const char* s, size_t len);
159
+ int leptris_utf8_valid_neon(const char* s, size_t len);
108
160
  static void count3_high_scalar(const char* s, size_t len,
109
161
  char c0, char c1, char c2,
110
162
  size_t* n0, size_t* n1, size_t* n2,
@@ -132,6 +184,7 @@ static void dispatch_init(void) {
132
184
  g_count_fn = leptris_text_count_char_avx2;
133
185
  g_count3_high_fn = count3_high_scalar;
134
186
  g_scan_run_fn = leptris_text_scan_run_avx2;
187
+ g_utf8_fn = leptris_utf8_valid_avx2;
135
188
  return;
136
189
  }
137
190
  #endif
@@ -144,6 +197,7 @@ static void dispatch_init(void) {
144
197
  g_count3_high_fn = leptris_text_count3_high_neon;
145
198
  g_copy3_fn = leptris_copy_count3_neon;
146
199
  g_scan_run_fn = leptris_text_scan_run_neon;
200
+ g_utf8_fn = leptris_utf8_valid_neon;
147
201
  return;
148
202
  }
149
203
  #endif
@@ -153,6 +207,7 @@ static void dispatch_init(void) {
153
207
  g_count_fn = count_scalar;
154
208
  g_count3_high_fn = count3_high_scalar;
155
209
  g_scan_run_fn = scan_run_scalar;
210
+ g_utf8_fn = leptris_utf8_valid_scalar;
156
211
  }
157
212
 
158
213
  /* Scalar kernel: one byte loop counts all three chars and detects
@@ -239,6 +294,12 @@ void leptris_copy_count3(char* dst, const char* src, size_t len,
239
294
  leptris_text_count3(src, len, c0, c1, c2, n0, n1, n2);
240
295
  }
241
296
 
297
+ int leptris_utf8_valid(const char* s, size_t len) {
298
+ if (!g_utf8_fn) dispatch_init();
299
+ if (!g_utf8_fn) g_utf8_fn = leptris_utf8_valid_scalar;
300
+ return g_utf8_fn(s, len);
301
+ }
302
+
242
303
  ptrdiff_t leptris_text_scan_run(const char* s, size_t len, char term,
243
304
  char f1, char f2,
244
305
  int* saw_f1, int* saw_f2) {
@@ -57,6 +57,19 @@ void leptris_text_count3_high(const char* s, size_t len,
57
57
  size_t* n0, size_t* n1, size_t* n2,
58
58
  int* saw_high);
59
59
 
60
+ /* Returns 1 when [s, s+len) is valid UTF-8, 0 otherwise. SIMD
61
+ * boundary-vector validation (the Lemire/Steagall construction) with
62
+ * the scalar DFA as the fallback; identical verdicts on every input
63
+ * (pinned by the encoding suite's adversarial matrix + a randomized
64
+ * cross-check). #1842 A5: the parser's UTF-8 gate ran a per-byte DFA
65
+ * over the whole document — the hottest line on UTF-8-dense
66
+ * fixtures (line-level profile, 2026-10-30). */
67
+ int leptris_utf8_valid(const char* s, size_t len);
68
+
69
+ /* The scalar DFA fallback (and the SIMD verdicts' oracle) — also
70
+ * the shared tail anchor for the ISA cores. */
71
+ int leptris_utf8_valid_scalar(const char* s, size_t len);
72
+
60
73
  /* Copy [src, src+len) to dst while counting c0/c1/c2 occurrences —
61
74
  * one fused memory pass (falls back to memcpy + count3 where no
62
75
  * SIMD path is compiled). The parser's owns-copy path gets its
@@ -75,6 +75,14 @@ ptrdiff_t leptris_text_find3_avx2(const char* s, size_t len,
75
75
  return -1;
76
76
  }
77
77
 
78
+ /* MSVC has no __builtin_popcount; __popcnt32 maps to the POPCNT
79
+ * instruction the /arch:AVX2 target guarantees. */
80
+ #if defined(_MSC_VER)
81
+ # define LEPTRIS_POPCOUNT32(x) __popcnt(x)
82
+ #else
83
+ # define LEPTRIS_POPCOUNT32(x) __builtin_popcount(x)
84
+ #endif
85
+
78
86
  /* Byte-population count: cmpeq → movemask → popcount per 32-byte
79
87
  * chunk, 16-byte SSE step, scalar tail. Never reads past s+len. */
80
88
  size_t leptris_text_count_char_avx2(const char* s, size_t len, char c) {
@@ -86,13 +94,13 @@ size_t leptris_text_count_char_avx2(const char* s, size_t len, char c) {
86
94
  while (end - p >= 32) {
87
95
  __m256i v = _mm256_loadu_si256((const __m256i*)p);
88
96
  __m256i eq = _mm256_cmpeq_epi8(v, needle);
89
- n += (size_t)__builtin_popcount((unsigned)_mm256_movemask_epi8(eq));
97
+ n += (size_t)LEPTRIS_POPCOUNT32((unsigned)_mm256_movemask_epi8(eq));
90
98
  p += 32;
91
99
  }
92
100
  if (end - p >= 16) {
93
101
  __m128i v = _mm_loadu_si128((const __m128i*)p);
94
102
  __m128i eq = _mm_cmpeq_epi8(v, _mm256_castsi256_si128(needle));
95
- n += (size_t)__builtin_popcount((unsigned)_mm_movemask_epi8(eq));
103
+ n += (size_t)LEPTRIS_POPCOUNT32((unsigned)_mm_movemask_epi8(eq));
96
104
  p += 16;
97
105
  }
98
106
  while (p < end) {
@@ -153,3 +161,149 @@ ptrdiff_t leptris_text_scan_run_avx2(const char* s, size_t len, char term,
153
161
  return (p < end) ? (ptrdiff_t)(p - s) : -1;
154
162
  }
155
163
  }
164
+
165
+ /* ---- UTF-8 validation (leptris#1842 A5) -------------------------------
166
+ *
167
+ * Per-16-byte block, per-byte required ranges derived from the three
168
+ * preceding bytes (carried across blocks via palignr):
169
+ * must_cont(j) = start2(j-1) | start3(j-1) | start4(j-1)
170
+ * | start3(j-2) | start4(j-2) | start4(j-3)
171
+ * where startN(v) = v in [C2..DF | E0..EF | F0..F4] by length class.
172
+ * A must-cont byte must land in its opener's continuation range
173
+ * (E0 -> A0..BF, ED -> 80..9F, F0 -> 90..BF, F4 -> 80..8F, else
174
+ * 80..BF); a non-cont byte must be ASCII or a legal start. The tail
175
+ * (<16 bytes) runs the scalar DFA anchored at a character boundary
176
+ * (walk back at most 3 bytes). Verdicts identical to
177
+ * leptris_utf8_valid_scalar — pinned by the encoding suite.
178
+ */
179
+ int leptris_utf8_valid_avx2(const char* src, size_t len) {
180
+ const unsigned char* p = (const unsigned char*)src;
181
+ if (len < 16) return leptris_utf8_valid_scalar(src, len);
182
+
183
+ const __m128i zero = _mm_setzero_si128();
184
+ const __m128i b80 = _mm_set1_epi8((char)0x80);
185
+ const __m128i bBF = _mm_set1_epi8((char)0xBF);
186
+ __m128i prev = zero; /* the previous block's 16 bytes */
187
+
188
+ size_t i = 0;
189
+ while (i + 16 <= len) {
190
+ __m128i cur = _mm_loadu_si128((const __m128i*)(p + i));
191
+ /* result[j] = byte(j-1): concat(a:b) shifted right leaves the
192
+ * PREVIOUS block's last lanes at the head — imm 15/14/13. */
193
+ __m128i b1 = _mm_alignr_epi8(cur, prev, 15);
194
+ __m128i b2 = _mm_alignr_epi8(cur, prev, 14);
195
+ __m128i b3 = _mm_alignr_epi8(cur, prev, 13);
196
+
197
+ /* start classes — the saturating idiom's operand order is the
198
+ * whole game: subs(v, lo)==0 tests v<=lo (saturate on
199
+ * underflow AND equality), so the LOWER bound must be
200
+ * subs(lo, v)==0 (0 iff lo<=v). subs(v, hi)==0 ⇔ v<=hi is
201
+ * already the right direction for the upper bound. */
202
+ #define IN_RANGE(v, lo, hi) \
203
+ (_mm_and_si128( \
204
+ _mm_cmpeq_epi8(_mm_subs_epu8(_mm_set1_epi8(lo), (v)), zero), \
205
+ _mm_cmpeq_epi8(_mm_subs_epu8((v), _mm_set1_epi8(hi)), zero)))
206
+ __m128i st2c = IN_RANGE(cur, (char)0xC2, (char)0xDF);
207
+ __m128i st3c = IN_RANGE(cur, (char)0xE0, (char)0xEF);
208
+ __m128i st4c = IN_RANGE(cur, (char)0xF0, (char)0xF4);
209
+ __m128i is_ascii =
210
+ _mm_cmpeq_epi8(_mm_and_si128(cur, b80), zero);
211
+ __m128i legal_start = _mm_or_si128(
212
+ st2c, _mm_or_si128(st3c, st4c));
213
+
214
+ __m128i st2b1 = IN_RANGE(b1, (char)0xC2, (char)0xDF);
215
+ __m128i st3b1 = IN_RANGE(b1, (char)0xE0, (char)0xEF);
216
+ __m128i st4b1 = IN_RANGE(b1, (char)0xF0, (char)0xF4);
217
+ __m128i st3b2 = IN_RANGE(b2, (char)0xE0, (char)0xEF);
218
+ __m128i st4b2 = IN_RANGE(b2, (char)0xF0, (char)0xF4);
219
+ __m128i st4b3 = IN_RANGE(b3, (char)0xF0, (char)0xF4);
220
+ #undef IN_RANGE
221
+
222
+ __m128i must_cont = _mm_or_si128(
223
+ _mm_or_si128(st2b1, _mm_or_si128(st3b1, st4b1)),
224
+ _mm_or_si128(_mm_or_si128(st3b2, st4b2), st4b3));
225
+
226
+ /* continuation range refinement from the opener (b1) */
227
+ const __m128i bA0 = _mm_set1_epi8((char)0xA0);
228
+ const __m128i b90 = _mm_set1_epi8((char)0x90);
229
+ const __m128i b9F = _mm_set1_epi8((char)0x9F);
230
+ const __m128i b8F = _mm_set1_epi8((char)0x8F);
231
+ const __m128i e0 = _mm_cmpeq_epi8(b1, _mm_set1_epi8((char)0xE0));
232
+ const __m128i ed = _mm_cmpeq_epi8(b1, _mm_set1_epi8((char)0xED));
233
+ const __m128i f0 = _mm_cmpeq_epi8(b1, _mm_set1_epi8((char)0xF0));
234
+ const __m128i f4 = _mm_cmpeq_epi8(b1, _mm_set1_epi8((char)0xF4));
235
+ __m128i lo = _mm_blendv_epi8(b80, bA0, e0);
236
+ lo = _mm_blendv_epi8(lo, b90, f0);
237
+ __m128i hi = _mm_blendv_epi8(bBF, b9F, ed);
238
+ hi = _mm_blendv_epi8(hi, b8F, f4);
239
+
240
+ /* cont-in-range — NEON's vcge/vcle pair translated to the
241
+ * saturating-subtraction idiom: subs(lo, cur) saturates to 0
242
+ * iff lo <= cur, subs(cur, hi) iff cur <= hi (the operand
243
+ * order is the whole game: subs(cur, lo) tests cur <= lo). */
244
+ __m128i cge = _mm_subs_epu8(lo, cur);
245
+ __m128i cle = _mm_subs_epu8(cur, hi);
246
+ __m128i cont_ok = _mm_and_si128(
247
+ _mm_cmpeq_epi8(cge, zero), _mm_cmpeq_epi8(cle, zero));
248
+
249
+ const __m128i ff = _mm_set1_epi8((char)0xFF);
250
+ __m128i not_must = _mm_xor_si128(must_cont, ff);
251
+ __m128i not_ascii = _mm_xor_si128(is_ascii, ff);
252
+ __m128i not_start = _mm_xor_si128(legal_start, ff);
253
+ /* error = must_cont & !cont_ok | !must_cont & !ascii &
254
+ * !legal_start (0x80..C1 and F5..FF fall out of the latter). */
255
+ __m128i block_error = _mm_or_si128(
256
+ _mm_and_si128(must_cont,
257
+ _mm_xor_si128(cont_ok, ff)),
258
+ _mm_and_si128(not_must,
259
+ _mm_and_si128(not_ascii, not_start)));
260
+ if (_mm_movemask_epi8(
261
+ _mm_cmpeq_epi8(_mm_and_si128(block_error, b80), b80)) != 0)
262
+ return 0;
263
+ prev = cur;
264
+ i += 16;
265
+ }
266
+ if (i >= len) {
267
+ /* The input ENDS at the block edge: the last sequence must
268
+ * be complete (the scalar DFA's "len - i <= n" rule). Walk
269
+ * back over at most 3 continuations; s2 lands ON the last
270
+ * continuation (or the final non-continuation byte). */
271
+ size_t s2 = i;
272
+ size_t conts = 0;
273
+ while (s2 > 0 && (p[s2 - 1] & 0xC0) == 0x80 && conts < 3) {
274
+ s2--;
275
+ conts++;
276
+ }
277
+ if (conts > 0) {
278
+ if (s2 == 0) return 0; /* continuations, no opener anywhere */
279
+ unsigned op = p[s2 - 1]; /* the opener BEFORE the run */
280
+ unsigned owed = (op >= 0xF0) ? 3
281
+ : (op >= 0xE0) ? 2
282
+ : (op >= 0xC2) ? 1
283
+ : 0;
284
+ if (owed > conts) return 0;
285
+ return 1;
286
+ }
287
+ /* the last byte is not a continuation: it must be ASCII */
288
+ unsigned op = p[i - 1];
289
+ unsigned owed = (op >= 0xF0) ? 3
290
+ : (op >= 0xE0) ? 2
291
+ : (op >= 0xC2) ? 1
292
+ : 0;
293
+ return owed == 0 ? 1 : 0;
294
+ }
295
+ /* Tail: scalar DFA anchored at a character boundary (at most 3
296
+ * bytes back from the SIMD end; a sequence is at most 4) — the
297
+ * anchored run also covers a sequence spanning the block edge.
298
+ * One more step back when the byte before the anchor is a start
299
+ * byte: an opener in the final block lane reaches into the tail
300
+ * (400k-case randomized cross-check vs the scalar oracle). */
301
+ size_t start = i;
302
+ while (start > 0 && (p[start] & 0xC0) == 0x80 && i - start < 3)
303
+ start--;
304
+ if (i - start < 3 && start > 0) {
305
+ unsigned char b = p[start - 1];
306
+ if (b >= 0xC2 && b <= 0xF4) start--;
307
+ }
308
+ return leptris_utf8_valid_scalar((const char*)(p + start), len - start);
309
+ }