1use std::collections::{BTreeMap, BTreeSet, VecDeque};
21
22pub use uqa_core::rpq::{parse_rpq, RPQParseError, RegularPathExpr};
23
24pub const MAX_RPQ_AST_DEPTH: usize = 256;
28pub const MAX_NFA_STATES: usize = 16_384;
29pub const MAX_DFA_STATES: usize = 16_384;
30
31#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
32pub enum RPQBuildError {
33 #[error("bounded repetition minimum {min} exceeds maximum {max}")]
34 InvalidBound { min: u32, max: u32 },
35 #[error("regular path expression depth {depth} exceeds limit {limit}")]
36 ExpressionTooDeep { depth: usize, limit: usize },
37 #[error("regular path NFA requires {required} states, exceeding limit {limit}")]
38 NfaStateLimitExceeded { required: usize, limit: usize },
39 #[error("regular path DFA exceeded state limit {limit}")]
40 DfaStateLimitExceeded { limit: usize },
41 #[error("invalid NFA: {0}")]
42 InvalidNfa(String),
43 #[error("unable to reserve memory for {states} NFA states")]
44 AllocationFailed { states: usize },
45}
46
47pub fn simplify(expr: &RegularPathExpr) -> Result<RegularPathExpr, RPQBuildError> {
55 required_nfa_states(expr)?;
59 Ok(simplify_validated(expr))
60}
61
62fn simplify_validated(expr: &RegularPathExpr) -> RegularPathExpr {
63 match expr {
64 RegularPathExpr::Label(_) => expr.clone(),
65 RegularPathExpr::Alternation(l, r) => {
66 let mut left = simplify_validated(l);
67 let mut right = simplify_validated(r);
68 if left == right {
69 return left;
70 }
71 if let RegularPathExpr::KleeneStar(inner) = &left {
72 if **inner == right {
73 return left;
74 }
75 }
76 if let RegularPathExpr::KleeneStar(inner) = &right {
77 if **inner == left {
78 return right;
79 }
80 }
81 let lr = format!("{left:?}");
83 let rr = format!("{right:?}");
84 if lr > rr {
85 std::mem::swap(&mut left, &mut right);
86 }
87 RegularPathExpr::alt(left, right)
88 }
89 RegularPathExpr::Concat(l, r) => {
90 let left = simplify_validated(l);
91 let right = simplify_validated(r);
92 if let (RegularPathExpr::KleeneStar(li), RegularPathExpr::KleeneStar(ri)) =
93 (&left, &right)
94 {
95 if li == ri {
96 return left;
97 }
98 }
99 RegularPathExpr::concat(left, right)
100 }
101 RegularPathExpr::KleeneStar(inner) => {
102 let s = simplify_validated(inner);
103 if matches!(s, RegularPathExpr::KleeneStar(_)) {
104 s
105 } else {
106 RegularPathExpr::star(s)
107 }
108 }
109 RegularPathExpr::Bounded { inner, min, max } => {
110 RegularPathExpr::bounded(simplify_validated(inner), *min, *max)
111 }
112 }
113}
114
115pub type StateId = u32;
120
121#[derive(Debug, Clone)]
124pub struct NfaTransition {
125 pub label: Option<String>,
126 pub target: StateId,
127}
128
129#[derive(Debug, Default)]
130pub struct Nfa {
131 pub transitions: Vec<Vec<NfaTransition>>,
134 pub start: StateId,
135 pub accept: StateId,
136}
137
138impl Nfa {
139 fn new() -> Self {
140 Self {
141 transitions: Vec::new(),
142 start: 0,
143 accept: 0,
144 }
145 }
146
147 fn new_state(&mut self) -> Result<StateId, RPQBuildError> {
148 if self.transitions.len() >= MAX_NFA_STATES {
149 return Err(RPQBuildError::NfaStateLimitExceeded {
150 required: self.transitions.len().saturating_add(1),
151 limit: MAX_NFA_STATES,
152 });
153 }
154 let id = StateId::try_from(self.transitions.len()).map_err(|_| {
155 RPQBuildError::NfaStateLimitExceeded {
156 required: self.transitions.len().saturating_add(1),
157 limit: MAX_NFA_STATES,
158 }
159 })?;
160 self.transitions.push(Vec::new());
161 Ok(id)
162 }
163
164 fn add_transition(
165 &mut self,
166 from: StateId,
167 label: Option<String>,
168 to: StateId,
169 ) -> Result<(), RPQBuildError> {
170 if usize::try_from(to)
171 .ok()
172 .is_none_or(|target| target >= self.transitions.len())
173 {
174 return Err(RPQBuildError::InvalidNfa(format!(
175 "transition target {to} is outside {} states",
176 self.transitions.len()
177 )));
178 }
179 let state_count = self.transitions.len();
180 let transitions = self
181 .transitions
182 .get_mut(usize::try_from(from).map_err(|_| {
183 RPQBuildError::InvalidNfa(format!("transition source {from} is not addressable"))
184 })?)
185 .ok_or_else(|| {
186 RPQBuildError::InvalidNfa(format!(
187 "transition source {from} is outside {state_count} states"
188 ))
189 })?;
190 transitions.push(NfaTransition { label, target: to });
191 Ok(())
192 }
193
194 pub fn states(&self) -> Result<Vec<StateId>, RPQBuildError> {
195 validate_nfa(self)?;
196 let end = StateId::try_from(self.transitions.len()).map_err(|_| {
197 RPQBuildError::NfaStateLimitExceeded {
198 required: self.transitions.len(),
199 limit: MAX_NFA_STATES,
200 }
201 })?;
202 Ok((0..end).collect())
203 }
204}
205
206pub fn build_nfa(expr: &RegularPathExpr) -> Result<Nfa, RPQBuildError> {
209 let required = required_nfa_states(expr)?;
210 let mut nfa = Nfa::new();
211 nfa.transitions
212 .try_reserve_exact(required)
213 .map_err(|_| RPQBuildError::AllocationFailed { states: required })?;
214 let (start, accept) = build_fragment(&mut nfa, expr)?;
215 nfa.start = start;
216 nfa.accept = accept;
217 Ok(nfa)
218}
219
220fn required_nfa_states(expr: &RegularPathExpr) -> Result<usize, RPQBuildError> {
221 let mut work = vec![(expr, 1_usize, false)];
222 let mut values = Vec::<usize>::new();
223 while let Some((current, depth, visited)) = work.pop() {
224 if depth > MAX_RPQ_AST_DEPTH {
225 return Err(RPQBuildError::ExpressionTooDeep {
226 depth,
227 limit: MAX_RPQ_AST_DEPTH,
228 });
229 }
230 if !visited {
231 work.push((current, depth, true));
232 match current {
233 RegularPathExpr::Label(_) => {}
234 RegularPathExpr::Concat(left, right)
235 | RegularPathExpr::Alternation(left, right) => {
236 work.push((right, depth.saturating_add(1), false));
237 work.push((left, depth.saturating_add(1), false));
238 }
239 RegularPathExpr::KleeneStar(inner) | RegularPathExpr::Bounded { inner, .. } => {
240 work.push((inner, depth.saturating_add(1), false));
241 }
242 }
243 continue;
244 }
245
246 let required = match current {
247 RegularPathExpr::Label(_) => Some(2),
248 RegularPathExpr::Concat(_, _) => {
249 let right = values.pop().ok_or_else(|| {
250 RPQBuildError::InvalidNfa("missing concat right fragment".into())
251 })?;
252 let left = values.pop().ok_or_else(|| {
253 RPQBuildError::InvalidNfa("missing concat left fragment".into())
254 })?;
255 left.checked_add(right)
256 }
257 RegularPathExpr::Alternation(_, _) => {
258 let right = values.pop().ok_or_else(|| {
259 RPQBuildError::InvalidNfa("missing alternation right fragment".into())
260 })?;
261 let left = values.pop().ok_or_else(|| {
262 RPQBuildError::InvalidNfa("missing alternation left fragment".into())
263 })?;
264 left.checked_add(right).and_then(|sum| sum.checked_add(2))
265 }
266 RegularPathExpr::KleeneStar(_) => values
267 .pop()
268 .ok_or_else(|| RPQBuildError::InvalidNfa("missing Kleene-star fragment".into()))?
269 .checked_add(2),
270 RegularPathExpr::Bounded { min, max, .. } => {
271 if min > max {
272 return Err(RPQBuildError::InvalidBound {
273 min: *min,
274 max: *max,
275 });
276 }
277 let inner = values.pop().ok_or_else(|| {
278 RPQBuildError::InvalidNfa("missing bounded-repeat fragment".into())
279 })?;
280 usize::try_from(*max)
281 .ok()
282 .and_then(|copies| inner.checked_mul(copies))
283 .and_then(|states| states.checked_add(2))
284 }
285 }
286 .ok_or(RPQBuildError::NfaStateLimitExceeded {
287 required: usize::MAX,
288 limit: MAX_NFA_STATES,
289 })?;
290 if required > MAX_NFA_STATES {
291 return Err(RPQBuildError::NfaStateLimitExceeded {
292 required,
293 limit: MAX_NFA_STATES,
294 });
295 }
296 values.push(required);
297 }
298 values
299 .pop()
300 .ok_or_else(|| RPQBuildError::InvalidNfa("regular path expression has no fragment".into()))
301}
302
303fn build_fragment(
304 nfa: &mut Nfa,
305 expr: &RegularPathExpr,
306) -> Result<(StateId, StateId), RPQBuildError> {
307 match expr {
308 RegularPathExpr::Label(name) => {
309 let s = nfa.new_state()?;
310 let a = nfa.new_state()?;
311 nfa.add_transition(s, Some(name.clone()), a)?;
312 Ok((s, a))
313 }
314 RegularPathExpr::Concat(l, r) => {
315 let (ls, la) = build_fragment(nfa, l)?;
316 let (rs, ra) = build_fragment(nfa, r)?;
317 nfa.add_transition(la, None, rs)?;
318 Ok((ls, ra))
319 }
320 RegularPathExpr::Alternation(l, r) => {
321 let s = nfa.new_state()?;
322 let a = nfa.new_state()?;
323 let (ls, la) = build_fragment(nfa, l)?;
324 let (rs, ra) = build_fragment(nfa, r)?;
325 nfa.add_transition(s, None, ls)?;
326 nfa.add_transition(s, None, rs)?;
327 nfa.add_transition(la, None, a)?;
328 nfa.add_transition(ra, None, a)?;
329 Ok((s, a))
330 }
331 RegularPathExpr::KleeneStar(inner) => {
332 let s = nfa.new_state()?;
333 let a = nfa.new_state()?;
334 let (is, ia) = build_fragment(nfa, inner)?;
335 nfa.add_transition(s, None, is)?;
336 nfa.add_transition(s, None, a)?;
337 nfa.add_transition(ia, None, is)?;
338 nfa.add_transition(ia, None, a)?;
339 Ok((s, a))
340 }
341 RegularPathExpr::Bounded { inner, min, max } => {
342 if min > max {
343 return Err(RPQBuildError::InvalidBound {
344 min: *min,
345 max: *max,
346 });
347 }
348 let start = nfa.new_state()?;
349 let mut current_end = start;
350 for _ in 0..*min {
351 let (is, ia) = build_fragment(nfa, inner)?;
352 nfa.add_transition(current_end, None, is)?;
353 current_end = ia;
354 }
355 let accept = nfa.new_state()?;
356 if min == max {
357 nfa.add_transition(current_end, None, accept)?;
358 } else {
359 nfa.add_transition(current_end, None, accept)?;
360 for _ in 0..(*max - *min) {
361 let (is, ia) = build_fragment(nfa, inner)?;
362 nfa.add_transition(current_end, None, is)?;
363 nfa.add_transition(ia, None, accept)?;
364 current_end = ia;
365 }
366 }
367 Ok((start, accept))
368 }
369 }
370}
371
372pub fn epsilon_closure(
375 nfa: &Nfa,
376 states: &BTreeSet<StateId>,
377) -> Result<BTreeSet<StateId>, RPQBuildError> {
378 validate_nfa(nfa)?;
379 let mut closure = states.clone();
380 let mut stack: Vec<StateId> = states.iter().copied().collect();
381 while let Some(s) = stack.pop() {
382 let outgoing = nfa
383 .transitions
384 .get(usize::try_from(s).map_err(|_| {
385 RPQBuildError::InvalidNfa(format!("closure state {s} is not addressable"))
386 })?)
387 .ok_or_else(|| {
388 RPQBuildError::InvalidNfa(format!("closure state {s} is outside the NFA"))
389 })?;
390 for t in outgoing {
391 if t.label.is_none() && !closure.contains(&t.target) {
392 closure.insert(t.target);
393 stack.push(t.target);
394 }
395 }
396 }
397 Ok(closure)
398}
399
400fn validate_nfa(nfa: &Nfa) -> Result<(), RPQBuildError> {
401 let state_count = nfa.transitions.len();
402 if state_count == 0 {
403 return Err(RPQBuildError::InvalidNfa("NFA has no states".into()));
404 }
405 if state_count > MAX_NFA_STATES {
406 return Err(RPQBuildError::NfaStateLimitExceeded {
407 required: state_count,
408 limit: MAX_NFA_STATES,
409 });
410 }
411 for (name, state) in [("start", nfa.start), ("accept", nfa.accept)] {
412 if usize::try_from(state)
413 .ok()
414 .is_none_or(|index| index >= state_count)
415 {
416 return Err(RPQBuildError::InvalidNfa(format!(
417 "{name} state {state} is outside {state_count} states"
418 )));
419 }
420 }
421 for (source, transitions) in nfa.transitions.iter().enumerate() {
422 for transition in transitions {
423 if usize::try_from(transition.target)
424 .ok()
425 .is_none_or(|target| target >= state_count)
426 {
427 return Err(RPQBuildError::InvalidNfa(format!(
428 "transition from state {source} targets missing state {}",
429 transition.target
430 )));
431 }
432 }
433 }
434 Ok(())
435}
436
437pub type DfaState = BTreeSet<StateId>;
442
443#[derive(Debug)]
444pub struct Dfa {
445 pub start: DfaState,
446 pub accepts: BTreeSet<DfaState>,
447 pub transitions: BTreeMap<DfaState, BTreeMap<String, DfaState>>,
448}
449
450pub fn subset_construction(nfa: &Nfa) -> Result<Dfa, RPQBuildError> {
452 validate_nfa(nfa)?;
453 let mut alphabet: BTreeSet<String> = BTreeSet::new();
455 for transitions in &nfa.transitions {
456 for t in transitions {
457 if let Some(label) = &t.label {
458 alphabet.insert(label.clone());
459 }
460 }
461 }
462
463 let initial = epsilon_closure(nfa, &BTreeSet::from([nfa.start]))?;
464 let mut transitions: BTreeMap<DfaState, BTreeMap<String, DfaState>> = BTreeMap::new();
465 let mut accepts: BTreeSet<DfaState> = BTreeSet::new();
466 let mut seen: BTreeSet<DfaState> = BTreeSet::from([initial.clone()]);
467 let mut work: VecDeque<DfaState> = VecDeque::from([initial.clone()]);
468
469 if initial.contains(&nfa.accept) {
470 accepts.insert(initial.clone());
471 }
472
473 while let Some(current) = work.pop_front() {
474 let mut step: BTreeMap<String, DfaState> = BTreeMap::new();
475 for label in &alphabet {
476 let mut next_nfa: BTreeSet<StateId> = BTreeSet::new();
477 for sid in ¤t {
478 let outgoing = nfa
479 .transitions
480 .get(usize::try_from(*sid).map_err(|_| {
481 RPQBuildError::InvalidNfa(format!("DFA state {sid} is not addressable"))
482 })?)
483 .ok_or_else(|| {
484 RPQBuildError::InvalidNfa(format!("DFA state {sid} is outside the NFA"))
485 })?;
486 for t in outgoing {
487 if t.label.as_deref() == Some(label.as_str()) {
488 next_nfa.insert(t.target);
489 }
490 }
491 }
492 if next_nfa.is_empty() {
493 continue;
494 }
495 let closed = epsilon_closure(nfa, &next_nfa)?;
496 step.insert(label.clone(), closed.clone());
497 if !seen.contains(&closed) {
498 if seen.len() >= MAX_DFA_STATES {
499 return Err(RPQBuildError::DfaStateLimitExceeded {
500 limit: MAX_DFA_STATES,
501 });
502 }
503 seen.insert(closed.clone());
504 work.push_back(closed.clone());
505 if closed.contains(&nfa.accept) {
506 accepts.insert(closed);
507 }
508 }
509 }
510 transitions.insert(current, step);
511 }
512
513 Ok(Dfa {
514 start: initial,
515 accepts,
516 transitions,
517 })
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523
524 #[test]
525 fn build_rejects_unbounded_state_allocation_before_expansion() {
526 let expr = RegularPathExpr::bounded(RegularPathExpr::label("a"), 0, u32::MAX);
527 assert!(matches!(
528 build_nfa(&expr),
529 Err(RPQBuildError::NfaStateLimitExceeded { .. })
530 ));
531 }
532
533 #[test]
534 fn build_rejects_programmatically_reversed_bound() {
535 let expr = RegularPathExpr::bounded(RegularPathExpr::label("a"), 5, 2);
536 assert_eq!(
537 build_nfa(&expr).unwrap_err(),
538 RPQBuildError::InvalidBound { min: 5, max: 2 }
539 );
540 }
541
542 #[test]
543 fn subset_construction_rejects_missing_transition_target() {
544 let malformed = Nfa {
545 transitions: vec![vec![NfaTransition {
546 label: None,
547 target: 1,
548 }]],
549 start: 0,
550 accept: 0,
551 };
552 assert!(matches!(
553 subset_construction(&malformed),
554 Err(RPQBuildError::InvalidNfa(message)) if message.contains("missing state")
555 ));
556 }
557
558 #[test]
559 fn simplify_idempotent_alternation() {
560 let e = RegularPathExpr::alt(RegularPathExpr::label("a"), RegularPathExpr::label("a"));
561 assert_eq!(simplify(&e).unwrap(), RegularPathExpr::label("a"));
562 }
563
564 #[test]
565 fn simplify_nested_kleene() {
566 let e = RegularPathExpr::star(RegularPathExpr::star(RegularPathExpr::label("a")));
567 assert_eq!(
568 simplify(&e).unwrap(),
569 RegularPathExpr::star(RegularPathExpr::label("a"))
570 );
571 }
572
573 #[test]
574 fn simplify_star_subsumes_label() {
575 let e = RegularPathExpr::alt(
576 RegularPathExpr::star(RegularPathExpr::label("a")),
577 RegularPathExpr::label("a"),
578 );
579 assert_eq!(
580 simplify(&e).unwrap(),
581 RegularPathExpr::star(RegularPathExpr::label("a"))
582 );
583 }
584
585 #[test]
586 fn nfa_label_two_states() {
587 let nfa = build_nfa(&RegularPathExpr::label("a")).unwrap();
588 assert_eq!(nfa.transitions.len(), 2);
589 assert_ne!(nfa.start, nfa.accept);
590 }
591
592 #[test]
593 fn dfa_recognizes_a_or_b() {
594 let nfa = build_nfa(&RegularPathExpr::alt(
595 RegularPathExpr::label("a"),
596 RegularPathExpr::label("b"),
597 ))
598 .unwrap();
599 let dfa = subset_construction(&nfa).unwrap();
600 let after_a = dfa
602 .transitions
603 .get(&dfa.start)
604 .and_then(|m| m.get("a"))
605 .expect("no `a` transition");
606 assert!(dfa.accepts.contains(after_a));
607 let after_b = dfa
608 .transitions
609 .get(&dfa.start)
610 .and_then(|m| m.get("b"))
611 .expect("no `b` transition");
612 assert!(dfa.accepts.contains(after_b));
613 assert!(dfa
615 .transitions
616 .get(&dfa.start)
617 .and_then(|m| m.get("c"))
618 .is_none());
619 }
620
621 #[test]
622 fn dfa_recognizes_kleene_star() {
623 let nfa = build_nfa(&RegularPathExpr::star(RegularPathExpr::label("a"))).unwrap();
624 let dfa = subset_construction(&nfa).unwrap();
625 assert!(dfa.accepts.contains(&dfa.start));
627 }
628}