Skip to main content

fhp_simd/
scalar.rs

1//! Portable scalar fallback for all SIMD operations.
2//!
3//! Every function here processes bytes one at a time. It is the baseline
4//! that SIMD backends must match semantically (and beat in throughput).
5
6use crate::{DelimiterResult, classify_byte};
7
8/// Scan `haystack` for the first occurrence of any HTML delimiter.
9///
10/// Delimiters: `<`, `>`, `&`, `"`, `'`, `=`, `/`.
11///
12/// # Safety
13///
14/// This function is safe. The `unsafe fn` signature exists so that it
15/// can be stored in the same function-pointer slot as the SIMD variants
16/// (which require `unsafe` due to target-feature intrinsics).
17pub unsafe fn find_delimiters(haystack: &[u8]) -> DelimiterResult {
18    find_delimiters_safe(haystack)
19}
20
21/// Safe inner implementation of [`find_delimiters`].
22#[inline]
23pub fn find_delimiters_safe(haystack: &[u8]) -> DelimiterResult {
24    for (i, &b) in haystack.iter().enumerate() {
25        if is_delimiter(b) {
26            return DelimiterResult::Found { pos: i, byte: b };
27        }
28    }
29    DelimiterResult::NotFound
30}
31
32/// Classify each byte in `input` into a category bitmask.
33///
34/// Returns a `Vec<u8>` of the same length as `input`, where each element
35/// is one of the constants from [`crate::class`].
36///
37/// # Safety
38///
39/// This function is safe. The `unsafe fn` signature matches the SIMD
40/// dispatch slot.
41pub unsafe fn classify_bytes(input: &[u8]) -> Vec<u8> {
42    classify_bytes_safe(input)
43}
44
45/// Safe inner implementation of [`classify_bytes`].
46#[inline]
47pub fn classify_bytes_safe(input: &[u8]) -> Vec<u8> {
48    input.iter().map(|&b| classify_byte(b)).collect()
49}
50
51/// Skip leading whitespace bytes and return the byte offset of the first
52/// non-whitespace byte (or `input.len()` if the entire slice is whitespace).
53///
54/// # Safety
55///
56/// This function is safe. The `unsafe fn` signature matches the SIMD
57/// dispatch slot.
58pub unsafe fn skip_whitespace(input: &[u8]) -> usize {
59    skip_whitespace_safe(input)
60}
61
62/// Safe inner implementation of [`skip_whitespace`].
63#[inline]
64pub fn skip_whitespace_safe(input: &[u8]) -> usize {
65    input
66        .iter()
67        .position(|&b| !b.is_ascii_whitespace())
68        .unwrap_or(input.len())
69}
70
71/// Produce a bitmask where bit `i` is set if `block[i] == byte`.
72///
73/// Processes up to 64 bytes. Bits beyond `block.len()` are always 0.
74///
75/// # Safety
76///
77/// This function is safe. The `unsafe fn` signature matches the SIMD
78/// dispatch slot.
79pub unsafe fn compute_byte_mask(block: &[u8], byte: u8) -> u64 {
80    compute_byte_mask_safe(block, byte)
81}
82
83/// Safe inner implementation of [`compute_byte_mask`].
84///
85/// The result is a 64-bit mask, so at most the first 64 bytes of `block` are
86/// considered; any bytes beyond offset 64 are ignored.
87#[inline]
88pub fn compute_byte_mask_safe(block: &[u8], byte: u8) -> u64 {
89    let mut mask = 0u64;
90    for (i, &b) in block.iter().take(64).enumerate() {
91        if b == byte {
92            mask |= 1u64 << i;
93        }
94    }
95    mask
96}
97
98/// Compute all seven delimiter bitmasks in a single pass over the block.
99///
100/// # Safety
101///
102/// This function is safe. The `unsafe fn` signature matches the SIMD
103/// dispatch slot.
104pub unsafe fn compute_all_masks(block: &[u8]) -> crate::AllMasks {
105    compute_all_masks_safe(block)
106}
107
108/// Safe inner implementation of [`compute_all_masks`].
109///
110/// The masks are 64-bit, so at most the first 64 bytes of `block` are
111/// considered; any bytes beyond offset 64 are ignored.
112#[inline]
113pub fn compute_all_masks_safe(block: &[u8]) -> crate::AllMasks {
114    let mut masks = crate::AllMasks::default();
115    for (i, &b) in block.iter().take(64).enumerate() {
116        let bit = 1u64 << i;
117        match b {
118            b'<' => masks.lt |= bit,
119            b'>' => masks.gt |= bit,
120            b'"' => masks.quot |= bit,
121            b'\'' => masks.apos |= bit,
122            _ => {}
123        }
124    }
125    masks
126}
127
128/// Returns `true` if `b` is one of the 7 HTML delimiters.
129#[inline(always)]
130fn is_delimiter(b: u8) -> bool {
131    matches!(b, b'<' | b'>' | b'&' | b'"' | b'\'' | b'=' | b'/')
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::class;
138
139    #[test]
140    fn find_delimiters_lt() {
141        let input = b"hello <world>";
142        let result = unsafe { find_delimiters(input) };
143        assert_eq!(result, DelimiterResult::Found { pos: 6, byte: b'<' });
144    }
145
146    #[test]
147    fn find_delimiters_amp() {
148        let input = b"a &amp; b";
149        let result = unsafe { find_delimiters(input) };
150        assert_eq!(result, DelimiterResult::Found { pos: 2, byte: b'&' });
151    }
152
153    #[test]
154    fn find_delimiters_none() {
155        let input = b"hello world";
156        let result = unsafe { find_delimiters(input) };
157        assert_eq!(result, DelimiterResult::NotFound);
158    }
159
160    #[test]
161    fn find_delimiters_empty() {
162        let result = unsafe { find_delimiters(b"") };
163        assert_eq!(result, DelimiterResult::NotFound);
164    }
165
166    #[test]
167    fn find_delimiters_first_byte() {
168        let result = unsafe { find_delimiters(b"<html>") };
169        assert_eq!(result, DelimiterResult::Found { pos: 0, byte: b'<' });
170    }
171
172    #[test]
173    fn find_delimiters_all_types() {
174        for &delim in b"<>&\"'=/" {
175            let input = [b'x', b'x', delim, b'x'];
176            let result = unsafe { find_delimiters(&input) };
177            assert_eq!(
178                result,
179                DelimiterResult::Found {
180                    pos: 2,
181                    byte: delim
182                },
183                "failed for delimiter 0x{delim:02X}"
184            );
185        }
186    }
187
188    #[test]
189    fn classify_bytes_mixed() {
190        let input = b"a1 <";
191        let result = unsafe { classify_bytes(input) };
192        assert_eq!(result[0], class::ALPHA); // 'a'
193        assert_eq!(result[1], class::DIGIT); // '1'
194        assert_eq!(result[2], class::WHITESPACE); // ' '
195        assert_eq!(result[3], class::DELIMITER); // '<'
196    }
197
198    #[test]
199    fn classify_bytes_empty() {
200        let result = unsafe { classify_bytes(b"") };
201        assert!(result.is_empty());
202    }
203
204    #[test]
205    fn skip_whitespace_leading() {
206        let result = unsafe { skip_whitespace(b"   hello") };
207        assert_eq!(result, 3);
208    }
209
210    #[test]
211    fn skip_whitespace_mixed() {
212        let result = unsafe { skip_whitespace(b" \t\n\rX") };
213        assert_eq!(result, 4);
214    }
215
216    #[test]
217    fn skip_whitespace_all() {
218        let result = unsafe { skip_whitespace(b"   ") };
219        assert_eq!(result, 3);
220    }
221
222    #[test]
223    fn skip_whitespace_none() {
224        let result = unsafe { skip_whitespace(b"hello") };
225        assert_eq!(result, 0);
226    }
227
228    #[test]
229    fn skip_whitespace_empty() {
230        let result = unsafe { skip_whitespace(b"") };
231        assert_eq!(result, 0);
232    }
233
234    #[test]
235    fn compute_byte_mask_basic() {
236        let input = b"hello <world>";
237        let mask = unsafe { compute_byte_mask(input, b'<') };
238        assert_eq!(mask, 1 << 6);
239    }
240
241    #[test]
242    fn compute_byte_mask_multiple() {
243        let input = b"a<b<c";
244        let mask = unsafe { compute_byte_mask(input, b'<') };
245        assert_eq!(mask, (1 << 1) | (1 << 3));
246    }
247
248    #[test]
249    fn compute_byte_mask_none() {
250        let input = b"hello world";
251        let mask = unsafe { compute_byte_mask(input, b'<') };
252        assert_eq!(mask, 0);
253    }
254
255    #[test]
256    fn compute_byte_mask_empty() {
257        let mask = unsafe { compute_byte_mask(b"", b'<') };
258        assert_eq!(mask, 0);
259    }
260
261    #[test]
262    fn compute_byte_mask_over_64_bytes_does_not_overflow() {
263        // A u64 mask can only represent 64 positions. An over-long block must
264        // be capped at 64 rather than overflowing the shift (UB/panic).
265        let input = vec![b'<'; 80];
266        let mask = compute_byte_mask_safe(&input, b'<');
267        assert_eq!(mask, u64::MAX, "first 64 '<' set, rest ignored");
268    }
269
270    #[test]
271    fn compute_all_masks_over_64_bytes_does_not_overflow() {
272        let input = vec![b'<'; 80];
273        let masks = compute_all_masks_safe(&input);
274        assert_eq!(masks.lt, u64::MAX);
275    }
276
277    #[test]
278    fn compute_all_masks_basic() {
279        let input = b"<div class=\"foo\">";
280        let masks = unsafe { compute_all_masks(input) };
281        assert_eq!(masks.lt, 1 << 0); // '<' at 0
282        assert_eq!(masks.gt, 1 << 16); // '>' at 16
283        assert_eq!(masks.quot, (1 << 11) | (1 << 15)); // '"' at 11 and 15
284    }
285
286    #[test]
287    fn compute_all_masks_empty() {
288        let masks = unsafe { compute_all_masks(b"") };
289        assert_eq!(masks.lt, 0);
290        assert_eq!(masks.gt, 0);
291    }
292
293    #[test]
294    fn compute_all_masks_matches_individual() {
295        let input = b"Hello <World> & \"test\" = 'value' / 123\n\r\t end!!";
296        let masks = unsafe { compute_all_masks(input) };
297        assert_eq!(masks.lt, unsafe { compute_byte_mask(input, b'<') });
298        assert_eq!(masks.gt, unsafe { compute_byte_mask(input, b'>') });
299        assert_eq!(masks.quot, unsafe { compute_byte_mask(input, b'"') });
300        assert_eq!(masks.apos, unsafe { compute_byte_mask(input, b'\'') });
301    }
302}