1use std::collections::{BTreeMap, BTreeSet, VecDeque};
21
22pub const MAX_RPQ_AST_DEPTH: usize = 256;
26pub const MAX_NFA_STATES: usize = 16_384;
27pub const MAX_DFA_STATES: usize = 16_384;
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub enum RegularPathExpr {
32 Label(String),
34 Concat(Box<RegularPathExpr>, Box<RegularPathExpr>),
36 Alternation(Box<RegularPathExpr>, Box<RegularPathExpr>),
38 KleeneStar(Box<RegularPathExpr>),
40 Bounded {
42 inner: Box<RegularPathExpr>,
43 min: u32,
44 max: u32,
45 },
46}
47
48impl RegularPathExpr {
49 pub fn label(name: impl Into<String>) -> Self {
50 Self::Label(name.into())
51 }
52 pub fn concat(left: Self, right: Self) -> Self {
53 Self::Concat(Box::new(left), Box::new(right))
54 }
55 pub fn alt(left: Self, right: Self) -> Self {
56 Self::Alternation(Box::new(left), Box::new(right))
57 }
58 pub fn star(inner: Self) -> Self {
59 Self::KleeneStar(Box::new(inner))
60 }
61 pub fn bounded(inner: Self, min: u32, max: u32) -> Self {
62 Self::Bounded {
63 inner: Box::new(inner),
64 min,
65 max,
66 }
67 }
68}
69
70#[derive(Debug, thiserror::Error, PartialEq, Eq)]
75pub enum RPQParseError {
76 #[error("unexpected token at position {position}: {token:?}")]
77 Unexpected { position: usize, token: String },
78 #[error("unexpected end of expression")]
79 Eof,
80 #[error("missing closing parenthesis")]
81 MissingParen,
82 #[error("malformed bounded repetition: {0}")]
83 MalformedBound(String),
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
87pub enum RPQBuildError {
88 #[error("bounded repetition minimum {min} exceeds maximum {max}")]
89 InvalidBound { min: u32, max: u32 },
90 #[error("regular path expression depth {depth} exceeds limit {limit}")]
91 ExpressionTooDeep { depth: usize, limit: usize },
92 #[error("regular path NFA requires {required} states, exceeding limit {limit}")]
93 NfaStateLimitExceeded { required: usize, limit: usize },
94 #[error("regular path DFA exceeded state limit {limit}")]
95 DfaStateLimitExceeded { limit: usize },
96 #[error("invalid NFA: {0}")]
97 InvalidNfa(String),
98 #[error("unable to reserve memory for {states} NFA states")]
99 AllocationFailed { states: usize },
100}
101
102pub fn parse_rpq(expr: &str) -> Result<RegularPathExpr, RPQParseError> {
103 let tokens = tokenize(expr);
104 let (result, pos) = parse_alternation(&tokens, 0)?;
105 if pos != tokens.len() {
106 return Err(RPQParseError::Unexpected {
107 position: pos,
108 token: tokens[pos].clone(),
109 });
110 }
111 Ok(result)
112}
113
114fn tokenize(expr: &str) -> Vec<String> {
115 let mut tokens = Vec::new();
116 let bytes = expr.as_bytes();
117 let mut i = 0;
118 while i < bytes.len() {
119 let ch = bytes[i] as char;
120 if ch.is_ascii_whitespace() {
121 i += 1;
122 continue;
123 }
124 if matches!(ch, '(' | ')' | '/' | '|' | '*' | '{' | '}' | ',') {
125 tokens.push(ch.to_string());
126 i += 1;
127 } else {
128 let start = i;
129 while i < bytes.len() {
130 let c = bytes[i] as char;
131 if c.is_ascii_whitespace()
132 || matches!(c, '(' | ')' | '/' | '|' | '*' | '{' | '}' | ',')
133 {
134 break;
135 }
136 i += 1;
137 }
138 tokens.push(expr[start..i].to_string());
139 }
140 }
141 tokens
142}
143
144fn parse_alternation(
145 tokens: &[String],
146 mut pos: usize,
147) -> Result<(RegularPathExpr, usize), RPQParseError> {
148 let (mut left, p) = parse_concat(tokens, pos)?;
149 pos = p;
150 while pos < tokens.len() && tokens[pos] == "|" {
151 pos += 1;
152 let (right, p) = parse_concat(tokens, pos)?;
153 pos = p;
154 left = RegularPathExpr::alt(left, right);
155 }
156 Ok((left, pos))
157}
158
159fn parse_concat(
160 tokens: &[String],
161 mut pos: usize,
162) -> Result<(RegularPathExpr, usize), RPQParseError> {
163 let (mut left, p) = parse_star(tokens, pos)?;
164 pos = p;
165 while pos < tokens.len() && tokens[pos] == "/" {
166 pos += 1;
167 let (right, p) = parse_star(tokens, pos)?;
168 pos = p;
169 left = RegularPathExpr::concat(left, right);
170 }
171 Ok((left, pos))
172}
173
174fn parse_star(
175 tokens: &[String],
176 mut pos: usize,
177) -> Result<(RegularPathExpr, usize), RPQParseError> {
178 let (mut expr, p) = parse_atom(tokens, pos)?;
179 pos = p;
180 while pos < tokens.len() && (tokens[pos] == "*" || tokens[pos] == "{") {
181 if tokens[pos] == "*" {
182 pos += 1;
183 expr = RegularPathExpr::star(expr);
184 } else {
185 pos += 1;
186 let min = tokens
187 .get(pos)
188 .ok_or_else(|| RPQParseError::MalformedBound("missing min".into()))?
189 .parse::<u32>()
190 .map_err(|e| RPQParseError::MalformedBound(format!("min: {e}")))?;
191 pos += 1;
192 if tokens.get(pos).map(String::as_str) != Some(",") {
193 return Err(RPQParseError::MalformedBound("expected ','".into()));
194 }
195 pos += 1;
196 let max = tokens
197 .get(pos)
198 .ok_or_else(|| RPQParseError::MalformedBound("missing max".into()))?
199 .parse::<u32>()
200 .map_err(|e| RPQParseError::MalformedBound(format!("max: {e}")))?;
201 if min > max {
202 return Err(RPQParseError::MalformedBound(format!(
203 "min {min} exceeds max {max}"
204 )));
205 }
206 pos += 1;
207 if tokens.get(pos).map(String::as_str) != Some("}") {
208 return Err(RPQParseError::MalformedBound("expected '}'".into()));
209 }
210 pos += 1;
211 expr = RegularPathExpr::bounded(expr, min, max);
212 }
213 }
214 Ok((expr, pos))
215}
216
217fn parse_atom(
218 tokens: &[String],
219 mut pos: usize,
220) -> Result<(RegularPathExpr, usize), RPQParseError> {
221 let token = tokens.get(pos).ok_or(RPQParseError::Eof)?;
222 if token == "(" {
223 pos += 1;
224 let (inner, p) = parse_alternation(tokens, pos)?;
225 pos = p;
226 if tokens.get(pos).map(String::as_str) != Some(")") {
227 return Err(RPQParseError::MissingParen);
228 }
229 pos += 1;
230 Ok((inner, pos))
231 } else if matches!(token.as_str(), ")" | "/" | "|" | "*" | "{" | "}" | ",") {
232 Err(RPQParseError::Unexpected {
233 position: pos,
234 token: token.clone(),
235 })
236 } else {
237 pos += 1;
238 Ok((RegularPathExpr::label(token.clone()), pos))
239 }
240}
241
242pub fn simplify(expr: &RegularPathExpr) -> Result<RegularPathExpr, RPQBuildError> {
250 required_nfa_states(expr)?;
254 Ok(simplify_validated(expr))
255}
256
257fn simplify_validated(expr: &RegularPathExpr) -> RegularPathExpr {
258 match expr {
259 RegularPathExpr::Label(_) => expr.clone(),
260 RegularPathExpr::Alternation(l, r) => {
261 let mut left = simplify_validated(l);
262 let mut right = simplify_validated(r);
263 if left == right {
264 return left;
265 }
266 if let RegularPathExpr::KleeneStar(inner) = &left {
267 if **inner == right {
268 return left;
269 }
270 }
271 if let RegularPathExpr::KleeneStar(inner) = &right {
272 if **inner == left {
273 return right;
274 }
275 }
276 let lr = format!("{left:?}");
278 let rr = format!("{right:?}");
279 if lr > rr {
280 std::mem::swap(&mut left, &mut right);
281 }
282 RegularPathExpr::alt(left, right)
283 }
284 RegularPathExpr::Concat(l, r) => {
285 let left = simplify_validated(l);
286 let right = simplify_validated(r);
287 if let (RegularPathExpr::KleeneStar(li), RegularPathExpr::KleeneStar(ri)) =
288 (&left, &right)
289 {
290 if li == ri {
291 return left;
292 }
293 }
294 RegularPathExpr::concat(left, right)
295 }
296 RegularPathExpr::KleeneStar(inner) => {
297 let s = simplify_validated(inner);
298 if matches!(s, RegularPathExpr::KleeneStar(_)) {
299 s
300 } else {
301 RegularPathExpr::star(s)
302 }
303 }
304 RegularPathExpr::Bounded { inner, min, max } => {
305 RegularPathExpr::bounded(simplify_validated(inner), *min, *max)
306 }
307 }
308}
309
310pub type StateId = u32;
315
316#[derive(Debug, Clone)]
319pub struct NfaTransition {
320 pub label: Option<String>,
321 pub target: StateId,
322}
323
324#[derive(Debug, Default)]
325pub struct Nfa {
326 pub transitions: Vec<Vec<NfaTransition>>,
329 pub start: StateId,
330 pub accept: StateId,
331}
332
333impl Nfa {
334 fn new() -> Self {
335 Self {
336 transitions: Vec::new(),
337 start: 0,
338 accept: 0,
339 }
340 }
341
342 fn new_state(&mut self) -> Result<StateId, RPQBuildError> {
343 if self.transitions.len() >= MAX_NFA_STATES {
344 return Err(RPQBuildError::NfaStateLimitExceeded {
345 required: self.transitions.len().saturating_add(1),
346 limit: MAX_NFA_STATES,
347 });
348 }
349 let id = StateId::try_from(self.transitions.len()).map_err(|_| {
350 RPQBuildError::NfaStateLimitExceeded {
351 required: self.transitions.len().saturating_add(1),
352 limit: MAX_NFA_STATES,
353 }
354 })?;
355 self.transitions.push(Vec::new());
356 Ok(id)
357 }
358
359 fn add_transition(
360 &mut self,
361 from: StateId,
362 label: Option<String>,
363 to: StateId,
364 ) -> Result<(), RPQBuildError> {
365 if usize::try_from(to)
366 .ok()
367 .is_none_or(|target| target >= self.transitions.len())
368 {
369 return Err(RPQBuildError::InvalidNfa(format!(
370 "transition target {to} is outside {} states",
371 self.transitions.len()
372 )));
373 }
374 let state_count = self.transitions.len();
375 let transitions = self
376 .transitions
377 .get_mut(usize::try_from(from).map_err(|_| {
378 RPQBuildError::InvalidNfa(format!("transition source {from} is not addressable"))
379 })?)
380 .ok_or_else(|| {
381 RPQBuildError::InvalidNfa(format!(
382 "transition source {from} is outside {state_count} states"
383 ))
384 })?;
385 transitions.push(NfaTransition { label, target: to });
386 Ok(())
387 }
388
389 pub fn states(&self) -> Result<Vec<StateId>, RPQBuildError> {
390 validate_nfa(self)?;
391 let end = StateId::try_from(self.transitions.len()).map_err(|_| {
392 RPQBuildError::NfaStateLimitExceeded {
393 required: self.transitions.len(),
394 limit: MAX_NFA_STATES,
395 }
396 })?;
397 Ok((0..end).collect())
398 }
399}
400
401pub fn build_nfa(expr: &RegularPathExpr) -> Result<Nfa, RPQBuildError> {
404 let required = required_nfa_states(expr)?;
405 let mut nfa = Nfa::new();
406 nfa.transitions
407 .try_reserve_exact(required)
408 .map_err(|_| RPQBuildError::AllocationFailed { states: required })?;
409 let (start, accept) = build_fragment(&mut nfa, expr)?;
410 nfa.start = start;
411 nfa.accept = accept;
412 Ok(nfa)
413}
414
415fn required_nfa_states(expr: &RegularPathExpr) -> Result<usize, RPQBuildError> {
416 let mut work = vec![(expr, 1_usize, false)];
417 let mut values = Vec::<usize>::new();
418 while let Some((current, depth, visited)) = work.pop() {
419 if depth > MAX_RPQ_AST_DEPTH {
420 return Err(RPQBuildError::ExpressionTooDeep {
421 depth,
422 limit: MAX_RPQ_AST_DEPTH,
423 });
424 }
425 if !visited {
426 work.push((current, depth, true));
427 match current {
428 RegularPathExpr::Label(_) => {}
429 RegularPathExpr::Concat(left, right)
430 | RegularPathExpr::Alternation(left, right) => {
431 work.push((right, depth.saturating_add(1), false));
432 work.push((left, depth.saturating_add(1), false));
433 }
434 RegularPathExpr::KleeneStar(inner) | RegularPathExpr::Bounded { inner, .. } => {
435 work.push((inner, depth.saturating_add(1), false));
436 }
437 }
438 continue;
439 }
440
441 let required = match current {
442 RegularPathExpr::Label(_) => Some(2),
443 RegularPathExpr::Concat(_, _) => {
444 let right = values.pop().ok_or_else(|| {
445 RPQBuildError::InvalidNfa("missing concat right fragment".into())
446 })?;
447 let left = values.pop().ok_or_else(|| {
448 RPQBuildError::InvalidNfa("missing concat left fragment".into())
449 })?;
450 left.checked_add(right)
451 }
452 RegularPathExpr::Alternation(_, _) => {
453 let right = values.pop().ok_or_else(|| {
454 RPQBuildError::InvalidNfa("missing alternation right fragment".into())
455 })?;
456 let left = values.pop().ok_or_else(|| {
457 RPQBuildError::InvalidNfa("missing alternation left fragment".into())
458 })?;
459 left.checked_add(right).and_then(|sum| sum.checked_add(2))
460 }
461 RegularPathExpr::KleeneStar(_) => values
462 .pop()
463 .ok_or_else(|| RPQBuildError::InvalidNfa("missing Kleene-star fragment".into()))?
464 .checked_add(2),
465 RegularPathExpr::Bounded { min, max, .. } => {
466 if min > max {
467 return Err(RPQBuildError::InvalidBound {
468 min: *min,
469 max: *max,
470 });
471 }
472 let inner = values.pop().ok_or_else(|| {
473 RPQBuildError::InvalidNfa("missing bounded-repeat fragment".into())
474 })?;
475 usize::try_from(*max)
476 .ok()
477 .and_then(|copies| inner.checked_mul(copies))
478 .and_then(|states| states.checked_add(2))
479 }
480 }
481 .ok_or(RPQBuildError::NfaStateLimitExceeded {
482 required: usize::MAX,
483 limit: MAX_NFA_STATES,
484 })?;
485 if required > MAX_NFA_STATES {
486 return Err(RPQBuildError::NfaStateLimitExceeded {
487 required,
488 limit: MAX_NFA_STATES,
489 });
490 }
491 values.push(required);
492 }
493 values
494 .pop()
495 .ok_or_else(|| RPQBuildError::InvalidNfa("regular path expression has no fragment".into()))
496}
497
498fn build_fragment(
499 nfa: &mut Nfa,
500 expr: &RegularPathExpr,
501) -> Result<(StateId, StateId), RPQBuildError> {
502 match expr {
503 RegularPathExpr::Label(name) => {
504 let s = nfa.new_state()?;
505 let a = nfa.new_state()?;
506 nfa.add_transition(s, Some(name.clone()), a)?;
507 Ok((s, a))
508 }
509 RegularPathExpr::Concat(l, r) => {
510 let (ls, la) = build_fragment(nfa, l)?;
511 let (rs, ra) = build_fragment(nfa, r)?;
512 nfa.add_transition(la, None, rs)?;
513 Ok((ls, ra))
514 }
515 RegularPathExpr::Alternation(l, r) => {
516 let s = nfa.new_state()?;
517 let a = nfa.new_state()?;
518 let (ls, la) = build_fragment(nfa, l)?;
519 let (rs, ra) = build_fragment(nfa, r)?;
520 nfa.add_transition(s, None, ls)?;
521 nfa.add_transition(s, None, rs)?;
522 nfa.add_transition(la, None, a)?;
523 nfa.add_transition(ra, None, a)?;
524 Ok((s, a))
525 }
526 RegularPathExpr::KleeneStar(inner) => {
527 let s = nfa.new_state()?;
528 let a = nfa.new_state()?;
529 let (is, ia) = build_fragment(nfa, inner)?;
530 nfa.add_transition(s, None, is)?;
531 nfa.add_transition(s, None, a)?;
532 nfa.add_transition(ia, None, is)?;
533 nfa.add_transition(ia, None, a)?;
534 Ok((s, a))
535 }
536 RegularPathExpr::Bounded { inner, min, max } => {
537 if min > max {
538 return Err(RPQBuildError::InvalidBound {
539 min: *min,
540 max: *max,
541 });
542 }
543 let start = nfa.new_state()?;
544 let mut current_end = start;
545 for _ in 0..*min {
546 let (is, ia) = build_fragment(nfa, inner)?;
547 nfa.add_transition(current_end, None, is)?;
548 current_end = ia;
549 }
550 let accept = nfa.new_state()?;
551 if min == max {
552 nfa.add_transition(current_end, None, accept)?;
553 } else {
554 nfa.add_transition(current_end, None, accept)?;
555 for _ in 0..(*max - *min) {
556 let (is, ia) = build_fragment(nfa, inner)?;
557 nfa.add_transition(current_end, None, is)?;
558 nfa.add_transition(ia, None, accept)?;
559 current_end = ia;
560 }
561 }
562 Ok((start, accept))
563 }
564 }
565}
566
567pub fn epsilon_closure(
570 nfa: &Nfa,
571 states: &BTreeSet<StateId>,
572) -> Result<BTreeSet<StateId>, RPQBuildError> {
573 validate_nfa(nfa)?;
574 let mut closure = states.clone();
575 let mut stack: Vec<StateId> = states.iter().copied().collect();
576 while let Some(s) = stack.pop() {
577 let outgoing = nfa
578 .transitions
579 .get(usize::try_from(s).map_err(|_| {
580 RPQBuildError::InvalidNfa(format!("closure state {s} is not addressable"))
581 })?)
582 .ok_or_else(|| {
583 RPQBuildError::InvalidNfa(format!("closure state {s} is outside the NFA"))
584 })?;
585 for t in outgoing {
586 if t.label.is_none() && !closure.contains(&t.target) {
587 closure.insert(t.target);
588 stack.push(t.target);
589 }
590 }
591 }
592 Ok(closure)
593}
594
595fn validate_nfa(nfa: &Nfa) -> Result<(), RPQBuildError> {
596 let state_count = nfa.transitions.len();
597 if state_count == 0 {
598 return Err(RPQBuildError::InvalidNfa("NFA has no states".into()));
599 }
600 if state_count > MAX_NFA_STATES {
601 return Err(RPQBuildError::NfaStateLimitExceeded {
602 required: state_count,
603 limit: MAX_NFA_STATES,
604 });
605 }
606 for (name, state) in [("start", nfa.start), ("accept", nfa.accept)] {
607 if usize::try_from(state)
608 .ok()
609 .is_none_or(|index| index >= state_count)
610 {
611 return Err(RPQBuildError::InvalidNfa(format!(
612 "{name} state {state} is outside {state_count} states"
613 )));
614 }
615 }
616 for (source, transitions) in nfa.transitions.iter().enumerate() {
617 for transition in transitions {
618 if usize::try_from(transition.target)
619 .ok()
620 .is_none_or(|target| target >= state_count)
621 {
622 return Err(RPQBuildError::InvalidNfa(format!(
623 "transition from state {source} targets missing state {}",
624 transition.target
625 )));
626 }
627 }
628 }
629 Ok(())
630}
631
632pub type DfaState = BTreeSet<StateId>;
637
638#[derive(Debug)]
639pub struct Dfa {
640 pub start: DfaState,
641 pub accepts: BTreeSet<DfaState>,
642 pub transitions: BTreeMap<DfaState, BTreeMap<String, DfaState>>,
643}
644
645pub fn subset_construction(nfa: &Nfa) -> Result<Dfa, RPQBuildError> {
647 validate_nfa(nfa)?;
648 let mut alphabet: BTreeSet<String> = BTreeSet::new();
650 for transitions in &nfa.transitions {
651 for t in transitions {
652 if let Some(label) = &t.label {
653 alphabet.insert(label.clone());
654 }
655 }
656 }
657
658 let initial = epsilon_closure(nfa, &BTreeSet::from([nfa.start]))?;
659 let mut transitions: BTreeMap<DfaState, BTreeMap<String, DfaState>> = BTreeMap::new();
660 let mut accepts: BTreeSet<DfaState> = BTreeSet::new();
661 let mut seen: BTreeSet<DfaState> = BTreeSet::from([initial.clone()]);
662 let mut work: VecDeque<DfaState> = VecDeque::from([initial.clone()]);
663
664 if initial.contains(&nfa.accept) {
665 accepts.insert(initial.clone());
666 }
667
668 while let Some(current) = work.pop_front() {
669 let mut step: BTreeMap<String, DfaState> = BTreeMap::new();
670 for label in &alphabet {
671 let mut next_nfa: BTreeSet<StateId> = BTreeSet::new();
672 for sid in ¤t {
673 let outgoing = nfa
674 .transitions
675 .get(usize::try_from(*sid).map_err(|_| {
676 RPQBuildError::InvalidNfa(format!("DFA state {sid} is not addressable"))
677 })?)
678 .ok_or_else(|| {
679 RPQBuildError::InvalidNfa(format!("DFA state {sid} is outside the NFA"))
680 })?;
681 for t in outgoing {
682 if t.label.as_deref() == Some(label.as_str()) {
683 next_nfa.insert(t.target);
684 }
685 }
686 }
687 if next_nfa.is_empty() {
688 continue;
689 }
690 let closed = epsilon_closure(nfa, &next_nfa)?;
691 step.insert(label.clone(), closed.clone());
692 if !seen.contains(&closed) {
693 if seen.len() >= MAX_DFA_STATES {
694 return Err(RPQBuildError::DfaStateLimitExceeded {
695 limit: MAX_DFA_STATES,
696 });
697 }
698 seen.insert(closed.clone());
699 work.push_back(closed.clone());
700 if closed.contains(&nfa.accept) {
701 accepts.insert(closed);
702 }
703 }
704 }
705 transitions.insert(current, step);
706 }
707
708 Ok(Dfa {
709 start: initial,
710 accepts,
711 transitions,
712 })
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718
719 #[test]
720 fn parse_single_label() {
721 let e = parse_rpq("knows").unwrap();
722 assert_eq!(e, RegularPathExpr::label("knows"));
723 }
724
725 #[test]
726 fn parse_concat() {
727 let e = parse_rpq("knows/likes").unwrap();
728 assert_eq!(
729 e,
730 RegularPathExpr::concat(
731 RegularPathExpr::label("knows"),
732 RegularPathExpr::label("likes")
733 )
734 );
735 }
736
737 #[test]
738 fn parse_alternation_lower_prec_than_concat() {
739 let e = parse_rpq("a/b|c").unwrap();
740 assert_eq!(
742 e,
743 RegularPathExpr::alt(
744 RegularPathExpr::concat(RegularPathExpr::label("a"), RegularPathExpr::label("b")),
745 RegularPathExpr::label("c")
746 )
747 );
748 }
749
750 #[test]
751 fn parse_star_binds_tightest() {
752 let e = parse_rpq("a*").unwrap();
753 assert_eq!(e, RegularPathExpr::star(RegularPathExpr::label("a")));
754 }
755
756 #[test]
757 fn parse_bounded() {
758 let e = parse_rpq("a{2,5}").unwrap();
759 assert_eq!(
760 e,
761 RegularPathExpr::bounded(RegularPathExpr::label("a"), 2, 5)
762 );
763 }
764
765 #[test]
766 fn parse_rejects_reversed_bound() {
767 assert!(matches!(
768 parse_rpq("a{5,2}"),
769 Err(RPQParseError::MalformedBound(message)) if message.contains("exceeds")
770 ));
771 }
772
773 #[test]
774 fn build_rejects_unbounded_state_allocation_before_expansion() {
775 let expr = RegularPathExpr::bounded(RegularPathExpr::label("a"), 0, u32::MAX);
776 assert!(matches!(
777 build_nfa(&expr),
778 Err(RPQBuildError::NfaStateLimitExceeded { .. })
779 ));
780 }
781
782 #[test]
783 fn build_rejects_programmatically_reversed_bound() {
784 let expr = RegularPathExpr::bounded(RegularPathExpr::label("a"), 5, 2);
785 assert_eq!(
786 build_nfa(&expr).unwrap_err(),
787 RPQBuildError::InvalidBound { min: 5, max: 2 }
788 );
789 }
790
791 #[test]
792 fn subset_construction_rejects_missing_transition_target() {
793 let malformed = Nfa {
794 transitions: vec![vec![NfaTransition {
795 label: None,
796 target: 1,
797 }]],
798 start: 0,
799 accept: 0,
800 };
801 assert!(matches!(
802 subset_construction(&malformed),
803 Err(RPQBuildError::InvalidNfa(message)) if message.contains("missing state")
804 ));
805 }
806
807 #[test]
808 fn parse_grouping() {
809 let e = parse_rpq("(a|b)*").unwrap();
810 assert_eq!(
811 e,
812 RegularPathExpr::star(RegularPathExpr::alt(
813 RegularPathExpr::label("a"),
814 RegularPathExpr::label("b")
815 ))
816 );
817 }
818
819 #[test]
820 fn simplify_idempotent_alternation() {
821 let e = RegularPathExpr::alt(RegularPathExpr::label("a"), RegularPathExpr::label("a"));
822 assert_eq!(simplify(&e).unwrap(), RegularPathExpr::label("a"));
823 }
824
825 #[test]
826 fn simplify_nested_kleene() {
827 let e = RegularPathExpr::star(RegularPathExpr::star(RegularPathExpr::label("a")));
828 assert_eq!(
829 simplify(&e).unwrap(),
830 RegularPathExpr::star(RegularPathExpr::label("a"))
831 );
832 }
833
834 #[test]
835 fn simplify_star_subsumes_label() {
836 let e = RegularPathExpr::alt(
837 RegularPathExpr::star(RegularPathExpr::label("a")),
838 RegularPathExpr::label("a"),
839 );
840 assert_eq!(
841 simplify(&e).unwrap(),
842 RegularPathExpr::star(RegularPathExpr::label("a"))
843 );
844 }
845
846 #[test]
847 fn nfa_label_two_states() {
848 let nfa = build_nfa(&RegularPathExpr::label("a")).unwrap();
849 assert_eq!(nfa.transitions.len(), 2);
850 assert_ne!(nfa.start, nfa.accept);
851 }
852
853 #[test]
854 fn dfa_recognizes_a_or_b() {
855 let nfa = build_nfa(&RegularPathExpr::alt(
856 RegularPathExpr::label("a"),
857 RegularPathExpr::label("b"),
858 ))
859 .unwrap();
860 let dfa = subset_construction(&nfa).unwrap();
861 let after_a = dfa
863 .transitions
864 .get(&dfa.start)
865 .and_then(|m| m.get("a"))
866 .expect("no `a` transition");
867 assert!(dfa.accepts.contains(after_a));
868 let after_b = dfa
869 .transitions
870 .get(&dfa.start)
871 .and_then(|m| m.get("b"))
872 .expect("no `b` transition");
873 assert!(dfa.accepts.contains(after_b));
874 assert!(dfa
876 .transitions
877 .get(&dfa.start)
878 .and_then(|m| m.get("c"))
879 .is_none());
880 }
881
882 #[test]
883 fn dfa_recognizes_kleene_star() {
884 let nfa = build_nfa(&RegularPathExpr::star(RegularPathExpr::label("a"))).unwrap();
885 let dfa = subset_construction(&nfa).unwrap();
886 assert!(dfa.accepts.contains(&dfa.start));
888 }
889}