Skip to main content

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}