vyre_libs/scan/substring/substring.rs
1//! Brute-force substring search - each invocation checks whether the
2//! needle matches at its starting byte offset, writes `1` to the
3//! match bitmap at that offset on hit.
4//!
5//! Category A composition. Sufficient for short needles; long
6//! needles should compile to a DFA via the future `dfa_compile`
7//! function and use that as a prefilter.
8
9use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
10
11use crate::region::wrap_anonymous;
12
13/// Canonical scan op id.
14pub const SCAN_SUBSTRING_OP_ID: &str = "vyre-libs::scan::substring_search";
15
16/// Build a Program that writes `1` to `matches[i]` when `haystack[i..]`
17/// starts with `needle`, else `0`. Both buffers are u32 byte arrays
18/// packed one byte per u32 for simplicity (a future packed-u8 version
19/// is Category A over `DataType::U8`).
20#[must_use]
21pub fn substring_search(
22 haystack: &str,
23 needle: &str,
24 matches: &str,
25 haystack_len: u32,
26 needle_len: u32,
27) -> Program {
28 build_substring_program(haystack, needle, matches, haystack_len, needle_len)
29}
30
31fn build_substring_program(
32 haystack: &str,
33 needle: &str,
34 matches: &str,
35 haystack_len: u32,
36 needle_len: u32,
37) -> Program {
38 let counted_storage = |name: &str, binding, count| {
39 let decl = BufferDecl::storage(name, binding, BufferAccess::ReadOnly, DataType::U32);
40 if count == 0 {
41 decl
42 } else {
43 decl.with_count(count)
44 }
45 };
46 let output_count = haystack_len.max(1);
47 let visible_output_bytes = (haystack_len as usize).saturating_mul(4);
48 let output = BufferDecl::output(matches, 2, DataType::U32)
49 .with_count(output_count)
50 .with_output_byte_range(0..visible_output_bytes);
51
52 let i = Expr::var("i");
53 // ok accumulates AND of per-byte equality checks. Start at 1; each
54 // byte mismatch AND-s in 0 and latches the match bit off.
55 let mut check_body: Vec<Node> = vec![Node::let_bind("ok", Expr::u32(1))];
56 // Walk the needle one byte at a time. bytes are packed u32/byte for
57 // simplicity - a packed-u8 variant is Category A over DataType::U8.
58 check_body.push(Node::loop_for(
59 "k",
60 Expr::u32(0),
61 Expr::u32(needle_len),
62 vec![Node::assign(
63 "ok",
64 Expr::bitand(
65 Expr::var("ok"),
66 // Select turns the bool comparison into u32 {0,1} so
67 // the accumulator stays in integer arithmetic.
68 Expr::select(
69 Expr::eq(
70 Expr::load(haystack, Expr::add(i.clone(), Expr::var("k"))),
71 Expr::load(needle, Expr::var("k")),
72 ),
73 Expr::u32(1),
74 Expr::u32(0),
75 ),
76 ),
77 )],
78 ));
79 check_body.push(Node::Store {
80 buffer: matches.into(),
81 index: i.clone(),
82 value: Expr::var("ok"),
83 });
84
85 // Overflow-safe guard. The straight expression `i + needle_len <= buf_len`
86 // can wrap at i ≈ u32::MAX − needle_len, producing a false positive on the
87 // last few offsets. The correct invariant is `i <= buf_len - needle_len`;
88 // we rewrite it as a subtraction-free chain of comparisons by reasoning
89 // through `buf_len` only:
90 //
91 // needle_len <= buf_len ∧ i + needle_len <= buf_len
92 //
93 // Passing both conjuncts also handles the empty-haystack case (buf_len=0,
94 // needle_len=0, i=0 → both true → vacuous check_body).
95 // V7-CORR-006: the original guard `i + needle_len <= haystack_len`
96 // wraps when i is near u32::MAX (Expr::add is Expr::BinOp { Add,
97 // .. } which u32::wrapping_add). We rewrite as two separate
98 // non-wrapping comparisons: (1) needle_len <= haystack_len ensures
99 // the implicit subtraction in (2) is non-underflowing, and (2)
100 // `i <= haystack_len - needle_len` keeps the rhs a constant-folded
101 // expression from the builder so no wrap is possible. Since
102 // needle_len is a compile-time u32 and haystack_len is a runtime
103 // u32, the host-side `saturating_sub` pre-computes the cap value
104 // safely and lets Expr::le do the comparison without Expr::add.
105 let body = vec![
106 Node::let_bind("i", Expr::InvocationId { axis: 0 }),
107 Node::let_bind("haystack_len", Expr::buf_len(haystack)),
108 Node::if_then(
109 Expr::and(
110 Expr::le(Expr::u32(needle_len), Expr::var("haystack_len")),
111 Expr::le(
112 i.clone(),
113 // `haystack_len - needle_len` as a runtime Expr sub. If
114 // the compile-time needle_len exceeds the runtime
115 // haystack_len the first conjunct already short-
116 // circuits, so this sub-expression is evaluated only
117 // on the safe branch (eager vs lazy evaluation is
118 // the job of the optimizer's short-circuit pass -
119 // here the conjunct ordering gives a safe guard).
120 Expr::sub(Expr::var("haystack_len"), Expr::u32(needle_len)),
121 ),
122 ),
123 check_body,
124 ),
125 ];
126 Program::wrapped(
127 vec![
128 counted_storage(haystack, 0, haystack_len),
129 counted_storage(needle, 1, needle_len),
130 output,
131 ],
132 [64, 1, 1],
133 vec![wrap_anonymous(SCAN_SUBSTRING_OP_ID, body)],
134 )
135}
136
137inventory::submit! {
138 vyre_foundation::operation::OperationRegistration {
139 semantic_version: 1,
140 signature: None,
141 tier: vyre_foundation::operation::OperationTier::Library,
142 laws: &[],
143 tolerance: vyre_foundation::operation::TolerancePolicy::EXACT,
144 id: SCAN_SUBSTRING_OP_ID,
145 build: Some(|| substring_search("haystack", "needle", "matches", 8, 3)),
146 test_inputs: Some(|| {
147 let to_u32_vec = |s: &str| s.bytes().map(u32::from).collect::<Vec<_>>();
148 vec![
149 vec![
150 crate::fixture_bytes::u32_bytes(&to_u32_vec("abcabc++")),
151 crate::fixture_bytes::u32_bytes(&to_u32_vec("abc")),
152 ],
153 vec![
154 crate::fixture_bytes::u32_bytes(&to_u32_vec("xyzxyzxy")),
155 crate::fixture_bytes::u32_bytes(&to_u32_vec("xyz")),
156 ]
157 ]
158 }),
159 expected_output: Some(|| {
160 // Case 0: haystack="abcabc++", needle="abc". Matches at
161 // i ∈ {0, 3}. Positions i > haystack_len - needle_len
162 // (5) stay at their zero init because the guard never
163 // fires.
164 // Case 1: haystack="xyzxyzxy", needle="xyz". Matches at
165 // i ∈ {0, 3}.
166 let case0 = crate::fixture_bytes::u32_bytes(&[1u32, 0, 0, 1, 0, 0, 0, 0]);
167 let case1 = crate::fixture_bytes::u32_bytes(&[1u32, 0, 0, 1, 0, 0, 0, 0]);
168 vec![vec![case0], vec![case1]]
169 }),
170 category: Some("scan"),
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn builder_uses_canonical_scan_op_id() {
180 let program = substring_search("haystack", "needle", "matches", 8, 3);
181 let [Node::Region { generator, .. }] = program.entry() else {
182 panic!("expected substring search to emit one scan region");
183 };
184
185 assert_eq!(generator.as_str(), SCAN_SUBSTRING_OP_ID);
186 }
187}