1use crate::{Anchor, Automaton, CaptureId, Instruction, StateId, TagBoundary, TextLimits};
4use std::collections::{BTreeMap, BTreeSet};
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub struct CaptureSpan {
9 pub start: usize,
11 pub end: usize,
13}
14
15#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct ExecutionMatch {
18 pub start: usize,
20 pub end: usize,
22 pub captures: BTreeMap<CaptureId, CaptureSpan>,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum ExecutionLimit {
29 States,
31 Transitions,
33 CaptureHistory,
35 Subject,
37}
38
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
41pub struct ExecutionReceipt {
42 pub state_count: usize,
44 pub state_visits: usize,
46 pub transitions: usize,
48 pub capture_history: usize,
50 pub subject_symbols: usize,
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum UnsupportedFeature {
57 VariableWidthAssertion(crate::AssertionId),
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
63pub enum ExecutionOutcome {
64 Match {
66 matched: ExecutionMatch,
68 receipt: ExecutionReceipt,
70 },
71 NoMatch {
73 receipt: ExecutionReceipt,
75 },
76 Limit {
78 limit: ExecutionLimit,
80 receipt: ExecutionReceipt,
82 },
83 Unsupported {
85 feature: UnsupportedFeature,
87 receipt: ExecutionReceipt,
89 },
90}
91
92#[derive(Clone, Debug, Default)]
93struct History {
94 open: BTreeMap<CaptureId, usize>,
95 closed: BTreeMap<CaptureId, CaptureSpan>,
96}
97
98#[derive(Clone, Debug)]
99struct Thread {
100 state: StateId,
101 repeats: BTreeMap<StateId, usize>,
102 history: History,
103}
104
105type SpanMatcher<'a, S, E> = dyn Fn(&E, &[S], usize) -> Option<usize> + 'a;
106
107pub fn execute_regular<S, E>(
113 automaton: &Automaton<S, E>,
114 subject: &[S],
115 limits: TextLimits,
116 extension_matches: impl Fn(&E, &S) -> bool,
117) -> ExecutionOutcome
118where
119 S: PartialEq,
120{
121 execute_spanning(
122 automaton,
123 subject,
124 limits,
125 |extension, subject, position| {
126 subject
127 .get(position)
128 .filter(|symbol| extension_matches(extension, symbol))
129 .map(|_| position + 1)
130 },
131 )
132}
133
134pub(crate) fn execute_spanning<S, E>(
141 automaton: &Automaton<S, E>,
142 subject: &[S],
143 limits: TextLimits,
144 extension_match: impl Fn(&E, &[S], usize) -> Option<usize>,
145) -> ExecutionOutcome
146where
147 S: PartialEq,
148{
149 execute_regular_inner(automaton, subject, limits, &extension_match)
150}
151
152fn execute_regular_inner<S, E>(
153 automaton: &Automaton<S, E>,
154 subject: &[S],
155 limits: TextLimits,
156 extension_match: &SpanMatcher<'_, S, E>,
157) -> ExecutionOutcome
158where
159 S: PartialEq,
160{
161 let mut receipt = ExecutionReceipt {
162 state_count: automaton.evidence().state_count,
163 subject_symbols: subject.len(),
164 ..ExecutionReceipt::default()
165 };
166 if receipt.state_count > limits.max_states {
167 return limited(ExecutionLimit::States, receipt);
168 }
169 if receipt.subject_symbols > limits.max_subject_symbols {
170 return limited(ExecutionLimit::Subject, receipt);
171 }
172
173 let mut current = vec![(
174 0,
175 Thread {
176 state: automaton.start(),
177 repeats: BTreeMap::new(),
178 history: History::default(),
179 },
180 )];
181 let mut seen = BTreeSet::new();
182 while let Some((position, thread)) = current.pop() {
183 receipt.state_visits += 1;
184 if !seen.insert((position, thread.state)) {
187 continue;
188 }
189 let Some(state) = automaton.states().get(thread.state.0 as usize) else {
190 continue;
191 };
192 match &state.instruction {
193 Instruction::Accept => {
194 return ExecutionOutcome::Match {
195 matched: ExecutionMatch {
196 start: 0,
197 end: position,
198 captures: thread.history.closed,
199 },
200 receipt,
201 };
202 }
203 Instruction::Symbol { symbol, next } => {
204 if subject.get(position).is_some_and(|found| found == symbol) {
205 push_at(&mut current, position + 1, thread, *next);
206 }
207 }
208 Instruction::Any { next } => {
209 if position < subject.len() {
210 push_at(&mut current, position + 1, thread, *next);
211 }
212 }
213 Instruction::Extension { extension, next } => {
214 if let Some(end) = extension_match(extension, subject, position)
215 && (position..=subject.len()).contains(&end)
216 {
217 push_at(&mut current, end, thread, *next);
218 }
219 }
220 Instruction::Epsilon { next } => push_at(&mut current, position, thread, *next),
221 Instruction::Split { alternatives } => {
222 for next in alternatives.iter().rev() {
223 push_at(&mut current, position, thread.clone(), *next);
224 }
225 }
226 Instruction::Tag {
227 capture,
228 boundary,
229 next,
230 } => {
231 if receipt.capture_history == limits.max_capture_history {
232 return limited(ExecutionLimit::CaptureHistory, receipt);
233 }
234 receipt.capture_history += 1;
235 let mut thread = thread;
236 match boundary {
237 TagBoundary::Start => {
238 thread.history.open.insert(*capture, position);
239 }
240 TagBoundary::End => {
241 if let Some(start) = thread.history.open.remove(capture) {
242 thread.history.closed.insert(
243 *capture,
244 CaptureSpan {
245 start,
246 end: position,
247 },
248 );
249 }
250 }
251 }
252 push_at(&mut current, position, thread, *next);
253 }
254 Instruction::Anchor { anchor, next } => {
255 let holds = match anchor {
256 Anchor::SubjectStart => position == 0,
257 Anchor::SubjectEnd => position == subject.len(),
258 };
259 if holds {
260 push_at(&mut current, position, thread, *next);
261 }
262 }
263 Instruction::Repeat {
264 body,
265 exit,
266 min,
267 max,
268 greedy,
269 } => {
270 let count = thread.repeats.get(&thread.state).copied().unwrap_or(0);
271 let can_repeat = max.is_none_or(|maximum| count < maximum);
272 let can_exit = count >= *min;
273 let mut body_thread = thread.clone();
274 body_thread.repeats.insert(thread.state, count + 1);
275 let choices = if *greedy {
276 [(can_exit, *exit, thread), (can_repeat, *body, body_thread)]
277 } else {
278 [(can_repeat, *body, body_thread), (can_exit, *exit, thread)]
279 };
280 for (enabled, next, thread) in choices {
281 if enabled {
282 push_at(&mut current, position, thread, next);
283 }
284 }
285 }
286 Instruction::Assertion { assertion, next } => {
287 let Some(program) = automaton.assertion(*assertion) else {
288 return ExecutionOutcome::Unsupported {
289 feature: UnsupportedFeature::VariableWidthAssertion(*assertion),
290 receipt,
291 };
292 };
293 let end = position.saturating_add(program.width());
294 if let Some(window) = subject.get(position..end) {
295 let remaining = TextLimits {
296 max_steps: limits.max_steps.saturating_sub(receipt.transitions),
297 max_states: limits.max_states,
298 max_capture_history: limits
299 .max_capture_history
300 .saturating_sub(receipt.capture_history),
301 max_subject_symbols: limits.max_subject_symbols,
302 };
303 match execute_regular_inner(
304 program.automaton(),
305 window,
306 remaining,
307 extension_match,
308 ) {
309 ExecutionOutcome::Match {
310 matched,
311 receipt: nested,
312 } if matched.end == window.len() => {
313 receipt.state_visits += nested.state_visits;
314 receipt.transitions += nested.transitions;
315 receipt.capture_history += nested.capture_history;
316 push_at(&mut current, position, thread, *next);
317 }
318 ExecutionOutcome::Limit {
319 limit,
320 receipt: nested,
321 } => {
322 receipt.state_visits += nested.state_visits;
323 receipt.transitions += nested.transitions;
324 receipt.capture_history += nested.capture_history;
325 return limited(limit, receipt);
326 }
327 _ => {}
328 }
329 }
330 }
331 }
332 receipt.transitions += 1;
333 if receipt.transitions >= limits.max_steps {
334 return limited(ExecutionLimit::Transitions, receipt);
335 }
336 }
337 ExecutionOutcome::NoMatch { receipt }
338}
339
340fn push_at(stack: &mut Vec<(usize, Thread)>, position: usize, mut thread: Thread, state: StateId) {
341 thread.state = state;
342 stack.push((position, thread));
343}
344
345fn limited(limit: ExecutionLimit, receipt: ExecutionReceipt) -> ExecutionOutcome {
346 ExecutionOutcome::Limit { limit, receipt }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use crate::{ByteDomain, EnginePolicy, IrNode, PatternIr, RepeatBounds, compile};
353
354 fn run(root: IrNode<u8, ()>, subject: &[u8], limits: TextLimits) -> ExecutionOutcome {
355 let ir = PatternIr::<ByteDomain, ()>::new(root, BTreeMap::new(), &EnginePolicy::new([]))
356 .unwrap();
357 execute_regular(&compile(&ir), subject, limits, |_, _| false)
358 }
359
360 #[test]
361 fn nested_ambiguous_repetition_has_linear_accounted_work() {
362 let repeated_a = IrNode::Repeat {
363 node: Box::new(IrNode::Alternation(vec![
364 IrNode::Symbol(b'a'),
365 IrNode::Concat(vec![IrNode::Symbol(b'a')]),
366 ])),
367 bounds: RepeatBounds::new(0, None).unwrap(),
368 greedy: true,
369 };
370 let pattern = IrNode::Concat(vec![repeated_a, IrNode::Symbol(b'b')]);
371 for length in [32, 128, 512] {
372 let outcome = run(pattern.clone(), &vec![b'a'; length], TextLimits::default());
373 let ExecutionOutcome::NoMatch { receipt } = outcome else {
374 panic!("adversarial rejection must complete normally: {outcome:?}");
375 };
376 assert!(receipt.state_visits <= (length + 1) * receipt.state_count * 2);
377 }
378 }
379
380 #[test]
381 fn long_rejection_terminates_and_limits_are_typed() {
382 let pattern = IrNode::Concat(vec![IrNode::Any, IrNode::Symbol(b'z')]);
383 let subject = vec![b'a'; 10_000];
384 assert!(matches!(
385 run(pattern.clone(), &subject, TextLimits::default()),
386 ExecutionOutcome::NoMatch { .. }
387 ));
388 let limits = TextLimits {
389 max_steps: 1,
390 ..TextLimits::default()
391 };
392 assert!(matches!(
393 run(pattern, b"az", limits),
394 ExecutionOutcome::Limit {
395 limit: ExecutionLimit::Transitions,
396 ..
397 }
398 ));
399 }
400
401 #[test]
402 fn fixed_width_assertion_runs_without_consuming_subject() {
403 let assertion = crate::AssertionId(7);
404 let ir = PatternIr::<ByteDomain, ()>::new(
405 IrNode::Concat(vec![IrNode::Assertion(assertion), IrNode::Symbol(b'a')]),
406 BTreeMap::from([(assertion, IrNode::Symbol(b'a'))]),
407 &EnginePolicy::new([]),
408 )
409 .unwrap();
410 let outcome = execute_regular(&compile(&ir), b"a", TextLimits::default(), |_, _| false);
411 assert!(matches!(
412 outcome,
413 ExecutionOutcome::Match {
414 matched: ExecutionMatch { end: 1, .. },
415 ..
416 }
417 ));
418 }
419
420 #[test]
421 fn regular_pattern_keeps_the_pre_extension_receipt() {
422 let outcome = run(IrNode::Symbol(b'a'), b"a", TextLimits::default());
423 assert_eq!(
424 outcome,
425 ExecutionOutcome::Match {
426 matched: ExecutionMatch {
427 start: 0,
428 end: 1,
429 captures: BTreeMap::new(),
430 },
431 receipt: ExecutionReceipt {
432 state_count: 2,
433 state_visits: 2,
434 transitions: 1,
435 capture_history: 0,
436 subject_symbols: 1,
437 },
438 }
439 );
440 }
441
442 #[test]
443 fn variable_width_assertion_is_a_typed_refusal() {
444 let assertion = crate::AssertionId(9);
445 let ir = PatternIr::<ByteDomain, ()>::new(
446 IrNode::Assertion(assertion),
447 BTreeMap::from([(
448 assertion,
449 IrNode::Repeat {
450 node: Box::new(IrNode::Symbol(b'a')),
451 bounds: RepeatBounds::new(0, None).unwrap(),
452 greedy: true,
453 },
454 )]),
455 &EnginePolicy::new([]),
456 )
457 .unwrap();
458 assert!(matches!(
459 execute_regular(&compile(&ir), b"aaa", TextLimits::default(), |_, _| false),
460 ExecutionOutcome::Unsupported {
461 feature: UnsupportedFeature::VariableWidthAssertion(found),
462 ..
463 } if found == assertion
464 ));
465 }
466}