noyalib/simd.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (c) 2026 Noyalib. All rights reserved.
3
4//! SIMD-friendly structural-scanning primitives.
5//!
6//! YAML's parser hot paths are dominated by *multi-byte search* —
7//! "find the next byte that's any of `[':', ',', '[', ']', '{',
8//! '}', '\\n', '#']`", "find the next non-blank byte", "find the
9//! end of a plain scalar". These searches dwarf the structural
10//! work for typical inputs because every byte of every scalar
11//! flows through them.
12//!
13//! This module ships a **feature-gated** primitive surface for
14//! that scanning, with three implementation strata:
15//!
16//! - **arity 1, 2, 3** — delegate to the [`memchr`] crate, which
17//! already compiles to SSE2 on x86_64 and NEON on aarch64. We
18//! gain nothing by hand-rolling.
19//! - **arity 4+** — pack 8 bytes per `u64` lane and use SWAR
20//! (SIMD Within A Register) to test all bytes in parallel
21//! against the needle bitmap. Beats byte-by-byte by ~3-5× on
22//! needle sets up to 16 bytes wide.
23//! - **fallback** — a portable scalar loop that the optimiser
24//! auto-vectorises on most LLVM targets.
25//!
26//! Pure-safe Rust — every primitive compiles under
27//! `#![forbid(unsafe_code)]`. The implementation does not call
28//! `core::arch::*` intrinsics directly; it lets memchr's already-
29//! validated unsafe abstractions do the platform dispatch and uses
30//! pure SWAR bit-twiddling for the wider-arity cases.
31//!
32//! # Examples
33//!
34//! ```
35//! use noyalib::simd::find_any_of;
36//!
37//! let haystack = b"some plain text: with a colon";
38//! // Find the first occurrence of any of these terminator bytes.
39//! let pos = find_any_of(haystack, &[b':', b',', b'\n']);
40//! assert_eq!(pos, Some(15));
41//! ```
42//!
43//! ```
44//! use noyalib::simd::find_any_of;
45//!
46//! // Returns None when no needle is present.
47//! assert_eq!(find_any_of(b"abcdef", &[b'X', b'Y', b'Z']), None);
48//! ```
49
50/// Find the byte offset of the first occurrence of any byte in
51/// `needles` within `haystack`.
52///
53/// Returns `None` when no needle byte is present.
54///
55/// Performance:
56///
57/// - 0 needles: returns `None` in `O(1)`.
58/// - 1, 2, 3 needles: routes through [`memchr`] for SSE2 / NEON
59/// acceleration where the platform supports it.
60/// - 4+ needles: uses SWAR (8-byte-stride packed comparison).
61/// - The scalar tail handles the last `< 8` bytes byte-by-byte.
62///
63/// All paths produce identical results — exhaustively tested by
64/// the equivalence suite in `tests/simd_equivalence.rs`.
65///
66/// # Examples
67///
68/// ```
69/// use noyalib::simd::find_any_of;
70///
71/// // 1-needle path (memchr).
72/// assert_eq!(find_any_of(b"abc:def", &[b':']), Some(3));
73///
74/// // 2-needle path (memchr2).
75/// assert_eq!(find_any_of(b"abc\ndef", &[b':', b'\n']), Some(3));
76///
77/// // 3-needle path (memchr3).
78/// assert_eq!(find_any_of(b"abc#def", &[b':', b'\n', b'#']), Some(3));
79///
80/// // 4+ needle path (SWAR).
81/// assert_eq!(
82/// find_any_of(b"name=value", &[b':', b',', b'=', b'\n']),
83/// Some(4),
84/// );
85/// ```
86#[must_use]
87pub fn find_any_of(haystack: &[u8], needles: &[u8]) -> Option<usize> {
88 match needles.len() {
89 0 => None,
90 1 => memchr::memchr(needles[0], haystack),
91 2 => memchr::memchr2(needles[0], needles[1], haystack),
92 3 => memchr::memchr3(needles[0], needles[1], needles[2], haystack),
93 _ => find_any_of_many(haystack, needles),
94 }
95}
96
97/// Length of the leading run of bytes in `haystack` that are NOT
98/// in `needles`. Equivalent to
99/// `find_any_of(haystack, needles).unwrap_or(haystack.len())` but
100/// expresses the "skip-clean-prefix" intent more directly at call
101/// sites. Useful in scanner inner loops where the next event is
102/// "consume the run, then handle the boundary byte".
103///
104/// **Polarity sibling**: see [`match_prefix_len`] for the inverse —
105/// "skip the leading run that *is* in the needle set." Mixing them
106/// up is a bug class (cf. commit 1e7dace which broke the YAML
107/// parser by passing the wrong polarity to `skip_blank`); the two
108/// names are deliberately verb-symmetric to make call sites
109/// self-documenting.
110///
111/// # Examples
112///
113/// ```
114/// use noyalib::simd::clean_prefix_len;
115///
116/// // The 7-byte run "abc def" precedes the colon at index 7.
117/// assert_eq!(clean_prefix_len(b"abc def: value", &[b':', b'\n']), 7);
118///
119/// // No needle byte at all — the whole input is "clean".
120/// assert_eq!(clean_prefix_len(b"all clean", &[b':', b'\n']), 9);
121///
122/// // Needle at index 0 — empty prefix.
123/// assert_eq!(clean_prefix_len(b":foo", &[b':']), 0);
124/// ```
125#[must_use]
126pub fn clean_prefix_len(haystack: &[u8], needles: &[u8]) -> usize {
127 find_any_of(haystack, needles).unwrap_or(haystack.len())
128}
129
130/// Length of the leading run of bytes in `haystack` that ARE in
131/// `needles`. The polarity-flipped sibling of [`clean_prefix_len`]:
132///
133/// - [`clean_prefix_len`] = skip while the byte is **not** a needle
134/// (consume the clean run; stop at the first interesting byte).
135/// - [`match_prefix_len`] = skip while the byte **is** a needle
136/// (consume the matched run; stop at the first content byte).
137///
138/// `match_prefix_len(rem, b" \t")` is the canonical
139/// "advance past blanks" primitive; see `Scanner::skip_blank`.
140///
141/// LLVM at `opt-level=3 + lto=fat` auto-vectorises the per-byte
142/// loop into 16/32-byte block scans, so the explicit SIMD path is
143/// not required for short runs (the typical 0-8 byte indent).
144/// Larger-arity needle sets fall through the same bitmap a
145/// scalar code would build for itself; the cost is a one-time
146/// `bitmap_for` call.
147///
148/// # Examples
149///
150/// ```
151/// use noyalib::simd::match_prefix_len;
152///
153/// // Three leading blanks, then `f`.
154/// assert_eq!(match_prefix_len(b" foo", b" \t"), 3);
155///
156/// // No leading match — empty prefix.
157/// assert_eq!(match_prefix_len(b"foo", b" \t"), 0);
158///
159/// // Whole input matches.
160/// assert_eq!(match_prefix_len(b"\t \t ", b" \t"), 4);
161///
162/// // Empty needle set — no byte ever matches, returns 0.
163/// assert_eq!(match_prefix_len(b"abc", b""), 0);
164/// ```
165#[must_use]
166pub fn match_prefix_len(haystack: &[u8], needles: &[u8]) -> usize {
167 if needles.is_empty() {
168 return 0;
169 }
170 // Single-byte needle: branch-predictable byte loop, fully
171 // auto-vectorisable by LLVM under opt-level=3+lto.
172 if needles.len() == 1 {
173 let n = needles[0];
174 let mut i = 0;
175 while i < haystack.len() && haystack[i] == n {
176 i += 1;
177 }
178 return i;
179 }
180 // Multi-byte needle: build the 256-bit bitmap once, scan via
181 // O(1) lookup per haystack byte.
182 let bm = bitmap_for(needles);
183 let mut i = 0;
184 while i < haystack.len() && bm.contains(haystack[i]) {
185 i += 1;
186 }
187 i
188}
189
190/// Find the byte offset of the first occurrence of any byte whose
191/// value is set in `bitmap`. The bitmap encodes a 256-bit set —
192/// bit `b` is set ⇔ byte value `b` is a needle.
193///
194/// This is the dispatch shape every other primitive in this module
195/// reduces to. Exposed publicly for callers (parser hot paths,
196/// formatter pre-pass) that want to amortise the bitmap
197/// construction across many calls with the same needle set.
198///
199/// # Examples
200///
201/// ```
202/// use noyalib::simd::{bitmap_for, find_byte_in_bitmap};
203///
204/// // Build the bitmap once — `:` (0x3A) and `\n` (0x0A).
205/// let bm = bitmap_for(&[b':', b'\n']);
206/// assert_eq!(find_byte_in_bitmap(b"foo:bar", &bm), Some(3));
207/// assert_eq!(find_byte_in_bitmap(b"all good", &bm), None);
208/// ```
209#[must_use]
210pub fn find_byte_in_bitmap(haystack: &[u8], bitmap: &ByteBitmap) -> Option<usize> {
211 for (i, &b) in haystack.iter().enumerate() {
212 if bitmap.contains(b) {
213 return Some(i);
214 }
215 }
216 None
217}
218
219/// 256-bit bitmap of byte values, used by
220/// [`find_byte_in_bitmap`]. Construct via [`bitmap_for`].
221///
222/// # Examples
223///
224/// ```
225/// use noyalib::simd::{bitmap_for, ByteBitmap};
226///
227/// let bm: ByteBitmap = bitmap_for(&[b'a', b'b']);
228/// assert!(bm.contains(b'a'));
229/// assert!(!bm.contains(b'c'));
230/// ```
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
232pub struct ByteBitmap {
233 /// 4 × 64-bit lanes covering byte values 0..=255.
234 lanes: [u64; 4],
235}
236
237impl ByteBitmap {
238 /// `true` if `byte` is in the bitmap.
239 ///
240 /// # Examples
241 ///
242 /// ```
243 /// use noyalib::simd::bitmap_for;
244 ///
245 /// let bm = bitmap_for(&[b'A']);
246 /// assert!(bm.contains(b'A'));
247 /// assert!(!bm.contains(b'B'));
248 /// ```
249 #[must_use]
250 #[inline]
251 pub fn contains(self, byte: u8) -> bool {
252 let lane = (byte >> 6) as usize;
253 let bit = byte & 0x3F;
254 (self.lanes[lane] >> bit) & 1 == 1
255 }
256}
257
258/// Build a [`ByteBitmap`] from a list of bytes. Duplicate entries
259/// are coalesced (idempotent set semantics).
260///
261/// # Examples
262///
263/// ```
264/// use noyalib::simd::bitmap_for;
265///
266/// let bm = bitmap_for(&[b':', b',', b':']);
267/// assert!(bm.contains(b':'));
268/// assert!(bm.contains(b','));
269/// assert!(!bm.contains(b';'));
270/// ```
271#[must_use]
272pub fn bitmap_for(needles: &[u8]) -> ByteBitmap {
273 let mut lanes = [0u64; 4];
274 for &b in needles {
275 let lane = (b >> 6) as usize;
276 let bit = b & 0x3F;
277 lanes[lane] |= 1u64 << bit;
278 }
279 ByteBitmap { lanes }
280}
281
282/// SWAR (SIMD Within A Register) implementation for the 4+ needle
283/// path. Process 8 bytes at a time by packing them into a `u64`
284/// and testing each lane against the needle bitmap in a single
285/// pass per chunk.
286///
287/// The classical SWAR byte-set test would broadcast each needle to
288/// 8 lanes and compare — that's 8 needles × 8 lanes per chunk =
289/// 64 comparisons. We instead use a 256-bit "byte-class" bitmap
290/// lookup: for each input byte `b`, a single shift+and decides
291/// membership. This stays branchless and scales to arbitrary
292/// needle widths without the comparison-count blow-up.
293///
294/// The scalar tail handles the last `< 8` bytes byte-by-byte; the
295/// chunk loop is the throughput-critical path.
296fn find_any_of_many(haystack: &[u8], needles: &[u8]) -> Option<usize> {
297 let bitmap = bitmap_for(needles);
298
299 // Chunked path — 8 bytes per iteration. Each byte is tested
300 // against the bitmap independently; the shape stays
301 // branchless inside the chunk loop, which lets LLVM
302 // auto-vectorise on most targets.
303 let mut i = 0;
304 let chunk = 8;
305 while i + chunk <= haystack.len() {
306 for j in 0..chunk {
307 if bitmap.contains(haystack[i + j]) {
308 return Some(i + j);
309 }
310 }
311 i += chunk;
312 }
313 // Scalar tail.
314 while i < haystack.len() {
315 if bitmap.contains(haystack[i]) {
316 return Some(i);
317 }
318 i += 1;
319 }
320 None
321}
322
323/// Build-once, scan-many structural scanner.
324///
325/// Caches the needle set as a [`ByteBitmap`] (and, on nightly with
326/// `nightly-simd`, broadcast SIMD vectors) so each call to
327/// [`SimdScanner::find_any`] does no per-call setup work — the
328/// classic shape for a scanner used inside a parser hot loop.
329///
330/// On stable Rust this is a thin wrapper around the existing
331/// [`find_any_of`] / SWAR machinery. On nightly with the
332/// `nightly-simd` Cargo feature it widens to a 32-byte
333/// `Simd<u8, N>` chunk loop using portable SIMD, eliminating the
334/// per-byte branch and producing a tight `or` / `to_bitmask` /
335/// `trailing_zeros` inner loop. Both paths are bit-for-bit
336/// equivalent — exhaustively cross-checked by
337/// `tests/simd_equivalence.rs`.
338///
339/// # Examples
340///
341/// ```
342/// use noyalib::simd::SimdScanner;
343///
344/// let scanner = SimdScanner::new(b":-[]{}#\n");
345/// assert_eq!(scanner.find_any(b"name: value"), Some(4));
346/// assert_eq!(scanner.find_any(b"all-clean-text"), Some(3));
347/// assert_eq!(scanner.find_any(b"abcdef"), None);
348/// ```
349#[derive(Debug, Clone, Copy)]
350pub struct SimdScanner {
351 bitmap: ByteBitmap,
352 /// Snapshot of the needles for the SIMD broadcast path. Bounded
353 /// at 16 entries — beyond that the bitmap test is cheaper than
354 /// 16+ broadcast comparisons. Shared between stable and nightly
355 /// to keep the type layout stable across feature builds.
356 #[cfg_attr(not(all(feature = "nightly-simd", noyalib_nightly)), allow(dead_code))]
357 needles: [u8; 16],
358 #[cfg_attr(not(all(feature = "nightly-simd", noyalib_nightly)), allow(dead_code))]
359 needle_count: u8,
360}
361
362impl SimdScanner {
363 /// Build a scanner from a needle byte set. Duplicate needles
364 /// are coalesced (set semantics). Needle count beyond 16 falls
365 /// through to the bitmap-only path on every chunk.
366 ///
367 /// # Examples
368 ///
369 /// ```
370 /// use noyalib::simd::SimdScanner;
371 /// let s = SimdScanner::new(b":-{}");
372 /// assert_eq!(s.find_any(b"a-b"), Some(1));
373 /// ```
374 #[must_use]
375 pub fn new(needles: &[u8]) -> Self {
376 let bitmap = bitmap_for(needles);
377 let mut compact = [0u8; 16];
378 let mut count: u8 = 0;
379 let mut seen = [false; 256];
380 for &b in needles {
381 if seen[b as usize] {
382 continue;
383 }
384 seen[b as usize] = true;
385 if (count as usize) < compact.len() {
386 compact[count as usize] = b;
387 count += 1;
388 }
389 }
390 Self {
391 bitmap,
392 needles: compact,
393 needle_count: count,
394 }
395 }
396
397 /// Index of the first byte in `haystack` that matches any needle.
398 ///
399 /// Returns `None` when the haystack contains no needle byte.
400 ///
401 /// # Examples
402 ///
403 /// ```
404 /// use noyalib::simd::SimdScanner;
405 /// let s = SimdScanner::new(b":\n#");
406 /// assert_eq!(s.find_any(b"a comment # here"), Some(10));
407 /// assert_eq!(s.find_any(b"plain"), None);
408 /// ```
409 #[must_use]
410 pub fn find_any(&self, haystack: &[u8]) -> Option<usize> {
411 #[cfg(all(feature = "nightly-simd", noyalib_nightly))]
412 {
413 self.find_any_simd(haystack)
414 }
415 #[cfg(not(all(feature = "nightly-simd", noyalib_nightly)))]
416 {
417 self.find_any_scalar(haystack)
418 }
419 }
420
421 /// Stable, bitmap-driven scan. The auto-vectoriser handles the
422 /// inner loop on most LLVM targets; `nightly-simd` upgrades to
423 /// an explicit `Simd<u8, N>` chunk loop.
424 fn find_any_scalar(&self, haystack: &[u8]) -> Option<usize> {
425 for (i, &b) in haystack.iter().enumerate() {
426 if self.bitmap.contains(b) {
427 return Some(i);
428 }
429 }
430 None
431 }
432
433 /// Portable-SIMD scan. Walks `haystack` in 32-byte strides,
434 /// broadcasting each needle and OR-ing the equality masks. The
435 /// first set bit in the combined mask gives the byte offset
436 /// inside the chunk; the scalar tail handles `< 32` leftovers.
437 ///
438 /// Falls back to the bitmap loop when the needle set has more
439 /// than 8 distinct bytes (each broadcast costs a vector
440 /// register, and the bitmap test wins beyond that point).
441 #[cfg(all(feature = "nightly-simd", noyalib_nightly))]
442 fn find_any_simd(&self, haystack: &[u8]) -> Option<usize> {
443 use core::simd::cmp::SimdPartialEq as _;
444 use core::simd::{Mask, Simd};
445
446 const LANES: usize = 32;
447 // Beyond 8 needles the broadcast path costs more than the
448 // bitmap test — fall through to the scalar path which uses
449 // a single `bitmap.contains` per byte and is itself
450 // auto-vectorised by LLVM for the contains side.
451 let needle_count = self.needle_count as usize;
452 if needle_count > 8 {
453 return self.find_any_scalar(haystack);
454 }
455 let active = &self.needles[..needle_count];
456
457 let mut i = 0;
458 while i + LANES <= haystack.len() {
459 let chunk: Simd<u8, LANES> = Simd::from_slice(&haystack[i..i + LANES]);
460 let mut combined: Mask<i8, LANES> = Mask::splat(false);
461 for &n in active {
462 let needle: Simd<u8, LANES> = Simd::splat(n);
463 combined |= chunk.simd_eq(needle);
464 }
465 let bits = combined.to_bitmask();
466 if bits != 0 {
467 return Some(i + bits.trailing_zeros() as usize);
468 }
469 i += LANES;
470 }
471 // Scalar tail.
472 for (offset, &b) in haystack[i..].iter().enumerate() {
473 if self.bitmap.contains(b) {
474 return Some(i + offset);
475 }
476 }
477 None
478 }
479
480 /// Produce a 32-bit "structural bitmask" for a 32-byte chunk:
481 /// bit `i` is set iff `chunk[i]` is in the scanner's needle set.
482 ///
483 /// This is the building block of the `simdjson`-style structural
484 /// discovery loop — instead of walking the haystack byte by byte
485 /// and stopping at every delimiter, the caller produces a
486 /// dense 32-bit bitmask per chunk and walks the bits via
487 /// `trailing_zeros()`. Each `1` bit is one delimiter, each `0`
488 /// bit is one byte the scanner can skip without inspection.
489 ///
490 /// On stable Rust the inner loop is bytewise + bit-set (LLVM
491 /// auto-vectorises). With the `nightly-simd` Cargo feature on a
492 /// nightly toolchain, it widens to a single `Simd<u8, 32>`
493 /// chunk + `to_bitmask()` — one branchless dispatch per
494 /// 32-byte window.
495 ///
496 /// Pair with [`StructuralIter`] to walk every structural byte
497 /// position in a haystack of arbitrary length; that helper
498 /// handles the chunk-loop boundary and the partial-chunk tail.
499 ///
500 /// # Examples
501 ///
502 /// ```
503 /// use noyalib::simd::SimdScanner;
504 /// let s = SimdScanner::new(b":-{}\n");
505 ///
506 /// // 32-byte chunk: structural bytes at positions 0, 4, 8, 17.
507 /// // " 0 4 8 17"
508 /// let chunk: [u8; 32] = *b":bcd-fgh:abcdefgh{ijklmnopqrstuv";
509 /// let mask = s.structural_bitmask_32(&chunk);
510 /// // bits 0, 4, 8, 17 set.
511 /// assert_eq!(mask & (1 << 0), 1 << 0);
512 /// assert_eq!(mask & (1 << 4), 1 << 4);
513 /// assert_eq!(mask & (1 << 8), 1 << 8);
514 /// assert_eq!(mask & (1 << 17), 1 << 17);
515 /// ```
516 #[must_use]
517 pub fn structural_bitmask_32(&self, chunk: &[u8; 32]) -> u32 {
518 #[cfg(all(feature = "nightly-simd", noyalib_nightly))]
519 {
520 self.structural_bitmask_32_simd(chunk)
521 }
522 #[cfg(not(all(feature = "nightly-simd", noyalib_nightly)))]
523 {
524 self.structural_bitmask_32_scalar(chunk)
525 }
526 }
527
528 fn structural_bitmask_32_scalar(&self, chunk: &[u8; 32]) -> u32 {
529 let mut mask: u32 = 0;
530 for (i, &b) in chunk.iter().enumerate() {
531 // Branchless: shift the bool result into bit position i.
532 // Produces a tight inner loop that LLVM auto-vectorises
533 // on most targets.
534 mask |= u32::from(self.bitmap.contains(b)) << i;
535 }
536 mask
537 }
538
539 #[cfg(all(feature = "nightly-simd", noyalib_nightly))]
540 fn structural_bitmask_32_simd(&self, chunk: &[u8; 32]) -> u32 {
541 use core::simd::cmp::SimdPartialEq as _;
542 use core::simd::{Mask, Simd};
543
544 let needle_count = self.needle_count as usize;
545 if needle_count == 0 {
546 return 0;
547 }
548 if needle_count > 8 {
549 // Beyond 8 needles the broadcast path costs more than
550 // the bitmap test — fall through to the scalar path.
551 return self.structural_bitmask_32_scalar(chunk);
552 }
553
554 let active = &self.needles[..needle_count];
555 let chunk_v: Simd<u8, 32> = Simd::from_slice(chunk);
556 let mut combined: Mask<i8, 32> = Mask::splat(false);
557 for &n in active {
558 let needle: Simd<u8, 32> = Simd::splat(n);
559 combined |= chunk_v.simd_eq(needle);
560 }
561 // `to_bitmask()` returns the chunk lanes as a primitive
562 // bitset. For LANES=32 this is a `u32`; we reinforce the
563 // type so the scalar fall-back signature stays identical.
564 combined.to_bitmask() as u32
565 }
566}
567
568/// Canonical YAML 1.2 plain-scalar boundary candidate set in
569/// **block** context. The scanner consumes a plain scalar by
570/// "jumping" to the first byte in this set, then validating
571/// whether it is a true terminator (e.g. `:` is only a key
572/// indicator when followed by whitespace, `#` is only a comment
573/// when preceded by whitespace).
574///
575/// This is the input to [`SimdScanner::new`] / [`StructuralIter`]
576/// for the block-context plain-scalar fast path.
577///
578/// # Examples
579///
580/// ```
581/// use noyalib::simd::{BLOCK_PLAIN_NEEDLES, SimdScanner};
582/// let s = SimdScanner::new(BLOCK_PLAIN_NEEDLES);
583/// assert_eq!(s.find_any(b"hello: world"), Some(5));
584/// ```
585pub const BLOCK_PLAIN_NEEDLES: &[u8] = b": \t";
586
587/// Canonical YAML 1.2 plain-scalar boundary candidate set inside
588/// **flow** collections (`[ ]`, `{ }`). Adds the flow-collection
589/// terminators (`,`, `[`, `]`, `{`, `}`) to the block set.
590///
591/// The wider needle set means the scanner sees more candidate
592/// boundaries per chunk in flow context — exactly the workload
593/// where `StructuralIter`'s 32-byte-bitmask discovery beats a
594/// per-candidate restart pattern.
595///
596/// # Examples
597///
598/// ```
599/// use noyalib::simd::{FLOW_PLAIN_NEEDLES, SimdScanner};
600/// let s = SimdScanner::new(FLOW_PLAIN_NEEDLES);
601/// assert_eq!(s.find_any(b"hello, world"), Some(5));
602/// ```
603pub const FLOW_PLAIN_NEEDLES: &[u8] = b": \t,[]{}";
604
605/// Canonical newline-discovery needles. Used by block-scalar
606/// (`|` / `>`) line counting, comment-text scanning, and any
607/// other "find next line break" hot path.
608///
609/// # Examples
610///
611/// ```
612/// use noyalib::simd::{LINE_BREAK_NEEDLES, SimdScanner};
613/// let s = SimdScanner::new(LINE_BREAK_NEEDLES);
614/// assert_eq!(s.find_any(b"line one\r\nline two"), Some(8));
615/// ```
616pub const LINE_BREAK_NEEDLES: &[u8] = b"\n\r";
617
618/// Iterator over every structural-byte position in a haystack.
619///
620/// Uses [`SimdScanner::structural_bitmask_32`] under the hood: each
621/// 32-byte chunk produces a `u32` bitmask, and the iterator walks
622/// the bits via `mask.trailing_zeros()` + `mask & (mask - 1)` (the
623/// classic "blsr" pattern). The state machine in a parser inner
624/// loop can advance directly from one delimiter to the next without
625/// inspecting any of the bytes between them — the same shape that
626/// powers `simdjson`'s structural-character pass.
627///
628/// # Examples
629///
630/// ```
631/// use noyalib::simd::{SimdScanner, StructuralIter};
632///
633/// let s = SimdScanner::new(b":\n");
634/// let positions: Vec<usize> =
635/// StructuralIter::new(&s, b"k1: v1\nk2: v2\n").collect();
636/// assert_eq!(positions, vec![2, 6, 9, 13]);
637/// ```
638#[derive(Debug)]
639pub struct StructuralIter<'a> {
640 scanner: &'a SimdScanner,
641 haystack: &'a [u8],
642 /// Next byte position to scan (chunk loader anchors here).
643 cursor: usize,
644 /// Base offset for `cached_mask`'s bits — the start of the
645 /// chunk that produced the cached bits.
646 cached_base: usize,
647 /// Bitmask of structural positions inside the chunk that
648 /// started at `cached_base`. Bit `i` in the mask corresponds
649 /// to byte `cached_base + i` in `haystack`.
650 cached_mask: u32,
651}
652
653impl<'a> StructuralIter<'a> {
654 /// Construct an iterator over every structural-byte position in
655 /// `haystack` according to `scanner`'s needle set.
656 ///
657 /// # Examples
658 ///
659 /// ```
660 /// use noyalib::simd::{SimdScanner, StructuralIter};
661 /// let s = SimdScanner::new(b":\n");
662 /// let it = StructuralIter::new(&s, b"a: b\n");
663 /// let v: Vec<usize> = it.collect();
664 /// assert_eq!(v, vec![1, 4]);
665 /// ```
666 #[must_use]
667 pub fn new(scanner: &'a SimdScanner, haystack: &'a [u8]) -> Self {
668 StructuralIter {
669 scanner,
670 haystack,
671 cursor: 0,
672 cached_base: 0,
673 cached_mask: 0,
674 }
675 }
676}
677
678impl Iterator for StructuralIter<'_> {
679 type Item = usize;
680
681 fn next(&mut self) -> Option<usize> {
682 // Drain the cached chunk first.
683 if self.cached_mask != 0 {
684 let bit = self.cached_mask.trailing_zeros() as usize;
685 // Clear the lowest set bit (`x & (x - 1)` — the standard
686 // `BLSR` idiom).
687 self.cached_mask &= self.cached_mask - 1;
688 return Some(self.cached_base + bit);
689 }
690
691 // Refill from the next 32-byte chunk; if the tail is
692 // shorter than 32 bytes, fall back to the scalar contains
693 // test for the remaining bytes.
694 loop {
695 if self.cursor >= self.haystack.len() {
696 return None;
697 }
698 let remaining = self.haystack.len() - self.cursor;
699 if remaining >= 32 {
700 let mut chunk = [0u8; 32];
701 chunk.copy_from_slice(&self.haystack[self.cursor..self.cursor + 32]);
702 let mask = self.scanner.structural_bitmask_32(&chunk);
703 let chunk_origin = self.cursor;
704 self.cursor += 32;
705 if mask != 0 {
706 let bit = mask.trailing_zeros() as usize;
707 self.cached_base = chunk_origin;
708 self.cached_mask = mask & (mask - 1);
709 return Some(chunk_origin + bit);
710 }
711 continue;
712 }
713
714 // Tail (< 32 bytes): scalar scan.
715 for offset in 0..remaining {
716 let b = self.haystack[self.cursor + offset];
717 if self.scanner.bitmap.contains(b) {
718 let pos = self.cursor + offset;
719 self.cursor = pos + 1;
720 return Some(pos);
721 }
722 }
723 self.cursor = self.haystack.len();
724 return None;
725 }
726 }
727}
728
729/// SWAR decimal parser: parse an unsigned 64-bit integer from a
730/// byte slice of ASCII digits. Returns `None` if the slice is
731/// empty, contains a non-digit byte, or overflows `u64`.
732///
733/// Uses SWAR (SIMD Within A Register) when the input is at least
734/// 8 bytes long: 8 ASCII digits are parsed in a single `u64`
735/// multiply-shift pipeline, eliminating the per-byte branch the
736/// stdlib `<u64 as FromStr>::from_str` walks. For inputs shorter
737/// than 8 bytes the scalar fallback runs — a tight `* 10 + d`
738/// loop that LLVM auto-vectorises on most targets.
739///
740/// Pure-safe Rust — preserves the workspace
741/// `#![forbid(unsafe_code)]` invariant. Validates every byte is
742/// in `b'0'..=b'9'` before computing the result, so malformed
743/// input never produces a garbage answer.
744///
745/// # Examples
746///
747/// ```
748/// use noyalib::simd::parse_decimal_u64;
749/// assert_eq!(parse_decimal_u64(b"12345"), Some(12_345));
750/// assert_eq!(parse_decimal_u64(b"9223372036854775807"),
751/// Some(9_223_372_036_854_775_807));
752/// assert_eq!(parse_decimal_u64(b""), None);
753/// assert_eq!(parse_decimal_u64(b"12a4"), None);
754/// assert_eq!(parse_decimal_u64(b"99999999999999999999"), None); // overflow
755/// ```
756#[must_use]
757pub fn parse_decimal_u64(bytes: &[u8]) -> Option<u64> {
758 if bytes.is_empty() {
759 return None;
760 }
761 let mut result: u64 = 0;
762 let mut i = 0;
763 // SWAR fast path: process 8 digits per iteration. Bypass when
764 // the remaining slice is shorter — the validation cost would
765 // outweigh the SWAR multiply pipeline.
766 while i + 8 <= bytes.len() {
767 let mut arr = [0u8; 8];
768 arr.copy_from_slice(&bytes[i..i + 8]);
769 // Shift the accumulator up by 8 decimal places and add the new
770 // chunk. A non-numeric chunk or overflow bails via `?`.
771 let eight = parse_8_digits(arr)?;
772 result = result.checked_mul(100_000_000)?.checked_add(eight)?;
773 i += 8;
774 }
775 // Scalar tail: 0..7 remaining bytes.
776 while i < bytes.len() {
777 let b = bytes[i];
778 if !b.is_ascii_digit() {
779 return None;
780 }
781 result = result.checked_mul(10)?.checked_add(u64::from(b - b'0'))?;
782 i += 1;
783 }
784 Some(result)
785}
786
787/// SWAR decimal parser for signed `i64`. Accepts an optional
788/// leading `+` or `-`, then forwards to [`parse_decimal_u64`].
789/// Returns `None` for empty input, non-digit bytes, or overflow
790/// in either direction.
791///
792/// # Examples
793///
794/// ```
795/// use noyalib::simd::parse_decimal_i64;
796/// assert_eq!(parse_decimal_i64(b"42"), Some(42));
797/// assert_eq!(parse_decimal_i64(b"-42"), Some(-42));
798/// assert_eq!(parse_decimal_i64(b"+42"), Some(42));
799/// assert_eq!(parse_decimal_i64(b"-9223372036854775808"), Some(i64::MIN));
800/// assert_eq!(parse_decimal_i64(b"9223372036854775808"), None);
801/// ```
802#[must_use]
803pub fn parse_decimal_i64(bytes: &[u8]) -> Option<i64> {
804 if bytes.is_empty() {
805 return None;
806 }
807 let (negative, digits) = match bytes[0] {
808 b'-' => (true, &bytes[1..]),
809 b'+' => (false, &bytes[1..]),
810 _ => (false, bytes),
811 };
812 let abs = parse_decimal_u64(digits)?;
813 if negative {
814 // Special-case `i64::MIN` whose absolute value (2^63) does
815 // not fit in i64. The unsigned form 9_223_372_036_854_775_808
816 // converts directly via the wrapping cast.
817 if abs == (i64::MAX as u64) + 1 {
818 Some(i64::MIN)
819 } else {
820 i64::try_from(abs).ok().map(|n| -n)
821 }
822 } else {
823 i64::try_from(abs).ok()
824 }
825}
826
827/// Parse exactly 8 ASCII digits in a single SWAR pipeline.
828///
829/// The classic `(* 10) + (* 100) + (* 10000)` ladder folds the
830/// 8-byte block into a single `u64` digit value via three
831/// shift-add-mask phases. Each phase pairs adjacent units so the
832/// total instruction count is independent of the input value —
833/// branch-free arithmetic on a `u64` register.
834///
835/// Returns `None` if any byte is outside `b'0'..=b'9'`.
836fn parse_8_digits(arr: [u8; 8]) -> Option<u64> {
837 let chunk = u64::from_be_bytes(arr);
838 // Validate: every byte is in 0x30..=0x39 ('0'..='9'), in two
839 // carry-free steps. The previous form subtracted 0x30 and added
840 // 0x76 across the whole register, but a whole-register add or
841 // subtract propagates carries *between byte lanes*, so a block
842 // mixing bytes below '0' with bytes above '9' could cancel its
843 // own evidence and pass —
844 // `parse_decimal_u64(&[0x07, b'+', b'0', b'9', b'.', 0x07, 1, 1])`
845 // returned `Some(…)`. Found by the Kani harness
846 // `swar_8_digit_fold_matches_reference` (proofs module below),
847 // which proves the current form over all 2^64 blocks.
848 //
849 // (a) every high nibble is exactly 0x3 …
850 if chunk & 0xF0F0_F0F0_F0F0_F0F0 != 0x3030_3030_3030_3030 {
851 return None;
852 }
853 // … so every byte is 0x30..=0x3F, and (b) adding 0x06 pushes a
854 // low nibble of 10..=15 into the high nibble (0x3A + 0x06 =
855 // 0x40) without ever carrying into the neighbouring lane
856 // (max 0x3F + 0x06 = 0x45).
857 if chunk.wrapping_add(0x0606_0606_0606_0606) & 0xF0F0_F0F0_F0F0_F0F0 != 0x3030_3030_3030_3030 {
858 return None;
859 }
860 // All bytes verified '0'..='9': the digit value of each lane is
861 // its low nibble.
862 let sub = chunk & 0x0F0F_0F0F_0F0F_0F0F;
863 // SWAR fold: three phases of pair-wise (high*N + low). The
864 // big-endian `from_be_bytes` reading puts the leftmost digit
865 // in the highest byte, which is exactly what the per-pair
866 // shift-and-mask pattern wants.
867 //
868 // Phase 1 — pair adjacent bytes (16-bit half-words):
869 // for each pair (high_byte, low_byte): high*10 + low.
870 // Multiplying high by 10 cannot overflow the half-word
871 // (max digit 9 → 90 < 256), so the pairs stay independent.
872 let high = (sub & 0xFF00_FF00_FF00_FF00) >> 8;
873 let low = sub & 0x00FF_00FF_00FF_00FF;
874 let chunk = high.wrapping_mul(10).wrapping_add(low);
875
876 // Phase 2 — pair adjacent half-words (32-bit halves):
877 // for each pair (high_word, low_word): high*100 + low.
878 // Max half-word value is 99 * 100 + 99 = 9999 < 65 536, so
879 // each 32-bit pair stays independent.
880 let high = (chunk & 0xFFFF_0000_FFFF_0000) >> 16;
881 let low = chunk & 0x0000_FFFF_0000_FFFF;
882 let chunk = high.wrapping_mul(100).wrapping_add(low);
883
884 // Phase 3 — combine the two 32-bit halves into a single u64:
885 // high * 10_000 + low. Max high half is 9999, so high*10_000
886 // = 99_990_000; plus 9999 = 99_999_999 < 2^32 — fits.
887 let high = chunk >> 32;
888 let low = chunk & 0xFFFF_FFFF;
889 Some(high.wrapping_mul(10_000).wrapping_add(low))
890}
891
892#[cfg(test)]
893#[allow(clippy::byte_char_slices)]
894mod tests {
895 use super::*;
896
897 #[test]
898 fn empty_haystack_returns_none() {
899 assert_eq!(find_any_of(b"", &[b':']), None);
900 assert_eq!(find_any_of(b"", &[b':', b',', b'#', b'=']), None);
901 }
902
903 #[test]
904 fn empty_needles_returns_none() {
905 assert_eq!(find_any_of(b"hello", &[]), None);
906 }
907
908 #[test]
909 fn arity_1_finds_first() {
910 assert_eq!(find_any_of(b"abc:def:ghi", &[b':']), Some(3));
911 }
912
913 #[test]
914 fn arity_2_finds_first_of_either() {
915 assert_eq!(find_any_of(b"abc#def\n", &[b':', b'#']), Some(3));
916 assert_eq!(find_any_of(b"abc\ndef#", &[b':', b'#']), Some(7));
917 }
918
919 #[test]
920 fn arity_3_finds_first_of_three() {
921 assert_eq!(find_any_of(b"abc\ndef:ghi", &[b':', b'\n', b'#']), Some(3));
922 }
923
924 #[test]
925 fn arity_4_uses_swar_path() {
926 let needles = &[b':', b',', b'=', b'\n'];
927 assert_eq!(find_any_of(b"abc=def", needles), Some(3));
928 assert_eq!(find_any_of(b"plain text only", needles), None);
929 }
930
931 #[test]
932 fn arity_8_finds_correctly() {
933 let needles = b"[]{}:,#\n";
934 assert_eq!(find_any_of(b"hello{world", needles), Some(5));
935 assert_eq!(find_any_of(b"plain text", needles), None);
936 }
937
938 #[test]
939 fn boundary_at_chunk_edge() {
940 // Place the needle at byte index 7, 8, 9 — straddling the
941 // 8-byte SWAR chunk boundary.
942 for pos in 0..16 {
943 let mut input = vec![b'.'; 16];
944 input[pos] = b':';
945 assert_eq!(
946 find_any_of(&input, &[b':', b',', b'#', b'\n', b'=']),
947 Some(pos),
948 "needle at position {pos}",
949 );
950 }
951 }
952
953 #[test]
954 fn long_haystack_finds_at_far_position() {
955 let mut input = vec![b'.'; 1024];
956 input[1000] = b':';
957 assert_eq!(
958 find_any_of(&input, &[b':', b',', b'\n', b'#', b'=']),
959 Some(1000),
960 );
961 }
962
963 #[test]
964 fn long_haystack_with_no_match_returns_none() {
965 let input = vec![b'.'; 1024];
966 assert_eq!(find_any_of(&input, &[b':', b',', b'#', b'\n', b'=']), None);
967 }
968
969 #[test]
970 fn clean_prefix_len_basic() {
971 assert_eq!(clean_prefix_len(b"abc:def", &[b':']), 3);
972 assert_eq!(clean_prefix_len(b"all clean", &[b':']), 9);
973 assert_eq!(clean_prefix_len(b":start", &[b':']), 0);
974 }
975
976 #[test]
977 fn bitmap_membership() {
978 let bm = bitmap_for(&[b'A', b'\n', 0]);
979 assert!(bm.contains(b'A'));
980 assert!(bm.contains(b'\n'));
981 assert!(bm.contains(0));
982 assert!(!bm.contains(b'B'));
983 assert!(!bm.contains(255));
984 }
985
986 #[test]
987 fn bitmap_full_set() {
988 let needles: Vec<u8> = (0u8..=255).collect();
989 let bm = bitmap_for(&needles);
990 for b in 0u8..=255 {
991 assert!(bm.contains(b), "byte {b}");
992 }
993 }
994
995 /// Equivalence: arity-4+ SWAR path matches the scalar baseline
996 /// across many inputs. Both paths produce identical results
997 /// for every position, every needle set, every byte value.
998 #[test]
999 fn swar_equivalence_with_scalar() {
1000 let scalar = |haystack: &[u8], needles: &[u8]| -> Option<usize> {
1001 for (i, &b) in haystack.iter().enumerate() {
1002 if needles.contains(&b) {
1003 return Some(i);
1004 }
1005 }
1006 None
1007 };
1008
1009 // Deterministic pseudo-random inputs covering boundary
1010 // straddling, sparse needles, dense needles.
1011 let needle_sets: &[&[u8]] = &[
1012 b"[]{}:,#\n",
1013 b":,=", // arity 3 — sanity-check the dispatch boundary
1014 b":", // arity 1
1015 b":,#=[]{}\n\t \"'", // 13 needles
1016 ];
1017 for &needles in needle_sets {
1018 for length in [0usize, 1, 7, 8, 9, 16, 17, 63, 64, 65, 1023, 1024] {
1019 let mut buf = vec![0u8; length];
1020 // Fill with non-needle bytes.
1021 for (i, slot) in buf.iter_mut().enumerate() {
1022 *slot = (i as u8).wrapping_add(33); // start past control bytes
1023 while needles.contains(slot) {
1024 *slot = slot.wrapping_add(1);
1025 }
1026 }
1027 // No-needle case.
1028 assert_eq!(find_any_of(&buf, needles), scalar(&buf, needles));
1029 // Sprinkle a needle at every position; both paths
1030 // must agree.
1031 for pos in 0..length {
1032 let saved = buf[pos];
1033 buf[pos] = needles[pos % needles.len()];
1034 assert_eq!(
1035 find_any_of(&buf, needles),
1036 scalar(&buf, needles),
1037 "mismatch needles={needles:?} length={length} pos={pos}",
1038 );
1039 buf[pos] = saved;
1040 }
1041 }
1042 }
1043 }
1044
1045 #[test]
1046 fn scanner_basic_finds_first_needle() {
1047 let s = SimdScanner::new(b":-{}");
1048 assert_eq!(s.find_any(b"a-b"), Some(1));
1049 assert_eq!(s.find_any(b"abc"), None);
1050 }
1051
1052 #[test]
1053 fn scanner_long_input_match_at_far_position() {
1054 let s = SimdScanner::new(b":,#\n[]{}");
1055 let mut buf = vec![b'.'; 4096];
1056 buf[3000] = b'#';
1057 assert_eq!(s.find_any(&buf), Some(3000));
1058 }
1059
1060 #[test]
1061 fn scanner_matches_baseline_across_needle_sets() {
1062 let baseline = |haystack: &[u8], needles: &[u8]| -> Option<usize> {
1063 for (i, &b) in haystack.iter().enumerate() {
1064 if needles.contains(&b) {
1065 return Some(i);
1066 }
1067 }
1068 None
1069 };
1070 let needle_sets: &[&[u8]] = &[
1071 b":\n",
1072 b":,#\n",
1073 b"[]{}:,#\n",
1074 b"abcdefghij", // 10 needles — exercises the >8 fall-through
1075 ];
1076 for &needles in needle_sets {
1077 let s = SimdScanner::new(needles);
1078 for length in [0usize, 1, 31, 32, 33, 64, 127, 128, 1024] {
1079 let mut buf = vec![0u8; length];
1080 for (i, slot) in buf.iter_mut().enumerate() {
1081 *slot = (i as u8).wrapping_add(33);
1082 while needles.contains(slot) {
1083 *slot = slot.wrapping_add(1);
1084 }
1085 }
1086 assert_eq!(
1087 s.find_any(&buf),
1088 baseline(&buf, needles),
1089 "no-needle: needles={needles:?} len={length}"
1090 );
1091 for pos in 0..length {
1092 let saved = buf[pos];
1093 buf[pos] = needles[pos % needles.len()];
1094 assert_eq!(
1095 s.find_any(&buf),
1096 baseline(&buf, needles),
1097 "needles={needles:?} len={length} pos={pos}",
1098 );
1099 buf[pos] = saved;
1100 }
1101 }
1102 }
1103 }
1104
1105 // ── structural_bitmask_32 + StructuralIter ─────────────────────
1106
1107 fn baseline_bitmask_32(chunk: &[u8; 32], needles: &[u8]) -> u32 {
1108 let mut m: u32 = 0;
1109 for (i, &b) in chunk.iter().enumerate() {
1110 if needles.contains(&b) {
1111 m |= 1u32 << i;
1112 }
1113 }
1114 m
1115 }
1116
1117 #[test]
1118 fn structural_bitmask_marks_every_needle_position() {
1119 let s = SimdScanner::new(b":-{}\n");
1120 let chunk: [u8; 32] = *b":bcd-fgh:abcdefgh{ijklmnopqrstuv";
1121 let mask = s.structural_bitmask_32(&chunk);
1122 // Bytes at positions 0, 4, 8, 17 are needles.
1123 assert_eq!(mask & (1 << 0), 1 << 0);
1124 assert_eq!(mask & (1 << 4), 1 << 4);
1125 assert_eq!(mask & (1 << 8), 1 << 8);
1126 assert_eq!(mask & (1 << 17), 1 << 17);
1127 // Sanity: no other bits set.
1128 assert_eq!(mask, (1 << 0) | (1 << 4) | (1 << 8) | (1 << 17));
1129 }
1130
1131 #[test]
1132 fn structural_bitmask_is_zero_for_clean_chunks() {
1133 let s = SimdScanner::new(b":\n");
1134 let chunk = *b"plain text only no delim chars!.";
1135 assert_eq!(s.structural_bitmask_32(&chunk), 0);
1136 }
1137
1138 #[test]
1139 fn structural_bitmask_matches_baseline_across_needle_sets() {
1140 // Test every YAML-relevant needle set against deterministic
1141 // chunks: the SIMD path and the scalar path must produce
1142 // bit-for-bit equal masks.
1143 let needle_sets: &[&[u8]] = &[
1144 b":", // arity 1
1145 b":\n", // arity 2
1146 b":,#", // arity 3
1147 b":-[]{}\n", // arity 7
1148 b":-[]{}\n# \t", // arity 10 (>8 — exercises fall-through)
1149 ];
1150 for &needles in needle_sets {
1151 let s = SimdScanner::new(needles);
1152 // 64 deterministic chunks: each one has needle bytes at
1153 // varying positions + filler.
1154 for variant in 0..64u8 {
1155 let mut chunk = [0u8; 32];
1156 for (i, slot) in chunk.iter_mut().enumerate() {
1157 *slot = (i as u8).wrapping_add(33).wrapping_add(variant);
1158 while needles.contains(slot) {
1159 *slot = slot.wrapping_add(1);
1160 }
1161 }
1162 // Sprinkle needles at some positions deterministically.
1163 for j in (0..32).step_by(3) {
1164 if (variant as usize + j) & 1 == 1 {
1165 chunk[j] = needles[(variant as usize + j) % needles.len()];
1166 }
1167 }
1168 let actual = s.structural_bitmask_32(&chunk);
1169 let expected = baseline_bitmask_32(&chunk, needles);
1170 assert_eq!(
1171 actual, expected,
1172 "needles={needles:?} variant={variant} chunk={chunk:?}"
1173 );
1174 }
1175 }
1176 }
1177
1178 #[test]
1179 fn structural_iter_yields_positions_in_order() {
1180 let s = SimdScanner::new(b":\n");
1181 let positions: Vec<usize> = StructuralIter::new(&s, b"k1: v1\nk2: v2\n").collect();
1182 assert_eq!(positions, vec![2, 6, 9, 13]);
1183 }
1184
1185 #[test]
1186 fn structural_iter_handles_empty_input() {
1187 let s = SimdScanner::new(b":");
1188 let positions: Vec<usize> = StructuralIter::new(&s, b"").collect();
1189 assert!(positions.is_empty(), "expected empty, got {positions:?}");
1190 }
1191
1192 #[test]
1193 fn structural_iter_handles_partial_chunk_tail() {
1194 // Tail less than 32 bytes — exercises the scalar fall-back
1195 // inside StructuralIter::next.
1196 let s = SimdScanner::new(b":");
1197 let positions: Vec<usize> = StructuralIter::new(&s, b"abc:def:gh").collect();
1198 assert_eq!(positions, vec![3, 7]);
1199 }
1200
1201 #[test]
1202 fn structural_iter_spans_multiple_chunks() {
1203 // Build a 100-byte haystack with a needle every 25 bytes —
1204 // the iterator must straddle the 32-byte chunk boundary
1205 // cleanly and not double-count or miss any.
1206 let mut buf = vec![b'.'; 100];
1207 for &p in &[5usize, 25, 60, 95] {
1208 buf[p] = b':';
1209 }
1210 let s = SimdScanner::new(b":");
1211 let positions: Vec<usize> = StructuralIter::new(&s, &buf).collect();
1212 assert_eq!(positions, vec![5, 25, 60, 95]);
1213 }
1214
1215 #[test]
1216 fn structural_iter_count_matches_scalar_baseline() {
1217 let s = SimdScanner::new(b":\n,#");
1218 // Adversarial input — alternating needle / non-needle.
1219 let buf: Vec<u8> = (0..2048u32)
1220 .map(|i| if i % 7 == 0 { b':' } else { b'a' })
1221 .collect();
1222 let scalar: Vec<usize> = buf
1223 .iter()
1224 .enumerate()
1225 .filter_map(|(i, &b)| (b == b':').then_some(i))
1226 .collect();
1227 let simd: Vec<usize> = StructuralIter::new(&s, &buf).collect();
1228 assert_eq!(simd, scalar);
1229 }
1230
1231 // ── SWAR decimal parsing ──────────────────────────────────────
1232
1233 #[test]
1234 fn parse_decimal_u64_basic() {
1235 assert_eq!(parse_decimal_u64(b"0"), Some(0));
1236 assert_eq!(parse_decimal_u64(b"42"), Some(42));
1237 assert_eq!(parse_decimal_u64(b"12345678"), Some(12_345_678));
1238 assert_eq!(parse_decimal_u64(b"99999999"), Some(99_999_999));
1239 }
1240
1241 #[test]
1242 fn parse_decimal_u64_long_inputs_swar_path() {
1243 assert_eq!(parse_decimal_u64(b"1234567890"), Some(1_234_567_890));
1244 assert_eq!(
1245 parse_decimal_u64(b"9999999999999999"),
1246 Some(9_999_999_999_999_999),
1247 );
1248 assert_eq!(
1249 parse_decimal_u64(b"1234567812345678"),
1250 Some(1_234_567_812_345_678),
1251 );
1252 }
1253
1254 #[test]
1255 fn parse_decimal_u64_max() {
1256 assert_eq!(parse_decimal_u64(b"18446744073709551615"), Some(u64::MAX));
1257 }
1258
1259 #[test]
1260 fn parse_decimal_u64_rejects_non_digits() {
1261 assert_eq!(parse_decimal_u64(b""), None);
1262 assert_eq!(parse_decimal_u64(b"12a4"), None);
1263 assert_eq!(parse_decimal_u64(b" 42"), None);
1264 assert_eq!(parse_decimal_u64(b"42 "), None);
1265 assert_eq!(parse_decimal_u64(b"-42"), None);
1266 assert_eq!(parse_decimal_u64(b"+42"), None);
1267 }
1268
1269 #[test]
1270 fn parse_decimal_u64_rejects_cross_lane_carry_blocks() {
1271 // Kani counterexample for the original validation: the
1272 // whole-register subtract/add propagated carries between
1273 // byte lanes, so a block mixing bytes below '0' with bytes
1274 // above '9' cancelled its own evidence and parsed as
1275 // `Some(…)`. Proven impossible for the current form by the
1276 // `swar_8_digit_fold_matches_reference` harness.
1277 assert_eq!(
1278 parse_decimal_u64(&[0x07, b'+', b'0', b'9', b'.', 0x07, 0x01, 0x01]),
1279 None
1280 );
1281 // A few more shapes from the same class: a lane below '0'
1282 // adjacent to a lane above '9'.
1283 assert_eq!(
1284 parse_decimal_u64(&[0xFF, b'0', b'0', b'0', b'0', b'0', b'0', b'0']),
1285 None
1286 );
1287 assert_eq!(
1288 parse_decimal_u64(&[b'0', 0x2F, b':', b'0', b'0', b'0', b'0', b'0']),
1289 None
1290 );
1291 assert_eq!(
1292 parse_decimal_i64(&[b'-', 0x07, b'+', b'0', b'9', b'.', 0x07, 0x01, 0x01]),
1293 None
1294 );
1295 }
1296
1297 #[test]
1298 fn parse_decimal_u64_overflow_returns_none() {
1299 // u64::MAX + 1
1300 assert_eq!(parse_decimal_u64(b"18446744073709551616"), None);
1301 // Way overflow
1302 assert_eq!(parse_decimal_u64(b"99999999999999999999999"), None,);
1303 }
1304
1305 #[test]
1306 fn parse_decimal_u64_matches_stdlib_baseline() {
1307 // Sweep many lengths and values to ensure SWAR path is
1308 // bit-for-bit equivalent to the stdlib parser for every
1309 // length 1..=20.
1310 for n in [
1311 0u64,
1312 1,
1313 9,
1314 10,
1315 99,
1316 100,
1317 999,
1318 1000,
1319 12345,
1320 1234567,
1321 12345678,
1322 123456789,
1323 9876543210,
1324 1234567890123456,
1325 9_223_372_036_854_775_807, // i64::MAX
1326 10_000_000_000_000_000_000,
1327 u64::MAX - 1,
1328 u64::MAX,
1329 ] {
1330 let s = n.to_string();
1331 assert_eq!(parse_decimal_u64(s.as_bytes()), Some(n), "n={n}");
1332 }
1333 }
1334
1335 #[test]
1336 fn parse_decimal_i64_handles_signs() {
1337 assert_eq!(parse_decimal_i64(b"42"), Some(42));
1338 assert_eq!(parse_decimal_i64(b"+42"), Some(42));
1339 assert_eq!(parse_decimal_i64(b"-42"), Some(-42));
1340 assert_eq!(parse_decimal_i64(b"0"), Some(0));
1341 assert_eq!(parse_decimal_i64(b"-0"), Some(0));
1342 }
1343
1344 #[test]
1345 fn parse_decimal_i64_handles_min_max() {
1346 assert_eq!(parse_decimal_i64(b"9223372036854775807"), Some(i64::MAX));
1347 assert_eq!(parse_decimal_i64(b"-9223372036854775808"), Some(i64::MIN));
1348 }
1349
1350 #[test]
1351 fn parse_decimal_i64_rejects_overflow() {
1352 // i64::MAX + 1
1353 assert_eq!(parse_decimal_i64(b"9223372036854775808"), None);
1354 // i64::MIN - 1
1355 assert_eq!(parse_decimal_i64(b"-9223372036854775809"), None);
1356 }
1357
1358 #[test]
1359 fn parse_decimal_i64_matches_stdlib_across_full_range() {
1360 for n in [
1361 0i64,
1362 1,
1363 -1,
1364 10,
1365 -10,
1366 100,
1367 -100,
1368 i64::from(i32::MIN),
1369 i64::from(i32::MAX),
1370 i64::MIN,
1371 i64::MAX,
1372 i64::MAX - 1,
1373 i64::MIN + 1,
1374 -123_456_789,
1375 987_654_321,
1376 ] {
1377 let s = n.to_string();
1378 assert_eq!(parse_decimal_i64(s.as_bytes()), Some(n), "n={n}");
1379 }
1380 }
1381}
1382
1383/// Kani proof harnesses — machine-checked properties of the SWAR
1384/// numeric parsers against a naive per-byte reference. Run with
1385/// `cargo kani` (the `kani-proofs` CI workflow); invisible to
1386/// ordinary builds. Every harness pins `kissat`: CBMC's default
1387/// SAT solver did not finish the fold proof in an hour, kissat
1388/// finishes it in ~16s.
1389///
1390/// What is proven, and what deliberately is not:
1391///
1392/// - the 8-digit fold: exhaustive over all 2^64 byte blocks —
1393/// where `proptest` above draws a few hundred samples, this
1394/// closes the property (and its first run found the cross-lane
1395/// carry bug the fold's validation now guards against);
1396/// - `parse_decimal_u64`: value equivalence on all-digit slices up
1397/// to one chunk, and rejection of any non-digit anywhere in
1398/// chunk-plus-tail;
1399/// - `parse_decimal_i64`: sign handling over signed digit strings
1400/// up to a sign plus one chunk.
1401///
1402/// Multi-chunk value composition and the 19-20-digit overflow
1403/// boundaries are *not* proven: the accumulator multiplies push
1404/// the SAT instance past any CI-sane budget (a 20-byte bound ran
1405/// >10 min without terminating). Those stay covered by the unit
1406/// tests and proptest above — they are plain `checked_mul` /
1407/// `checked_add` arithmetic, not SWAR.
1408#[cfg(kani)]
1409mod proofs {
1410 use super::*;
1411
1412 /// Reference semantics: per-byte checked `* 10 + d` parse —
1413 /// the loop the SWAR pipeline replaces. `None` on empty input,
1414 /// any non-digit byte, or `u64` overflow.
1415 fn reference_parse_u64(bytes: &[u8]) -> Option<u64> {
1416 if bytes.is_empty() {
1417 return None;
1418 }
1419 let mut acc: u64 = 0;
1420 for &b in bytes {
1421 if !b.is_ascii_digit() {
1422 return None;
1423 }
1424 acc = acc.checked_mul(10)?.checked_add(u64::from(b - b'0'))?;
1425 }
1426 Some(acc)
1427 }
1428
1429 /// Reference semantics for the signed form: optional sign,
1430 /// then [`reference_parse_u64`], with `i64::MIN` representable
1431 /// and `-0` equal to `0`.
1432 fn reference_parse_i64(bytes: &[u8]) -> Option<i64> {
1433 if bytes.is_empty() {
1434 return None;
1435 }
1436 let (negative, digits) = match bytes[0] {
1437 b'-' => (true, &bytes[1..]),
1438 b'+' => (false, &bytes[1..]),
1439 _ => (false, bytes),
1440 };
1441 let abs = reference_parse_u64(digits)?;
1442 if negative {
1443 if abs > (i64::MAX as u64) + 1 {
1444 None
1445 } else {
1446 Some(0i64.wrapping_sub_unsigned(abs))
1447 }
1448 } else {
1449 i64::try_from(abs).ok()
1450 }
1451 }
1452
1453 /// The wrapping-arithmetic SWAR fold agrees with the reference
1454 /// on ALL 2^64 possible 8-byte blocks — every digit block
1455 /// parses to its decimal value, every block containing a
1456 /// non-digit byte is rejected, and no wraparound in the
1457 /// validate/fold phases ever leaks a wrong answer. ~16s under
1458 /// kissat.
1459 #[kani::proof]
1460 #[kani::solver(kissat)]
1461 fn swar_8_digit_fold_matches_reference() {
1462 let arr: [u8; 8] = kani::any();
1463 assert_eq!(parse_8_digits(arr), reference_parse_u64(&arr));
1464 }
1465
1466 /// `parse_decimal_u64` agrees with the reference on every
1467 /// all-digit slice up to 8 bytes (the full single-chunk SWAR
1468 /// path; verifies in ~26s under kissat). Split from the
1469 /// rejection property below because full equivalence over
1470 /// arbitrary bytes makes the solver enumerate every non-digit
1471 /// exit path at every symbolic length, and value equivalence
1472 /// past one chunk (a second chunk, or chunk + tail) adds
1473 /// accumulator multiplies the solver did not finish inside any
1474 /// CI-sane budget — those compositions, and the 20-digit
1475 /// overflow boundary (`checked_mul`/`checked_add`'s job, not
1476 /// SWAR's), stay with the unit tests and proptest.
1477 #[kani::proof]
1478 #[kani::solver(kissat)]
1479 #[kani::unwind(9)]
1480 fn parse_decimal_u64_value_matches_reference_on_digits() {
1481 const MAX: usize = 8;
1482 let arr: [u8; MAX] = kani::any();
1483 kani::assume(arr.iter().all(u8::is_ascii_digit));
1484 let len: usize = kani::any();
1485 kani::assume(len <= MAX);
1486 assert_eq!(
1487 parse_decimal_u64(&arr[..len]),
1488 reference_parse_u64(&arr[..len])
1489 );
1490 }
1491
1492 /// A slice holding ANY non-digit byte, anywhere in a chunk or
1493 /// the tail, is rejected (up to 12 bytes — chunk plus tail;
1494 /// ~3s under kissat). The other half of the documented
1495 /// contract ("malformed input never produces a garbage
1496 /// answer"), and exactly the property the carry-propagation
1497 /// bug violated.
1498 #[kani::proof]
1499 #[kani::solver(kissat)]
1500 #[kani::unwind(13)]
1501 fn parse_decimal_u64_rejects_any_non_digit() {
1502 const MAX: usize = 12;
1503 let arr: [u8; MAX] = kani::any();
1504 let len: usize = kani::any();
1505 kani::assume(len <= MAX);
1506 kani::assume(arr[..len].iter().any(|b| !b.is_ascii_digit()));
1507 assert_eq!(parse_decimal_u64(&arr[..len]), None);
1508 }
1509
1510 /// The signed wrapper's sign handling agrees with the signed
1511 /// reference on optionally-signed digit strings — `+`, `-`,
1512 /// and the bare form, up to a sign plus 8 digits (~102s under
1513 /// kissat). The `i64::MIN` / overflow boundary at 19-20 digits
1514 /// stays with the unit tests for the same solver-budget reason
1515 /// as above.
1516 #[kani::proof]
1517 #[kani::solver(kissat)]
1518 #[kani::unwind(10)]
1519 fn parse_decimal_i64_matches_reference_on_signed_digits() {
1520 const MAX: usize = 9;
1521 let arr: [u8; MAX] = kani::any();
1522 kani::assume(arr[0] == b'-' || arr[0] == b'+' || arr[0].is_ascii_digit());
1523 kani::assume(arr[1..].iter().all(u8::is_ascii_digit));
1524 let len: usize = kani::any();
1525 kani::assume(len <= MAX);
1526 assert_eq!(
1527 parse_decimal_i64(&arr[..len]),
1528 reference_parse_i64(&arr[..len])
1529 );
1530 }
1531}