use std::collections::HashMap;
pub type Alt = Vec<Symbol>;
#[derive(Debug, Clone, PartialEq)]
pub enum Symbol {
Terminal(u8),
AnyByte,
NonTerminal(usize),
}
pub(crate) const MAX_PDA_DEPTH: usize = 8192;
#[derive(Debug, Clone)]
pub struct Rule {
pub name: String,
pub alts: Vec<Alt>,
}
#[derive(Debug, Clone)]
pub struct CompiledGrammar {
pub rules: Vec<Rule>,
}
impl CompiledGrammar {
pub fn num_rules(&self) -> usize {
self.rules.len()
}
pub fn root(&self) -> &Rule {
&self.rules[0]
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct StackFrame {
pub rule_id: usize,
pub alt_idx: usize,
pub sym_pos: usize,
pub consumed: bool,
}
#[derive(Debug, Clone)]
pub struct GrammarState {
pub stack: Vec<StackFrame>,
pub partial_token_bytes: Vec<u8>,
pub complete: bool,
}
impl GrammarState {
pub fn initial() -> Self {
Self {
stack: vec![StackFrame {
rule_id: 0,
alt_idx: 0,
sym_pos: 0,
consumed: false,
}],
partial_token_bytes: Vec::new(),
complete: false,
}
}
pub fn is_complete(&self) -> bool {
self.complete
}
pub fn can_accept_more(&self) -> bool {
!self.complete || !self.stack.is_empty()
}
}
pub(crate) fn initial_grammar_state(grammar: &CompiledGrammar) -> GrammarState {
let mut state = GrammarState::initial();
state.complete = is_accepting(&state, grammar);
state
}
#[derive(Debug, Clone, PartialEq)]
pub enum StepResult {
Accepted,
Rejected,
}
pub fn advance_byte(state: &mut GrammarState, grammar: &CompiledGrammar, b: u8) -> StepResult {
if try_advance_byte(state, grammar, b) {
state.partial_token_bytes.push(b);
state.complete = is_accepting(state, grammar);
StepResult::Accepted
} else {
StepResult::Rejected
}
}
fn try_advance_byte(state: &mut GrammarState, grammar: &CompiledGrammar, b: u8) -> bool {
let mut stack = state.stack.clone();
if try_advance_stack(&mut stack, grammar, b) {
state.stack = stack;
return true;
}
false
}
fn try_advance_stack(stack: &mut Vec<StackFrame>, grammar: &CompiledGrammar, b: u8) -> bool {
loop {
if stack.is_empty() {
return false;
}
if stack.len() > MAX_PDA_DEPTH {
return false;
}
let frame_idx = stack.len() - 1;
let frame = &stack[frame_idx];
let Some(rule) = grammar.rules.get(frame.rule_id) else {
return false;
};
if rule.alts.is_empty() {
if !try_next_alt(stack, grammar, frame_idx) {
return false;
}
continue;
}
let Some(alt) = rule.alts.get(frame.alt_idx) else {
return false;
};
if frame.sym_pos >= alt.len() {
stack.pop();
if let Some(parent) = stack.last_mut() {
parent.sym_pos += 1;
}
continue;
}
let sym = &alt[frame.sym_pos].clone();
match sym {
Symbol::Terminal(t) => {
if *t == b {
mark_consumed(stack);
stack[frame_idx].sym_pos += 1;
collapse_exhausted(stack, grammar);
return true;
} else {
if !try_next_alt(stack, grammar, frame_idx) {
return false;
}
continue;
}
}
Symbol::AnyByte => {
mark_consumed(stack);
stack[frame_idx].sym_pos += 1;
collapse_exhausted(stack, grammar);
return true;
}
Symbol::NonTerminal(rule_id) => {
let rid = *rule_id;
let Some(referenced_rule) = grammar.rules.get(rid) else {
return false;
};
if referenced_rule.alts.is_empty() {
stack[frame_idx].sym_pos += 1;
continue;
}
stack.push(StackFrame {
rule_id: rid,
alt_idx: 0,
sym_pos: 0,
consumed: false,
});
}
}
}
}
fn try_next_alt(
stack: &mut Vec<StackFrame>,
grammar: &CompiledGrammar,
mut frame_idx: usize,
) -> bool {
loop {
if stack[frame_idx].consumed {
return false;
}
let rule_id = stack[frame_idx].rule_id;
let next_alt = stack[frame_idx].alt_idx + 1;
let Some(rule) = grammar.rules.get(rule_id) else {
return false;
};
let num_alts = rule.alts.len();
if next_alt < num_alts {
stack[frame_idx].alt_idx = next_alt;
stack[frame_idx].sym_pos = 0;
stack[frame_idx].consumed = false;
stack.truncate(frame_idx + 1);
return true;
}
if frame_idx == 0 {
return false;
}
stack.truncate(frame_idx);
frame_idx -= 1;
}
}
fn mark_consumed(stack: &mut [StackFrame]) {
for frame in stack.iter_mut() {
frame.consumed = true;
}
}
fn collapse_exhausted(stack: &mut Vec<StackFrame>, grammar: &CompiledGrammar) {
loop {
match stack.last() {
None => break,
Some(frame) => {
let Some(rule) = grammar.rules.get(frame.rule_id) else {
break;
};
if rule.alts.is_empty() {
stack.pop();
if let Some(parent) = stack.last_mut() {
parent.sym_pos += 1;
}
continue;
}
let Some(alt) = rule.alts.get(frame.alt_idx) else {
break;
};
if frame.sym_pos < alt.len() {
break;
}
stack.pop();
if let Some(parent) = stack.last_mut() {
parent.sym_pos += 1;
}
}
}
}
}
fn is_accepting(state: &GrammarState, grammar: &CompiledGrammar) -> bool {
let n = state.stack.len();
for (i, frame) in state.stack.iter().enumerate() {
if frame.rule_id >= grammar.rules.len() {
return false;
}
let rule = &grammar.rules[frame.rule_id];
if rule.alts.is_empty() {
continue;
}
if frame.alt_idx >= rule.alts.len() {
return false;
}
let check_from = if i == n - 1 {
frame.sym_pos
} else {
frame.sym_pos + 1
};
if !remaining_is_nullable(grammar, frame.rule_id, frame.alt_idx, check_from) {
return false;
}
}
true
}
#[derive(Clone, Copy)]
enum NullableFrame {
Alt {
rule_id: usize,
alt_idx: usize,
pos: usize,
},
Rule { rule_id: usize, alt_idx: usize },
}
std::thread_local! {
static NULLABLE_SCRATCH: std::cell::RefCell<(Vec<NullableFrame>, std::collections::HashSet<usize>)> =
std::cell::RefCell::new((Vec::new(), std::collections::HashSet::new()));
}
fn remaining_is_nullable(
grammar: &CompiledGrammar,
rule_id: usize,
alt_idx: usize,
pos: usize,
) -> bool {
use NullableFrame as Frame;
NULLABLE_SCRATCH.with(|scratch| {
let mut scratch = scratch.borrow_mut();
let (stack, visited) = &mut *scratch;
stack.clear();
visited.clear();
stack.push(Frame::Alt {
rule_id,
alt_idx,
pos,
});
let mut pending: Option<bool> = None;
loop {
let Some(&frame) = stack.last() else {
return pending.unwrap_or(true);
};
match frame {
Frame::Alt {
rule_id,
alt_idx,
mut pos,
} => {
let alt = &grammar.rules[rule_id].alts[alt_idx];
if let Some(sub) = pending.take() {
if let Symbol::NonTerminal(rid) = alt[pos] {
visited.remove(&rid);
}
if !sub {
stack.pop();
pending = Some(false);
continue;
}
pos += 1;
}
match alt.get(pos) {
None => {
stack.pop();
pending = Some(true);
}
Some(Symbol::Terminal(_)) | Some(Symbol::AnyByte) => {
stack.pop();
pending = Some(false);
}
Some(Symbol::NonTerminal(rid)) => {
let rid = *rid;
*stack.last_mut().unwrap() = Frame::Alt {
rule_id,
alt_idx,
pos,
};
if !visited.insert(rid) {
stack.pop();
pending = Some(false);
} else {
stack.push(Frame::Rule {
rule_id: rid,
alt_idx: 0,
});
}
}
}
}
Frame::Rule {
rule_id,
mut alt_idx,
} => {
if let Some(sub) = pending.take() {
if sub {
stack.pop();
pending = Some(true);
continue;
}
alt_idx += 1;
}
let Some(rule) = grammar.rules.get(rule_id) else {
stack.pop();
pending = Some(false);
continue;
};
if alt_idx >= rule.alts.len() {
stack.pop();
pending = Some(false);
} else {
*stack.last_mut().unwrap() = Frame::Rule { rule_id, alt_idx };
stack.push(Frame::Alt {
rule_id,
alt_idx,
pos: 0,
});
}
}
}
}
})
}
#[derive(Debug, Clone, PartialEq)]
pub enum SimResult {
Accept,
ContextDependent,
Reject,
}
pub fn simulate_token(
start: &GrammarState,
grammar: &CompiledGrammar,
token: &[u8],
) -> (SimResult, GrammarState) {
let mut state = start.clone();
for (i, &b) in token.iter().enumerate() {
match advance_byte(&mut state, grammar, b) {
StepResult::Accepted => {}
StepResult::Rejected => {
if i > 0 {
return (SimResult::ContextDependent, state);
}
return (SimResult::Reject, state);
}
}
}
(SimResult::Accept, state)
}
#[derive(Debug, Clone, PartialEq)]
pub struct BuilderError(pub String);
impl std::fmt::Display for BuilderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "grammar builder error: {}", self.0)
}
}
impl std::error::Error for BuilderError {}
pub struct GrammarBuilder {
rules: Vec<Rule>,
name_to_id: HashMap<String, usize>,
}
impl GrammarBuilder {
pub fn new() -> Self {
Self {
rules: Vec::new(),
name_to_id: HashMap::new(),
}
}
pub fn reserve(&mut self, name: &str) -> usize {
if let Some(&id) = self.name_to_id.get(name) {
return id;
}
let id = self.rules.len();
self.rules.push(Rule {
name: name.to_string(),
alts: Vec::new(),
});
self.name_to_id.insert(name.to_string(), id);
id
}
pub fn set_alts(&mut self, id: usize, alts: Vec<Alt>) -> Result<(), BuilderError> {
let Some(rule) = self.rules.get_mut(id) else {
return Err(BuilderError(format!(
"set_alts: rule id {id} was not reserved on this builder ({} rule(s) reserved)",
self.rules.len()
)));
};
rule.alts = alts;
Ok(())
}
pub fn add_rule(&mut self, name: &str, alts: Vec<Alt>) -> usize {
let id = self.reserve(name);
self.rules[id].alts = alts;
id
}
pub fn rule_id(&self, name: &str) -> Option<usize> {
self.name_to_id.get(name).copied()
}
pub fn build(self) -> CompiledGrammar {
CompiledGrammar { rules: self.rules }
}
}
impl Default for GrammarBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ab_grammar() -> CompiledGrammar {
let mut b = GrammarBuilder::new();
b.add_rule(
"root",
vec![vec![Symbol::Terminal(b'a'), Symbol::Terminal(b'b')]],
);
b.build()
}
fn or_grammar() -> CompiledGrammar {
let mut b = GrammarBuilder::new();
b.add_rule(
"root",
vec![vec![Symbol::Terminal(b'a')], vec![Symbol::Terminal(b'b')]],
);
b.build()
}
fn digits_grammar() -> CompiledGrammar {
let mut b = GrammarBuilder::new();
let digit_id = b.reserve("digit");
let digit_alts: Vec<Alt> = (b'0'..=b'9')
.map(|byte| vec![Symbol::Terminal(byte)])
.collect();
b.set_alts(digit_id, digit_alts).unwrap();
let rest_id = b.reserve("digit_rest");
b.set_alts(
rest_id,
vec![
vec![Symbol::NonTerminal(digit_id), Symbol::NonTerminal(rest_id)],
vec![], ],
)
.unwrap();
let root_id = b.reserve("root");
b.set_alts(
root_id,
vec![vec![
Symbol::NonTerminal(digit_id),
Symbol::NonTerminal(rest_id),
]],
)
.unwrap();
let mut grammar = b.build();
let root_pos = grammar.rules.iter().position(|r| r.name == "root").unwrap();
grammar.rules.swap(0, root_pos);
let orig_root_id = root_pos;
let swapped_to_id = 0usize;
if orig_root_id != 0 {
for rule in &mut grammar.rules {
for alt in &mut rule.alts {
for sym in alt.iter_mut() {
if let Symbol::NonTerminal(rid) = sym {
if *rid == orig_root_id {
*rid = swapped_to_id;
} else if *rid == 0 {
*rid = orig_root_id;
}
}
}
}
}
}
grammar
}
fn comma_list_grammar() -> CompiledGrammar {
let mut b = GrammarBuilder::new();
let root_id = b.reserve("root"); let body_id = b.reserve("body");
let tail_id = b.reserve("tail");
let elem_id = b.reserve("elem");
b.set_alts(elem_id, vec![vec![Symbol::Terminal(b'x')]])
.unwrap();
b.set_alts(
tail_id,
vec![
vec![
Symbol::Terminal(b','),
Symbol::NonTerminal(elem_id),
Symbol::NonTerminal(tail_id),
],
vec![], ],
)
.unwrap();
b.set_alts(
body_id,
vec![
vec![Symbol::NonTerminal(elem_id), Symbol::NonTerminal(tail_id)],
vec![], ],
)
.unwrap();
b.set_alts(
root_id,
vec![vec![
Symbol::Terminal(b'['),
Symbol::NonTerminal(body_id),
Symbol::Terminal(b']'),
]],
)
.unwrap();
b.build()
}
fn accepts_str(g: &CompiledGrammar, input: &[u8]) -> bool {
let state = GrammarState::initial();
let (result, final_state) = simulate_token(&state, g, input);
result == SimResult::Accept && final_state.is_complete()
}
#[test]
fn comma_list_rejects_trailing_comma() {
let g = comma_list_grammar();
assert!(accepts_str(&g, b"[]")); assert!(accepts_str(&g, b"[x]")); assert!(accepts_str(&g, b"[x,x]")); assert!(!accepts_str(&g, b"[x,]")); assert!(!accepts_str(&g, b"[x,x,]")); assert!(!accepts_str(&g, b"[,]")); }
#[test]
fn ab_grammar_accepts_ab() {
let g = ab_grammar();
let mut state = GrammarState::initial();
assert_eq!(advance_byte(&mut state, &g, b'a'), StepResult::Accepted);
assert!(!state.is_complete()); assert_eq!(advance_byte(&mut state, &g, b'b'), StepResult::Accepted);
assert!(state.is_complete());
}
#[test]
fn ab_grammar_rejects_ba() {
let g = ab_grammar();
let mut state = GrammarState::initial();
assert_eq!(advance_byte(&mut state, &g, b'b'), StepResult::Rejected);
}
#[test]
fn ab_grammar_rejects_partial_a_then_wrong() {
let g = ab_grammar();
let mut state = GrammarState::initial();
advance_byte(&mut state, &g, b'a');
assert_eq!(advance_byte(&mut state, &g, b'x'), StepResult::Rejected);
}
#[test]
fn rejected_byte_leaves_state_intact_and_resumes() {
let mut b = GrammarBuilder::new();
let root_id = b.reserve("root");
let child_id = b.reserve("child");
b.set_alts(
child_id,
vec![vec![Symbol::Terminal(b'b'), Symbol::Terminal(b'c')]],
)
.unwrap();
b.set_alts(
root_id,
vec![vec![Symbol::Terminal(b'a'), Symbol::NonTerminal(child_id)]],
)
.unwrap();
let g = b.build();
let mut state = GrammarState::initial();
assert_eq!(advance_byte(&mut state, &g, b'a'), StepResult::Accepted);
assert_eq!(advance_byte(&mut state, &g, b'b'), StepResult::Accepted);
assert_eq!(advance_byte(&mut state, &g, b'x'), StepResult::Rejected);
assert_eq!(advance_byte(&mut state, &g, b'c'), StepResult::Accepted);
assert!(state.complete);
}
#[test]
fn or_grammar_accepts_a_or_b() {
let g = or_grammar();
let mut s = GrammarState::initial();
assert_eq!(advance_byte(&mut s, &g, b'a'), StepResult::Accepted);
let mut s2 = GrammarState::initial();
assert_eq!(advance_byte(&mut s2, &g, b'b'), StepResult::Accepted);
}
#[test]
fn or_grammar_rejects_c() {
let g = or_grammar();
let mut s = GrammarState::initial();
assert_eq!(advance_byte(&mut s, &g, b'c'), StepResult::Rejected);
}
#[test]
fn dead_child_backtracks_to_parent_alternative() {
let mut builder = GrammarBuilder::new();
let root_id = builder.reserve("root");
let dead_id = builder.reserve("dead");
builder
.set_alts(dead_id, vec![vec![Symbol::Terminal(b'y')]])
.unwrap();
builder
.set_alts(
root_id,
vec![
vec![Symbol::NonTerminal(dead_id)],
vec![Symbol::Terminal(b'x')],
],
)
.unwrap();
let grammar = builder.build();
let mut state = GrammarState::initial();
assert_eq!(
advance_byte(&mut state, &grammar, b'x'),
StepResult::Accepted
);
assert!(state.complete);
}
#[test]
fn deeply_nested_parent_fallback_accepts_on_bounded_stack() {
let depth = MAX_PDA_DEPTH / 2;
let mut rules = Vec::with_capacity(depth);
rules.push(Rule {
name: "root".to_string(),
alts: vec![vec![Symbol::NonTerminal(1)], vec![Symbol::Terminal(b'x')]],
});
for rule_id in 1..depth - 1 {
rules.push(Rule {
name: String::new(),
alts: vec![vec![Symbol::NonTerminal(rule_id + 1)]],
});
}
rules.push(Rule {
name: String::new(),
alts: vec![vec![Symbol::Terminal(b'y')]],
});
let grammar = CompiledGrammar { rules };
std::thread::Builder::new()
.stack_size(64 * 1024)
.spawn(move || {
let mut state = GrammarState::initial();
assert_eq!(
advance_byte(&mut state, &grammar, b'x'),
StepResult::Accepted
);
assert!(state.complete);
})
.expect("bounded-stack regression thread spawns")
.join()
.expect("iterative fallback must not overflow the bounded stack");
}
#[test]
fn deeply_nested_nullable_chain_accepts_on_bounded_stack() {
let depth = MAX_PDA_DEPTH;
let mut rules = Vec::with_capacity(depth);
for rule_id in 0..depth - 1 {
rules.push(Rule {
name: String::new(),
alts: vec![vec![Symbol::NonTerminal(rule_id + 1)]],
});
}
rules.push(Rule {
name: String::new(),
alts: vec![vec![]],
});
let grammar = CompiledGrammar { rules };
std::thread::Builder::new()
.stack_size(64 * 1024)
.spawn(move || {
let state = initial_grammar_state(&grammar);
assert!(state.is_complete());
})
.expect("bounded-stack regression thread spawns")
.join()
.expect("iterative nullability walk must not overflow the bounded stack");
}
fn nested_terminal_grammar(depth: usize, terminal: u8) -> CompiledGrammar {
let mut rules = Vec::with_capacity(depth);
for rule_id in 0..depth - 1 {
rules.push(Rule {
name: String::new(),
alts: vec![vec![Symbol::NonTerminal(rule_id + 1)]],
});
}
rules.push(Rule {
name: String::new(),
alts: vec![vec![Symbol::Terminal(terminal)]],
});
CompiledGrammar { rules }
}
#[test]
fn nesting_depth_limit_matches_recursive_boundary() {
let at_limit = nested_terminal_grammar(MAX_PDA_DEPTH, b'x');
let mut state = GrammarState::initial();
assert_eq!(
advance_byte(&mut state, &at_limit, b'x'),
StepResult::Accepted
);
let past_limit = nested_terminal_grammar(MAX_PDA_DEPTH + 1, b'x');
let mut state = GrammarState::initial();
assert_eq!(
advance_byte(&mut state, &past_limit, b'x'),
StepResult::Rejected
);
}
fn leading_terminal_then_nt_grammar() -> CompiledGrammar {
let mut b = GrammarBuilder::new();
let root_id = b.reserve("root");
let nt_id = b.reserve("nonterm");
b.set_alts(
nt_id,
vec![vec![Symbol::Terminal(b'c'), Symbol::Terminal(b'd')]],
)
.unwrap();
b.set_alts(
root_id,
vec![
vec![Symbol::Terminal(b'a'), Symbol::NonTerminal(nt_id)],
vec![Symbol::Terminal(b'x')],
],
)
.unwrap();
b.build()
}
#[test]
fn leading_terminal_then_nt_accepts_valid() {
let g = leading_terminal_then_nt_grammar();
let s0 = GrammarState::initial();
let (r_acd, _) = simulate_token(&s0, &g, b"acd");
assert_eq!(r_acd, SimResult::Accept);
let s1 = GrammarState::initial();
let (r_x, _) = simulate_token(&s1, &g, b"x");
assert_eq!(r_x, SimResult::Accept);
}
#[test]
fn simulate_token_full_match() {
let g = ab_grammar();
let state = GrammarState::initial();
let (result, _) = simulate_token(&state, &g, b"ab");
assert_eq!(result, SimResult::Accept);
}
#[test]
fn simulate_token_reject() {
let g = ab_grammar();
let state = GrammarState::initial();
let (result, _) = simulate_token(&state, &g, b"ba");
assert_eq!(result, SimResult::Reject);
}
#[test]
fn simulate_token_partial_is_context_dependent() {
let g = ab_grammar();
let state = GrammarState::initial();
let (result, _) = simulate_token(&state, &g, b"ax");
assert_eq!(result, SimResult::ContextDependent);
}
#[test]
fn state_partial_bytes_recorded() {
let g = ab_grammar();
let mut state = GrammarState::initial();
advance_byte(&mut state, &g, b'a');
assert_eq!(state.partial_token_bytes, vec![b'a']);
advance_byte(&mut state, &g, b'b');
assert_eq!(state.partial_token_bytes, vec![b'a', b'b']);
}
#[test]
fn any_byte_matches_any_value() {
let mut b = GrammarBuilder::new();
b.add_rule("root", vec![vec![Symbol::AnyByte]]);
let g = b.build();
for byte in [b'a', b'z', b'0', b'\n', 0xffu8] {
let mut s = GrammarState::initial();
assert_eq!(advance_byte(&mut s, &g, byte), StepResult::Accepted);
assert!(s.is_complete());
}
}
#[test]
fn digits_grammar_accepts_single_digit() {
let g = digits_grammar();
let state = GrammarState::initial();
let (result, _) = simulate_token(&state, &g, b"5");
assert_eq!(result, SimResult::Accept);
}
#[test]
fn digits_grammar_accepts_multi_digit() {
let g = digits_grammar();
let state = GrammarState::initial();
let (result, final_state) = simulate_token(&state, &g, b"123");
assert_eq!(result, SimResult::Accept);
assert!(final_state.is_complete());
}
#[test]
fn digits_grammar_rejects_letter() {
let g = digits_grammar();
let state = GrammarState::initial();
let (result, _) = simulate_token(&state, &g, b"abc");
assert_eq!(result, SimResult::Reject);
}
#[test]
fn grammar_builder_reserve_idempotent() {
let mut builder = GrammarBuilder::new();
let id1 = builder.reserve("foo");
let id2 = builder.reserve("foo");
assert_eq!(id1, id2);
}
#[test]
fn missing_root_rule_id_rejects_without_panicking() {
let grammar = CompiledGrammar { rules: Vec::new() };
let mut state = GrammarState::initial();
let before = state.clone();
assert_eq!(
advance_byte(&mut state, &grammar, b'x'),
StepResult::Rejected
);
assert_eq!(state.stack, before.stack);
assert_eq!(state.partial_token_bytes, before.partial_token_bytes);
assert_eq!(state.complete, before.complete);
}
#[test]
fn out_of_range_state_rule_id_rejects_without_panicking() {
let grammar = CompiledGrammar {
rules: vec![Rule {
name: "root".to_string(),
alts: vec![vec![Symbol::Terminal(b'x')]],
}],
};
let mut state = GrammarState {
stack: vec![StackFrame {
rule_id: 1,
alt_idx: 0,
sym_pos: 0,
consumed: false,
}],
partial_token_bytes: Vec::new(),
complete: false,
};
let before = state.clone();
assert_eq!(
advance_byte(&mut state, &grammar, b'x'),
StepResult::Rejected
);
assert_eq!(state.stack, before.stack);
assert_eq!(state.partial_token_bytes, before.partial_token_bytes);
assert_eq!(state.complete, before.complete);
}
#[test]
fn out_of_range_non_terminal_rule_id_rejects_without_panicking() {
let grammar = CompiledGrammar {
rules: vec![Rule {
name: "root".to_string(),
alts: vec![vec![Symbol::NonTerminal(1)]],
}],
};
let mut state = GrammarState::initial();
let before = state.clone();
assert_eq!(
advance_byte(&mut state, &grammar, b'x'),
StepResult::Rejected
);
assert_eq!(state.stack, before.stack);
assert_eq!(state.partial_token_bytes, before.partial_token_bytes);
assert_eq!(state.complete, before.complete);
}
#[test]
fn set_alts_out_of_range_id_returns_err() {
let mut b = GrammarBuilder::new();
let root_id = b.reserve("root");
assert_eq!(root_id, 0);
let err = b
.set_alts(5, vec![vec![Symbol::Terminal(b'x')]])
.expect_err("set_alts on an unreserved id must return Err, not panic");
assert!(err.0.contains('5'), "error should name the bad id: {err:?}");
}
#[test]
fn set_alts_in_range_id_succeeds() {
let mut b = GrammarBuilder::new();
let id = b.reserve("root");
b.set_alts(id, vec![vec![Symbol::Terminal(b'x')]])
.expect("set_alts on a freshly reserved id must succeed");
let g = b.build();
assert!(accepts_str(&g, b"x"));
}
#[test]
fn out_of_range_alt_idx_rejects_without_panicking() {
let grammar = CompiledGrammar {
rules: vec![Rule {
name: "root".to_string(),
alts: vec![vec![Symbol::Terminal(b'x')]], }],
};
let mut state = GrammarState {
stack: vec![StackFrame {
rule_id: 0,
alt_idx: 7, sym_pos: 0,
consumed: false,
}],
partial_token_bytes: Vec::new(),
complete: false,
};
let before = state.clone();
assert_eq!(
advance_byte(&mut state, &grammar, b'x'),
StepResult::Rejected
);
assert_eq!(state.stack, before.stack);
assert_eq!(state.partial_token_bytes, before.partial_token_bytes);
assert_eq!(state.complete, before.complete);
}
#[test]
fn dangling_ancestor_rule_id_rejects_during_backtrack_without_panicking() {
let grammar = CompiledGrammar {
rules: vec![Rule {
name: "child".to_string(),
alts: vec![vec![Symbol::Terminal(b'z')]],
}],
};
let mut state = GrammarState {
stack: vec![
StackFrame {
rule_id: 999, alt_idx: 0,
sym_pos: 0,
consumed: false,
},
StackFrame {
rule_id: 0, alt_idx: 0,
sym_pos: 0,
consumed: false,
},
],
partial_token_bytes: Vec::new(),
complete: false,
};
let before = state.clone();
assert_eq!(
advance_byte(&mut state, &grammar, b'x'),
StepResult::Rejected
);
assert_eq!(state.stack, before.stack);
assert_eq!(state.partial_token_bytes, before.partial_token_bytes);
assert_eq!(state.complete, before.complete);
}
#[test]
fn dangling_ancestor_rule_id_stops_collapse_without_panicking() {
let grammar = CompiledGrammar {
rules: vec![Rule {
name: "child".to_string(),
alts: vec![vec![Symbol::Terminal(b'x')]],
}],
};
let mut state = GrammarState {
stack: vec![
StackFrame {
rule_id: 999, alt_idx: 0,
sym_pos: 0,
consumed: false,
},
StackFrame {
rule_id: 0, alt_idx: 0,
sym_pos: 0,
consumed: false,
},
],
partial_token_bytes: Vec::new(),
complete: false,
};
let result = advance_byte(&mut state, &grammar, b'x');
assert_eq!(result, StepResult::Accepted);
}
}