use crate::parser::core::SyntaxError;
use crate::parser::events::Event;
use crate::parser::lexer::{Token, VerbCtx, is_block_environment, is_math_environment};
use crate::syntax::SyntaxKind;
const BEGIN_CMD: &str = "\\begin";
const END_CMD: &str = "\\end";
const LEFT_CMD: &str = "\\left";
const RIGHT_CMD: &str = "\\right";
const PARSER_STEP_LIMIT: u32 = 15_000_000;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Block {
Document,
Environment,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum CommentMode {
Skip,
Stop,
}
struct TriviaScan {
next: usize,
next_kind: Option<SyntaxKind>,
saw_blank_line: bool,
comment_start: Option<usize>,
}
pub(crate) fn parse(tokens: &[Token], ctx: &VerbCtx) -> (Vec<Event>, Vec<SyntaxError>) {
let mut p = Parser::new(tokens, ctx);
p.document();
debug_assert_balanced(&p.events);
(p.events, p.errors)
}
fn debug_assert_balanced(events: &[Event]) {
if !cfg!(debug_assertions) {
return;
}
let mut depth: i32 = 0;
for ev in events {
match ev {
Event::Start(_) => depth += 1,
Event::Finish => {
depth -= 1;
debug_assert!(depth >= 0, "parser emitted a Finish with no open node");
}
Event::Tok(_) | Event::SubTok { .. } => {}
}
}
debug_assert_eq!(
depth, 0,
"parser left {depth} node(s) unclosed at end of parse"
);
}
struct Parser<'t> {
tokens: &'t [Token],
ctx: &'t VerbCtx,
starts: Vec<usize>,
pos: usize,
events: Vec<Event>,
errors: Vec<SyntaxError>,
steps: std::cell::Cell<u32>,
last_step_pos: std::cell::Cell<usize>,
}
impl<'t> Parser<'t> {
fn new(tokens: &'t [Token], ctx: &'t VerbCtx) -> Self {
let mut starts = Vec::with_capacity(tokens.len() + 1);
let mut off = 0;
for t in tokens {
starts.push(off);
off += t.text.len();
}
starts.push(off);
Self {
tokens,
ctx,
starts,
pos: 0,
events: Vec::new(),
steps: std::cell::Cell::new(0),
last_step_pos: std::cell::Cell::new(0),
errors: Vec::new(),
}
}
#[inline]
fn step(&self) {
if self.pos != self.last_step_pos.get() {
self.last_step_pos.set(self.pos);
self.steps.set(0);
}
let steps = self.steps.get();
assert!(
steps < PARSER_STEP_LIMIT,
"parser exceeded {PARSER_STEP_LIMIT} peeks without consuming a token at position {} \
— non-advancing loop",
self.pos
);
self.steps.set(steps + 1);
}
fn kind(&self) -> Option<SyntaxKind> {
self.step();
self.tokens.get(self.pos).map(|t| t.kind)
}
fn nth_kind(&self, n: usize) -> Option<SyntaxKind> {
self.step();
self.tokens.get(self.pos + n).map(|t| t.kind)
}
fn text(&self) -> &str {
self.tokens
.get(self.pos)
.map(|t| t.text.as_str())
.unwrap_or("")
}
fn at_end(&self) -> bool {
self.pos >= self.tokens.len()
}
fn at_command(&self, name: &str) -> bool {
self.kind() == Some(SyntaxKind::CONTROL_WORD) && self.text() == name
}
fn is_trivia(k: SyntaxKind) -> bool {
matches!(
k,
SyntaxKind::WHITESPACE
| SyntaxKind::NEWLINE
| SyntaxKind::COMMENT
| SyntaxKind::DOC_MARGIN
| SyntaxKind::GUARD
)
}
fn bump(&mut self) {
debug_assert!(!self.at_end(), "bump past end of input");
self.events.push(Event::Tok(self.pos));
self.pos += 1;
}
fn open(&mut self, kind: SyntaxKind) {
self.events.push(Event::Start(kind));
}
fn close(&mut self) {
self.events.push(Event::Finish);
}
fn error(&mut self, message: impl Into<String>) {
let (start, end) = if self.at_end() {
let end = *self.starts.last().expect("starts is non-empty");
(end, end)
} else {
(self.starts[self.pos], self.starts[self.pos + 1])
};
self.errors.push(SyntaxError {
message: message.into(),
start,
end,
});
}
fn error_at(&mut self, range: (usize, usize), message: impl Into<String>) {
self.errors.push(SyntaxError {
message: message.into(),
start: range.0,
end: range.1,
});
}
fn token_span(&self, pos: usize) -> (usize, usize) {
(self.starts[pos], self.starts[pos + 1])
}
fn skip_trivia(&mut self) {
while self.kind().is_some_and(Self::is_trivia) {
self.bump();
}
}
fn scan_trivia(&self, from: usize, comment_mode: CommentMode) -> TriviaScan {
let mut i = from;
let mut newlines = 0;
let mut saw_blank_line = false;
let mut comment_start = None;
while let Some(t) = self.tokens.get(i) {
match t.kind {
SyntaxKind::NEWLINE => {
newlines += 1;
if newlines >= 2 {
saw_blank_line = true;
comment_start = None;
}
}
SyntaxKind::WHITESPACE | SyntaxKind::DOC_MARGIN | SyntaxKind::GUARD => {}
SyntaxKind::COMMENT if comment_mode == CommentMode::Stop => break,
SyntaxKind::COMMENT => {
newlines = 0;
if comment_start.is_none() && self.comment_starts_line(i) {
comment_start = Some(i);
}
}
_ => break,
}
i += 1;
}
TriviaScan {
next: i,
next_kind: self.tokens.get(i).map(|t| t.kind),
saw_blank_line,
comment_start,
}
}
fn peek_meaningful(&self) -> (Option<SyntaxKind>, bool) {
let s = self.scan_trivia(self.pos, CommentMode::Skip);
(s.next_kind, s.saw_blank_line)
}
fn peek_meaningful_text(&self) -> Option<&str> {
let mut i = self.pos;
while let Some(t) = self.tokens.get(i) {
if !Self::is_trivia(t.kind) {
return Some(t.text.as_str());
}
i += 1;
}
None
}
fn at_paragraph_break(&self) -> bool {
self.scan_trivia(self.pos, CommentMode::Skip).saw_blank_line
}
fn comment_starts_line(&self, pos: usize) -> bool {
let mut i = pos;
while i > 0 {
i -= 1;
match self.tokens[i].kind {
SyntaxKind::WHITESPACE | SyntaxKind::DOC_MARGIN | SyntaxKind::GUARD => {
continue;
}
SyntaxKind::NEWLINE => return true,
_ => return false,
}
}
true
}
fn binding_run(&self, from: usize) -> Option<(usize, usize, SyntaxKind)> {
let s = self.scan_trivia(from, CommentMode::Skip);
let start = s.comment_start?;
if s.next_kind != Some(SyntaxKind::CONTROL_WORD) {
return None;
}
let kind = match self.tokens[s.next].text.as_str() {
BEGIN_CMD => SyntaxKind::ENVIRONMENT,
END_CMD => return None,
_ => SyntaxKind::COMMAND,
};
Some((start, s.next, kind))
}
fn document(&mut self) {
self.parse_block(Block::Document);
}
fn parse_block(&mut self, block: Block) {
loop {
if self.at_block_end(block) {
break;
}
if self.kind().is_some_and(Self::is_trivia) && self.trivia_run_is_separator(block) {
let stop = self
.binding_run(self.pos)
.map_or(self.tokens.len(), |(comment_start, ..)| comment_start);
while self.pos < stop && self.kind().is_some_and(Self::is_trivia) {
self.bump();
}
continue;
}
let checkpoint = self.events.len();
let mut nontrivia_count = 0usize;
let mut lone_block_env = false;
loop {
if self.at_block_end(block) {
break;
}
if self.kind().is_some_and(Self::is_trivia) && self.trivia_run_is_separator(block) {
break;
}
if let Some((comment_start, construct_pos, _)) = self.binding_run(self.pos) {
while self.pos < comment_start {
self.bump();
}
let checkpoint = self.events.len();
self.open(SyntaxKind::DOC_COMMENT);
while self.pos < construct_pos {
self.bump();
}
self.close();
let starts_block_env = self.tokens[construct_pos].text == BEGIN_CMD
&& peek_begin_name(self.tokens, construct_pos)
.as_deref()
.is_some_and(is_block_environment);
let construct_start = self.events.len();
self.element();
if let Event::Start(kind) = self.events[construct_start] {
self.events.remove(construct_start);
self.events.insert(checkpoint, Event::Start(kind));
}
nontrivia_count += 1;
lone_block_env = nontrivia_count == 1 && starts_block_env;
continue;
}
let is_nontrivia = !self.kind().is_some_and(Self::is_trivia);
let starts_block_env = self.at_command(BEGIN_CMD)
&& peek_begin_name(self.tokens, self.pos)
.as_deref()
.is_some_and(is_block_environment);
self.element();
if is_nontrivia {
nontrivia_count += 1;
lone_block_env = nontrivia_count == 1 && starts_block_env;
}
}
if !lone_block_env {
self.events
.insert(checkpoint, Event::Start(SyntaxKind::PARAGRAPH));
self.close(); }
}
}
fn at_block_end(&self, block: Block) -> bool {
self.at_end() || (block == Block::Environment && self.at_command(END_CMD))
}
fn trivia_run_is_separator(&self, block: Block) -> bool {
let s = self.scan_trivia(self.pos, CommentMode::Skip);
s.saw_blank_line
|| match s.next_kind {
None => true,
Some(SyntaxKind::CONTROL_WORD) => {
block == Block::Environment && self.tokens[s.next].text == END_CMD
}
Some(_) => false,
}
}
fn element(&mut self) {
let Some(k) = self.kind() else { return };
match k {
SyntaxKind::WHITESPACE
| SyntaxKind::NEWLINE
| SyntaxKind::COMMENT
| SyntaxKind::DOC_MARGIN
| SyntaxKind::GUARD => self.bump(),
SyntaxKind::CONTROL_WORD => {
if self.at_command(BEGIN_CMD) {
self.environment();
} else if self.at_command(END_CMD) {
self.stray_end();
} else {
self.command();
}
}
SyntaxKind::CONTROL_SYMBOL => {
let sym = self.text().to_owned();
match sym.as_str() {
"\\[" => self.delim_math(SyntaxKind::DISPLAY_MATH, "\\[", "\\]"),
"\\(" => self.delim_math(SyntaxKind::INLINE_MATH, "\\(", "\\)"),
"\\]" | "\\)" => {
self.error(format!("unmatched `{sym}`"));
self.bump();
}
"\\\\" => self.line_break(),
_ => self.bump(),
}
}
SyntaxKind::L_BRACE => self.group(),
SyntaxKind::R_BRACE => {
self.error("unmatched `}`");
self.bump();
}
SyntaxKind::DOLLAR => self.dollar_math(),
_ => self.bump(),
}
}
fn command(&mut self) {
self.open(SyntaxKind::COMMAND);
self.bump(); self.attach_arguments();
self.close();
}
fn line_break(&mut self) {
self.open(SyntaxKind::LINE_BREAK);
self.bump(); if self.kind() == Some(SyntaxKind::WORD) && self.text() == "*" {
self.bump(); }
if self.kind() == Some(SyntaxKind::L_BRACKET) {
self.optional(); }
self.close();
}
fn attach_arguments(&mut self) {
loop {
let (next, paragraph_break) = self.peek_meaningful();
if paragraph_break {
break;
}
match next {
Some(SyntaxKind::L_BRACE) => {
self.skip_trivia();
self.group();
}
Some(SyntaxKind::L_BRACKET) => {
self.skip_trivia();
self.optional();
}
Some(SyntaxKind::VERB)
if !self
.peek_meaningful_text()
.is_some_and(|t| t.starts_with('\\')) =>
{
self.skip_trivia();
self.bump(); }
_ => break,
}
}
}
fn group(&mut self) {
debug_assert_eq!(self.kind(), Some(SyntaxKind::L_BRACE));
let opener = self.token_span(self.pos);
self.open(SyntaxKind::GROUP);
self.bump(); loop {
match self.kind() {
None => {
self.error_at(opener, "unclosed `{`");
break;
}
Some(SyntaxKind::R_BRACE) => {
self.bump();
break;
}
_ => self.element(),
}
}
self.close();
}
fn optional(&mut self) {
debug_assert_eq!(self.kind(), Some(SyntaxKind::L_BRACKET));
let opener = self.token_span(self.pos);
self.open(SyntaxKind::OPTIONAL);
self.bump(); loop {
match self.kind() {
None | Some(SyntaxKind::R_BRACE) => {
self.error_at(opener, "unclosed `[`");
break;
}
Some(SyntaxKind::R_BRACKET) => {
self.bump();
break;
}
Some(SyntaxKind::CONTROL_WORD)
if self.at_command(BEGIN_CMD) || self.at_command(END_CMD) =>
{
self.error_at(opener, "unclosed `[`");
break;
}
_ => {
if self.at_paragraph_break() {
self.error_at(opener, "unclosed `[`");
break;
}
self.element();
}
}
}
self.close();
}
fn dollar_math(&mut self) {
let display = self.nth_kind(1) == Some(SyntaxKind::DOLLAR);
let (kind, label) = if display {
(SyntaxKind::DISPLAY_MATH, "$$")
} else {
(SyntaxKind::INLINE_MATH, "$")
};
let opener = (
self.starts[self.pos],
self.starts[self.pos + if display { 2 } else { 1 }],
);
self.open(kind);
self.bump(); if display {
self.bump(); }
self.open(SyntaxKind::MATH);
loop {
match self.kind() {
None => {
self.error_at(opener, format!("unclosed `{label}`"));
break;
}
Some(SyntaxKind::R_BRACE) => {
self.error_at(opener, format!("unclosed `{label}`"));
break;
}
Some(SyntaxKind::CONTROL_WORD) if self.at_command(END_CMD) => {
self.error_at(opener, format!("unclosed `{label}`"));
break;
}
Some(SyntaxKind::DOLLAR) => {
if display && self.nth_kind(1) != Some(SyntaxKind::DOLLAR) {
self.bump();
continue;
}
break;
}
_ => {
if self.at_paragraph_break() {
self.error_at(opener, format!("unclosed `{label}`"));
break;
}
self.math_element();
}
}
}
self.close(); if self.kind() == Some(SyntaxKind::DOLLAR) {
self.bump(); if display {
self.bump(); }
}
self.close(); }
fn delim_math(&mut self, kind: SyntaxKind, opener: &str, closer: &str) {
let opener_span = self.token_span(self.pos);
self.open(kind);
self.bump(); self.open(SyntaxKind::MATH);
loop {
match self.kind() {
None => {
self.error_at(opener_span, format!("unclosed `{opener}`"));
break;
}
Some(SyntaxKind::CONTROL_SYMBOL) if self.text() == closer => {
break;
}
Some(SyntaxKind::R_BRACE) => {
self.error_at(opener_span, format!("unclosed `{opener}`"));
break;
}
Some(SyntaxKind::CONTROL_WORD) if self.at_command(END_CMD) => {
self.error_at(opener_span, format!("unclosed `{opener}`"));
break;
}
_ => {
if self.at_paragraph_break() {
self.error_at(opener_span, format!("unclosed `{opener}`"));
break;
}
self.math_element();
}
}
}
self.close(); if self.kind() == Some(SyntaxKind::CONTROL_SYMBOL) && self.text() == closer {
self.bump(); }
self.close(); }
fn math_element(&mut self) {
match self.kind() {
Some(
SyntaxKind::WHITESPACE
| SyntaxKind::NEWLINE
| SyntaxKind::COMMENT
| SyntaxKind::DOC_MARGIN
| SyntaxKind::GUARD,
) => self.bump(),
_ => self.math_scripted(),
}
}
fn math_scripted(&mut self) {
if self.kind() == Some(SyntaxKind::WORD)
&& let Some(pieces) = split_math_word(self.text())
{
let idx = self.pos;
let (last, lead) = pieces.split_last().expect("split yields >= 2 pieces");
for &(start, end) in lead {
self.events.push(Event::SubTok { idx, start, end });
}
let checkpoint = self.events.len();
self.events.push(Event::SubTok {
idx,
start: last.0,
end: last.1,
});
self.pos += 1; self.math_scripts(checkpoint);
return;
}
let checkpoint = self.events.len();
self.math_atom();
self.math_scripts(checkpoint);
}
fn math_scripts(&mut self, checkpoint: usize) {
if !self.at_script() {
return; }
self.events
.insert(checkpoint, Event::Start(SyntaxKind::SCRIPTED));
while self.at_script() {
self.skip_trivia(); let sub = self.kind() == Some(SyntaxKind::UNDERSCORE);
self.open(if sub {
SyntaxKind::SUBSCRIPT
} else {
SyntaxKind::SUPERSCRIPT
});
self.bump(); self.math_script_arg();
self.close();
}
self.close(); }
fn at_script(&self) -> bool {
let s = self.scan_trivia(self.pos, CommentMode::Stop);
!s.saw_blank_line
&& matches!(
s.next_kind,
Some(SyntaxKind::CARET | SyntaxKind::UNDERSCORE)
)
}
fn math_atom(&mut self) {
match self.kind() {
Some(SyntaxKind::L_BRACE) => self.math_group(),
Some(SyntaxKind::CONTROL_WORD) => {
if self.at_command(BEGIN_CMD) {
self.environment();
} else if self.at_command(END_CMD) {
self.stray_end();
} else if self.at_command(LEFT_CMD) {
self.left_right();
} else if self.at_command(RIGHT_CMD) {
self.stray_right();
} else {
self.command();
}
}
Some(SyntaxKind::CONTROL_SYMBOL) if self.text() == "\\\\" => self.line_break(),
Some(_) => self.bump(),
None => {}
}
}
fn math_script_arg(&mut self) {
if self.at_paragraph_break() {
self.error("missing argument after `^`/`_`");
return;
}
self.skip_trivia();
let missing = match self.kind() {
None | Some(SyntaxKind::R_BRACE | SyntaxKind::DOLLAR) => true,
Some(SyntaxKind::CONTROL_SYMBOL) => matches!(self.text(), "\\]" | "\\)"),
Some(SyntaxKind::CONTROL_WORD) => self.at_command(END_CMD),
_ => false,
};
if missing {
self.error("missing argument after `^`/`_`");
return;
}
self.math_atom();
}
fn math_group(&mut self) {
debug_assert_eq!(self.kind(), Some(SyntaxKind::L_BRACE));
let opener = self.token_span(self.pos);
self.open(SyntaxKind::GROUP);
self.bump(); loop {
match self.kind() {
None => {
self.error_at(opener, "unclosed `{`");
break;
}
Some(SyntaxKind::R_BRACE) => {
self.bump();
break;
}
_ => self.math_element(),
}
}
self.close();
}
fn left_right(&mut self) {
debug_assert!(self.at_command(LEFT_CMD));
let opener = self.token_span(self.pos);
self.open(SyntaxKind::LEFT_RIGHT);
self.bump(); self.math_delim(LEFT_CMD);
self.open(SyntaxKind::MATH);
loop {
match self.kind() {
None => {
self.error_at(opener, "unclosed `\\left`");
break;
}
Some(SyntaxKind::CONTROL_WORD) if self.at_command(RIGHT_CMD) => break,
Some(SyntaxKind::R_BRACE | SyntaxKind::DOLLAR) => {
self.error_at(opener, "unclosed `\\left`");
break;
}
Some(SyntaxKind::CONTROL_SYMBOL) if matches!(self.text(), "\\]" | "\\)") => {
self.error_at(opener, "unclosed `\\left`");
break;
}
Some(SyntaxKind::CONTROL_WORD) if self.at_command(END_CMD) => {
self.error_at(opener, "unclosed `\\left`");
break;
}
_ => {
if self.at_paragraph_break() {
self.error_at(opener, "unclosed `\\left`");
break;
}
self.math_element();
}
}
}
self.close(); if self.at_command(RIGHT_CMD) {
self.bump(); self.math_delim(RIGHT_CMD);
}
self.close(); }
fn math_delim(&mut self, after: &str) {
self.skip_trivia();
let missing = match self.kind() {
None | Some(SyntaxKind::R_BRACE | SyntaxKind::DOLLAR) => true,
Some(SyntaxKind::CONTROL_SYMBOL) => matches!(self.text(), "\\]" | "\\)"),
Some(SyntaxKind::CONTROL_WORD) => {
self.at_command(END_CMD) || self.at_command(LEFT_CMD) || self.at_command(RIGHT_CMD)
}
_ => false,
};
if missing {
self.error(format!("missing delimiter after `{after}`"));
return;
}
self.bump();
}
fn stray_right(&mut self) {
debug_assert!(self.at_command(RIGHT_CMD));
self.error("`\\right` without matching `\\left`");
self.bump(); self.math_delim(RIGHT_CMD);
}
fn environment(&mut self) {
self.open(SyntaxKind::ENVIRONMENT);
let begin_start = self.starts[self.pos];
self.open(SyntaxKind::BEGIN);
self.bump(); let name = self.name_group();
let opener = (begin_start, self.starts[self.pos]);
self.attach_arguments(); self.close();
if name
.as_deref()
.is_some_and(|n| self.ctx.is_verbatim_environment(n))
{
self.verbatim_body(name.as_deref().expect("verbatim name"));
} else if name.as_deref().is_some_and(is_math_environment) {
self.math_environment_body();
} else {
self.parse_block(Block::Environment);
}
self.finish_environment(&name, opener);
}
fn finish_environment(&mut self, name: &Option<String>, opener: (usize, usize)) {
match self.kind() {
None => {
self.error_at(
opener,
format!("unclosed environment `{}`", name.as_deref().unwrap_or("")),
);
}
Some(_) => {
let end_name = peek_end_name(self.tokens, self.pos);
if name.is_none() || *name == end_name {
self.open(SyntaxKind::END);
self.bump(); self.name_group();
self.close();
} else {
self.error_at(
opener,
format!(
"unclosed environment `{}` (found `\\end{{{}}}`)",
name.as_deref().unwrap_or(""),
end_name.as_deref().unwrap_or("")
),
);
}
}
}
self.close(); }
fn math_environment_body(&mut self) {
self.open(SyntaxKind::MATH);
while !self.at_block_end(Block::Environment) {
self.math_element();
}
self.close(); }
fn verbatim_body(&mut self, name: &str) {
loop {
match self.kind() {
None => break,
Some(SyntaxKind::CONTROL_WORD)
if self.at_command(END_CMD)
&& peek_end_name(self.tokens, self.pos).as_deref() == Some(name) =>
{
break;
}
_ => self.bump(),
}
}
}
fn stray_end(&mut self) {
self.error("`\\end` without matching `\\begin`");
self.open(SyntaxKind::END);
self.bump(); self.name_group();
self.close();
}
fn name_group(&mut self) -> Option<String> {
self.skip_trivia();
if self.kind() != Some(SyntaxKind::L_BRACE) {
self.error("expected `{` for environment name");
return None;
}
self.open(SyntaxKind::NAME_GROUP);
self.bump(); let mut name = String::new();
loop {
match self.kind() {
None => {
self.error("unclosed environment name");
break;
}
Some(SyntaxKind::R_BRACE) => {
self.bump();
break;
}
_ => {
name.push_str(self.text());
self.bump();
}
}
}
self.close();
Some(name.trim().to_owned())
}
}
fn split_math_word(text: &str) -> Option<Vec<(usize, usize)>> {
#[derive(PartialEq, Clone, Copy)]
enum Cls {
Operand,
Sign,
Rel,
}
let classify = |c: char| match c {
'+' | '-' | '*' | '/' => Cls::Sign,
'=' | '<' | '>' => Cls::Rel,
_ => Cls::Operand,
};
let mut pieces = Vec::new();
let mut start = 0;
let mut prev: Option<Cls> = None;
for (i, c) in text.char_indices() {
let cls = classify(c);
let boundary = prev.is_some_and(|p| p != cls || cls == Cls::Sign);
if boundary {
pieces.push((start, i));
start = i;
}
prev = Some(cls);
}
pieces.push((start, text.len()));
(pieces.len() >= 2).then_some(pieces)
}
fn peek_begin_name(tokens: &[Token], begin_pos: usize) -> Option<String> {
peek_end_name(tokens, begin_pos)
}
fn peek_end_name(tokens: &[Token], end_pos: usize) -> Option<String> {
let mut i = end_pos + 1; while tokens.get(i).is_some_and(|t| Parser::is_trivia(t.kind)) {
i += 1;
}
if tokens.get(i).map(|t| t.kind) != Some(SyntaxKind::L_BRACE) {
return None;
}
i += 1;
let mut name = String::new();
while let Some(t) = tokens.get(i) {
if t.kind == SyntaxKind::R_BRACE {
break;
}
name.push_str(&t.text);
i += 1;
}
Some(name.trim().to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::lexer::lex;
#[test]
fn step_guard_trips_when_wedged() {
let tokens = lex("x");
let ctx = VerbCtx::default();
let p = Parser::new(&tokens, &ctx);
p.last_step_pos.set(p.pos);
p.steps.set(PARSER_STEP_LIMIT - 1);
p.step(); let wedged = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| p.step()));
assert!(wedged.is_err(), "the guard must abort a non-advancing loop");
}
#[test]
fn step_budget_resets_on_cursor_progress() {
let tokens = lex("xx");
let ctx = VerbCtx::default();
let mut p = Parser::new(&tokens, &ctx);
p.last_step_pos.set(p.pos);
p.steps.set(PARSER_STEP_LIMIT - 1);
p.pos += 1;
p.step();
assert_eq!(p.steps.get(), 1, "progress should reset the peek budget");
}
}