vyre_libs/scan/substring/
substring.rs1use vyre::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
10
11use crate::region::wrap_anonymous;
12
13pub const SCAN_SUBSTRING_OP_ID: &str = "vyre-libs::scan::substring_search";
15pub(crate) const LEGACY_MATCHING_SUBSTRING_OP_ID: &str = "vyre-libs::matching::substring_search";
17
18#[must_use]
23pub fn substring_search(
24 haystack: &str,
25 needle: &str,
26 matches: &str,
27 haystack_len: u32,
28 needle_len: u32,
29) -> Program {
30 substring_search_with_op_id(
31 SCAN_SUBSTRING_OP_ID,
32 haystack,
33 needle,
34 matches,
35 haystack_len,
36 needle_len,
37 )
38}
39
40#[must_use]
42pub(crate) fn substring_search_with_op_id(
43 op_id: &str,
44 haystack: &str,
45 needle: &str,
46 matches: &str,
47 haystack_len: u32,
48 needle_len: u32,
49) -> Program {
50 let counted_storage = |name: &str, binding, count| {
51 let decl = BufferDecl::storage(name, binding, BufferAccess::ReadOnly, DataType::U32);
52 if count == 0 {
53 decl
54 } else {
55 decl.with_count(count)
56 }
57 };
58 let output_count = haystack_len.max(1);
59 let visible_output_bytes = (haystack_len as usize).saturating_mul(4);
60 let output = BufferDecl::output(matches, 2, DataType::U32)
61 .with_count(output_count)
62 .with_output_byte_range(0..visible_output_bytes);
63
64 let i = Expr::var("i");
65 let mut check_body: Vec<Node> = vec![Node::let_bind("ok", Expr::u32(1))];
68 check_body.push(Node::loop_for(
71 "k",
72 Expr::u32(0),
73 Expr::u32(needle_len),
74 vec![Node::assign(
75 "ok",
76 Expr::bitand(
77 Expr::var("ok"),
78 Expr::select(
81 Expr::eq(
82 Expr::load(haystack, Expr::add(i.clone(), Expr::var("k"))),
83 Expr::load(needle, Expr::var("k")),
84 ),
85 Expr::u32(1),
86 Expr::u32(0),
87 ),
88 ),
89 )],
90 ));
91 check_body.push(Node::Store {
92 buffer: matches.into(),
93 index: i.clone(),
94 value: Expr::var("ok"),
95 });
96
97 let body = vec![
118 Node::let_bind("i", Expr::InvocationId { axis: 0 }),
119 Node::let_bind("haystack_len", Expr::buf_len(haystack)),
120 Node::if_then(
121 Expr::and(
122 Expr::le(Expr::u32(needle_len), Expr::var("haystack_len")),
123 Expr::le(
124 i.clone(),
125 Expr::sub(Expr::var("haystack_len"), Expr::u32(needle_len)),
133 ),
134 ),
135 check_body,
136 ),
137 ];
138 Program::wrapped(
139 vec![
140 counted_storage(haystack, 0, haystack_len),
141 counted_storage(needle, 1, needle_len),
142 output,
143 ],
144 [64, 1, 1],
145 vec![wrap_anonymous(op_id, body)],
146 )
147}
148
149inventory::submit! {
150 crate::harness::OpEntry {
151 id: SCAN_SUBSTRING_OP_ID,
152 build: || substring_search("haystack", "needle", "matches", 8, 3),
153 test_inputs: Some(|| {
154 let to_u32_vec = |s: &str| s.bytes().map(u32::from).collect::<Vec<_>>();
155 vec![
156 vec![
157 crate::test_support::byte_pack::u32_bytes(&to_u32_vec("abcabc++")),
158 crate::test_support::byte_pack::u32_bytes(&to_u32_vec("abc")),
159 ],
160 vec![
161 crate::test_support::byte_pack::u32_bytes(&to_u32_vec("xyzxyzxy")),
162 crate::test_support::byte_pack::u32_bytes(&to_u32_vec("xyz")),
163 ]
164 ]
165 }),
166 expected_output: Some(|| {
167 let case0 = crate::test_support::byte_pack::u32_bytes(&[1u32, 0, 0, 1, 0, 0, 0, 0]);
174 let case1 = crate::test_support::byte_pack::u32_bytes(&[1u32, 0, 0, 1, 0, 0, 0, 0]);
175 vec![vec![case0], vec![case1]]
176 }),
177 category: Some("scan"),
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn canonical_scan_builder_uses_scan_op_id_not_matching_id() {
187 let program = substring_search("haystack", "needle", "matches", 8, 3);
188 let [Node::Region { generator, .. }] = program.entry() else {
189 panic!("expected substring search to emit one scan region");
190 };
191
192 assert_eq!(generator.as_str(), SCAN_SUBSTRING_OP_ID);
193 assert_ne!(generator.as_str(), LEGACY_MATCHING_SUBSTRING_OP_ID);
194 }
195
196 #[test]
197 fn explicit_compatibility_builder_preserves_legacy_op_id() {
198 let program = substring_search_with_op_id(
199 LEGACY_MATCHING_SUBSTRING_OP_ID,
200 "haystack",
201 "needle",
202 "matches",
203 8,
204 3,
205 );
206 let [Node::Region { generator, .. }] = program.entry() else {
207 panic!("expected substring compatibility search to emit one region");
208 };
209
210 assert_eq!(generator.as_str(), LEGACY_MATCHING_SUBSTRING_OP_ID);
211 }
212
213 #[test]
214 fn source_boundary_keeps_matching_identity_out_of_canonical_builder() {
215 let source = include_str!("substring.rs");
216 let canonical_builder = source
217 .split("pub fn substring_search(")
218 .nth(1)
219 .expect("Fix: canonical substring builder must exist")
220 .split("/// Build a substring Program with an explicit compatibility op id.")
221 .next()
222 .expect("Fix: compatibility builder must follow canonical substring builder");
223
224 assert!(canonical_builder.contains("SCAN_SUBSTRING_OP_ID"));
225 assert!(!canonical_builder.contains("LEGACY_MATCHING_SUBSTRING_OP_ID"));
226 assert!(!canonical_builder.contains("vyre-libs::matching::substring_search"));
227 }
228}