use crate::parser::core::SyntaxError;
use crate::parser::events::Event;
use crate::parser::lexer::{
ExplToggle, Token, VerbCtx, expl_toggle, 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";
#[derive(Clone, Copy, PartialEq, Eq)]
enum BracketPolicy {
Greedy,
Tight,
Forbid,
}
fn is_big_delimiter_command(text: &str) -> bool {
let Some(name) = text.strip_prefix('\\') else {
return false;
};
["bigg", "Bigg", "big", "Big"].iter().any(|s| {
name.strip_prefix(s)
.is_some_and(|rest| matches!(rest, "" | "l" | "m" | "r"))
})
}
fn is_definition_body_command(text: &str) -> bool {
matches!(
text,
"\\newenvironment"
| "\\renewenvironment"
| "\\provideenvironment"
| "\\NewDocumentEnvironment"
| "\\RenewDocumentEnvironment"
| "\\ProvideDocumentEnvironment"
| "\\DeclareDocumentEnvironment"
| "\\newcommand"
| "\\renewcommand"
| "\\providecommand"
| "\\DeclareRobustCommand"
| "\\NewDocumentCommand"
| "\\RenewDocumentCommand"
| "\\ProvideDocumentCommand"
| "\\DeclareDocumentCommand"
| "\\AtBeginDocument"
| "\\AtEndDocument"
| "\\AtEndOfClass"
| "\\AtEndOfPackage"
| "\\AddToHook"
)
}
pub(crate) fn is_def_prefix_command(text: &str) -> bool {
matches!(text, "\\def" | "\\gdef" | "\\edef" | "\\xdef")
}
const PARSER_STEP_LIMIT: u32 = 15_000_000;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Block {
Document,
Environment,
Macrocode,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum CommentMode {
Skip,
Stop,
}
struct TriviaScan {
next: usize,
next_kind: Option<SyntaxKind>,
saw_blank_line: bool,
saw_blank_line_outside_guards: 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>,
math_depth: usize,
in_def_body: bool,
group_depth: usize,
demoted_envs: std::collections::HashSet<String>,
open_envs: Vec<String>,
group_opens: Vec<usize>,
macrocode_end: Option<usize>,
plain_braces: std::collections::HashSet<usize>,
expl_toggles: Vec<(usize, bool)>,
}
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;
let mut expl_toggles = Vec::new();
for (i, t) in tokens.iter().enumerate() {
starts.push(off);
off += t.text.len();
if t.kind == SyntaxKind::CONTROL_WORD
&& let Some(toggle) = expl_toggle(&t.text)
{
expl_toggles.push((i, toggle == ExplToggle::On));
}
}
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(),
math_depth: 0,
in_def_body: false,
group_depth: 0,
demoted_envs: std::collections::HashSet::new(),
open_envs: Vec::new(),
group_opens: Vec::new(),
macrocode_end: None,
plain_braces: std::collections::HashSet::new(),
expl_toggles,
}
}
fn in_expl_region(&self, idx: usize) -> bool {
let n = self.expl_toggles.partition_point(|&(i, _)| i < idx);
n > 0 && self.expl_toggles[n - 1].1
}
fn on_doc_margin_line(&self, idx: usize) -> bool {
self.tokens[..idx]
.iter()
.rev()
.take_while(|t| t.kind != SyntaxKind::NEWLINE)
.any(|t| t.kind == SyntaxKind::DOC_MARGIN)
}
fn doc_margin_exempt(&self, idx: usize) -> bool {
self.on_doc_margin_line(idx)
&& !self
.group_opens
.last()
.is_some_and(|&brace| self.on_doc_margin_line(brace))
}
fn end_orphans_a_demoted_begin(&self, idx: usize) -> bool {
if self.demoted_envs.is_empty() {
return false;
}
peek_end_name(self.tokens, idx).is_some_and(|name| {
self.demoted_envs.contains(&name) && !self.open_envs.contains(&name)
})
}
fn in_macro_code(&self, idx: usize) -> bool {
self.in_def_body || (self.in_expl_region(idx) && !self.on_doc_margin_line(idx))
}
fn in_math(&self) -> bool {
self.math_depth > 0
}
#[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 env_name_follows(&self, pos: usize) -> bool {
let s = self.scan_trivia(pos + 1, CommentMode::Skip);
if s.saw_blank_line || s.next_kind != Some(SyntaxKind::L_BRACE) {
return false;
}
for t in &self.tokens[s.next + 1..] {
match t.kind {
SyntaxKind::R_BRACE | SyntaxKind::NEWLINE => return true,
SyntaxKind::HASH
| SyntaxKind::CONTROL_WORD
| SyntaxKind::CONTROL_SYMBOL
| SyntaxKind::L_BRACE => return false,
_ => {}
}
}
true
}
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 guard_newlines = 0;
let mut saw_blank_line = false;
let mut saw_blank_line_outside_guards = false;
let mut comment_start = None;
while let Some(t) = self.tokens.get(i) {
match t.kind {
SyntaxKind::NEWLINE => {
newlines += 1;
guard_newlines += 1;
if newlines >= 2 {
saw_blank_line = true;
comment_start = None;
}
if guard_newlines >= 2 {
saw_blank_line_outside_guards = true;
}
}
SyntaxKind::WHITESPACE | SyntaxKind::DOC_MARGIN => {}
SyntaxKind::GUARD => guard_newlines = 0,
SyntaxKind::COMMENT if comment_mode == CommentMode::Stop => break,
SyntaxKind::COMMENT => {
newlines = 0;
guard_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,
saw_blank_line_outside_guards,
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 at_paragraph_break_outside_guards(&self) -> bool {
self.scan_trivia(self.pos, CommentMode::Skip)
.saw_blank_line_outside_guards
}
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()
|| match block {
Block::Document => false,
Block::Environment => {
self.at_command(END_CMD)
&& self.env_name_follows(self.pos)
&& !self.end_orphans_a_demoted_begin(self.pos)
}
Block::Macrocode => self.macrocode_end.is_some_and(|end| self.pos >= end),
}
}
fn trivia_run_is_separator(&self, block: Block) -> bool {
let s = self.scan_trivia(self.pos, CommentMode::Skip);
if s.saw_blank_line {
return true;
}
if block == Block::Macrocode {
return s.next_kind.is_none() || self.macrocode_end.is_some_and(|end| s.next >= end);
}
match s.next_kind {
None => true,
Some(SyntaxKind::CONTROL_WORD) => {
block == Block::Environment
&& self.tokens[s.next].text == END_CMD
&& self.env_name_follows(s.next)
}
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.in_macro_code(self.pos)
&& self.at_command(BEGIN_CMD)
&& self.env_name_follows(self.pos)
{
if self.environment_escapes_group(self.pos) {
if let Some(name) = peek_end_name(self.tokens, self.pos) {
self.demoted_envs.insert(name);
}
self.command();
} else {
self.environment();
}
} else if !self.in_macro_code(self.pos)
&& self.at_command(END_CMD)
&& self.env_name_follows(self.pos)
{
if (self.group_depth > 0 && !self.doc_margin_exempt(self.pos))
|| self.end_orphans_a_demoted_begin(self.pos)
{
self.command();
} else {
self.stray_end();
}
} else {
self.command();
}
}
SyntaxKind::CONTROL_SYMBOL => {
let sym = self.text().to_owned();
match sym.as_str() {
"\\[" => {
if self.delim_math_closes(self.pos, "\\]") {
self.delim_math(SyntaxKind::DISPLAY_MATH, "\\[", "\\]");
} else {
self.bump();
}
}
"\\(" => {
if self.delim_math_closes(self.pos, "\\)") {
self.delim_math(SyntaxKind::INLINE_MATH, "\\(", "\\)");
} else {
self.bump();
}
}
"\\]" | "\\)" => {
if !self.in_macro_code(self.pos) {
self.error(format!("unmatched `{sym}`"));
}
self.bump();
}
"\\\\" => self.line_break(),
_ => self.bump(),
}
}
SyntaxKind::L_BRACE => {
if self.plain_braces.contains(&self.pos) {
self.bump();
} else {
self.group();
}
}
SyntaxKind::R_BRACE => {
if !self.plain_braces.contains(&self.pos) {
self.error("unmatched `}`");
}
self.bump();
}
SyntaxKind::DOLLAR => {
let display = self.nth_kind(1) == Some(SyntaxKind::DOLLAR);
if self.dollar_closes(self.pos, display) {
self.dollar_math();
} else {
self.bump();
}
}
_ => self.bump(),
}
}
fn command(&mut self) {
let bracket = if is_big_delimiter_command(self.text()) {
BracketPolicy::Forbid
} else {
BracketPolicy::Greedy
};
let saved = self.in_def_body;
self.in_def_body = saved || is_definition_body_command(self.text());
let def_prefix = is_def_prefix_command(self.text());
self.open(SyntaxKind::COMMAND);
self.bump(); if def_prefix {
let scan = self.scan_trivia(self.pos, CommentMode::Skip);
if scan.next_kind == Some(SyntaxKind::CONTROL_SYMBOL) && !scan.saw_blank_line {
self.skip_trivia();
self.bump(); self.in_def_body = true;
}
}
self.attach_arguments(bracket);
self.in_def_body = saved;
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, bracket: BracketPolicy) {
loop {
let (next, paragraph_break) = self.peek_meaningful();
if paragraph_break {
break;
}
match next {
Some(SyntaxKind::L_BRACE) => {
let scan = self.scan_trivia(self.pos, CommentMode::Skip);
if self.plain_braces.contains(&scan.next) {
break;
}
self.skip_trivia();
self.group();
}
Some(SyntaxKind::L_BRACKET) => {
if bracket == BracketPolicy::Forbid {
break;
}
let scan = self.scan_trivia(self.pos, CommentMode::Skip);
let tight_only = self.in_math() || bracket == BracketPolicy::Tight;
if tight_only && scan.next != self.pos {
break;
}
if self.in_math() && !self.bracket_closes_before_math_end(scan.next) {
break;
}
if self.macrocode_end.is_some()
&& !self.bracket_closes_before_macrocode_end(scan.next)
{
break;
}
if !self.in_math()
&& self.macrocode_end.is_none()
&& !self.bracket_closes_in_text(scan.next)
{
break;
}
self.skip_trivia();
self.optional();
}
Some(SyntaxKind::VERB)
if self.scan_trivia(self.pos, CommentMode::Skip).next == self.pos
&& !self
.peek_meaningful_text()
.is_some_and(|t| t.starts_with('\\')) =>
{
self.bump(); }
Some(SyntaxKind::WORD) if self.at_star_variant_marker() => {
self.bump(); }
_ => break,
}
}
}
fn at_star_variant_marker(&self) -> bool {
if self.scan_trivia(self.pos, CommentMode::Skip).next != self.pos {
return false; }
if self.tokens.get(self.pos).map(|t| (t.kind, t.text.as_str()))
!= Some((SyntaxKind::WORD, "*"))
{
return false;
}
matches!(
self.scan_trivia(self.pos + 1, CommentMode::Skip).next_kind,
Some(SyntaxKind::L_BRACKET | SyntaxKind::L_BRACE)
)
}
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(); self.group_depth += 1;
self.group_opens.push(self.pos - 1);
loop {
match self.kind() {
None => {
self.error_at(opener, "unclosed `{`");
break;
}
Some(SyntaxKind::R_BRACE) => {
self.bump();
break;
}
_ => self.element(),
}
}
self.group_depth -= 1;
self.group_opens.pop();
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.in_macro_code(self.pos)
&& (self.at_command(BEGIN_CMD) || self.at_command(END_CMD))
&& self.env_name_follows(self.pos) =>
{
self.error_at(opener, "unclosed `[`");
break;
}
_ => {
if self.at_paragraph_break_outside_guards()
|| self.macrocode_end.is_some_and(|end| self.pos >= end)
{
self.error_at(opener, "unclosed `[`");
break;
}
self.element();
}
}
}
self.close();
}
fn bracket_closes_before_macrocode_end(&self, open: usize) -> bool {
let Some(end) = self.macrocode_end else {
return true;
};
let mut depth = 0usize;
let mut newline_run = 0;
for (off, t) in self.tokens[open + 1..end.min(self.tokens.len())]
.iter()
.enumerate()
{
let idx = open + 1 + off;
match t.kind {
SyntaxKind::NEWLINE => {
newline_run += 1;
if newline_run >= 2 {
return false;
}
continue;
}
SyntaxKind::WHITESPACE => continue,
SyntaxKind::L_BRACE if !self.plain_braces.contains(&idx) => depth += 1,
SyntaxKind::R_BRACE if !self.plain_braces.contains(&idx) => {
if depth == 0 {
return false;
}
depth -= 1;
}
SyntaxKind::R_BRACKET if depth == 0 => return true,
_ => {}
}
newline_run = 0;
}
false
}
fn bracket_closes_before_math_end(&self, open: usize) -> bool {
let mut depth = 0usize;
let mut brackets = 0usize;
let mut in_inline = false;
let mut newlines = 0;
let mut abuts_command = false;
for (off, t) in self.tokens[open + 1..].iter().enumerate() {
let idx = open + 1 + off;
let prev_abuts_command = abuts_command;
abuts_command = false;
match t.kind {
SyntaxKind::NEWLINE => {
newlines += 1;
if newlines >= 2 {
return false;
}
continue;
}
SyntaxKind::WHITESPACE | SyntaxKind::DOC_MARGIN | SyntaxKind::GUARD => continue,
SyntaxKind::L_BRACE => depth += 1,
SyntaxKind::R_BRACE => {
if depth == 0 {
return false;
}
depth -= 1;
}
SyntaxKind::DOLLAR if depth == 0 => in_inline = !in_inline,
SyntaxKind::L_BRACKET if depth == 0 && !in_inline && prev_abuts_command => {
brackets += 1
}
SyntaxKind::R_BRACKET if depth == 0 && !in_inline => {
if brackets == 0 {
return true;
}
brackets -= 1;
}
SyntaxKind::CONTROL_SYMBOL if matches!(t.text.as_str(), "\\]" | "\\)") => {
return false;
}
SyntaxKind::CONTROL_WORD
if matches!(t.text.as_str(), BEGIN_CMD | END_CMD)
&& self.env_name_follows(idx) =>
{
return false;
}
SyntaxKind::CONTROL_WORD | SyntaxKind::CONTROL_SYMBOL => abuts_command = true,
_ => {}
}
newlines = 0;
}
false
}
fn bracket_closes_in_text(&self, open: usize) -> bool {
let mut depth = 0usize;
let mut brackets = 0usize;
let mut newlines = 0;
let mut abuts_command = false;
for (off, t) in self.tokens[open + 1..].iter().enumerate() {
let idx = open + 1 + off;
let prev_abuts_command = abuts_command;
abuts_command = false;
match t.kind {
SyntaxKind::NEWLINE => {
newlines += 1;
if newlines >= 2 {
return false;
}
continue;
}
SyntaxKind::WHITESPACE | SyntaxKind::DOC_MARGIN | SyntaxKind::GUARD => continue,
SyntaxKind::L_BRACE if !self.plain_braces.contains(&idx) => depth += 1,
SyntaxKind::R_BRACE if !self.plain_braces.contains(&idx) => {
if depth == 0 {
return false;
}
depth -= 1;
}
SyntaxKind::L_BRACKET if depth == 0 && prev_abuts_command => brackets += 1,
SyntaxKind::R_BRACKET if depth == 0 => {
if brackets == 0 {
return true;
}
brackets -= 1;
}
SyntaxKind::CONTROL_WORD
if !self.in_macro_code(idx)
&& matches!(t.text.as_str(), BEGIN_CMD | END_CMD)
&& self.env_name_follows(idx) =>
{
return false;
}
SyntaxKind::CONTROL_WORD | SyntaxKind::CONTROL_SYMBOL => abuts_command = true,
_ => {}
}
newlines = 0;
}
false
}
fn paragraph_break_blocks(depth: usize, envs: usize) -> bool {
depth == 0 && envs == 0
}
fn dollar_closes(&self, open: usize, display: bool) -> bool {
let mut depth = 0usize;
let mut envs = 0usize;
let mut newlines = 0;
let start = open + if display { 2 } else { 1 };
let end = self
.macrocode_end
.unwrap_or(self.tokens.len())
.min(self.tokens.len());
let mut i = start;
while i < end {
let t = &self.tokens[i];
match t.kind {
SyntaxKind::NEWLINE => {
newlines += 1;
if newlines >= 2 && Self::paragraph_break_blocks(depth, envs) {
return false;
}
i += 1;
continue;
}
SyntaxKind::WHITESPACE | SyntaxKind::DOC_MARGIN | SyntaxKind::GUARD => {
i += 1;
continue;
}
SyntaxKind::L_BRACE if !self.plain_braces.contains(&i) => depth += 1,
SyntaxKind::R_BRACE if !self.plain_braces.contains(&i) => {
if depth == 0 {
return false;
}
depth -= 1;
}
SyntaxKind::DOLLAR if depth == 0 => {
if !display
|| self.tokens.get(i + 1).map(|t| t.kind) == Some(SyntaxKind::DOLLAR)
{
return true;
}
}
SyntaxKind::CONTROL_WORD if !self.in_macro_code(i) => {
if t.text.as_str() == BEGIN_CMD && self.env_name_follows(i) {
envs += 1;
} else if t.text.as_str() == END_CMD && self.env_name_follows(i) {
if envs == 0 {
return false;
}
envs -= 1;
}
}
_ => {}
}
newlines = 0;
i += 1;
}
false
}
fn delim_math_closes(&self, open: usize, closer: &str) -> bool {
let mut depth = 0usize;
let mut envs = 0usize;
let mut newlines = 0;
let end = self
.macrocode_end
.unwrap_or(self.tokens.len())
.min(self.tokens.len());
let mut i = open + 1;
while i < end {
let t = &self.tokens[i];
match t.kind {
SyntaxKind::NEWLINE => {
newlines += 1;
if newlines >= 2 && Self::paragraph_break_blocks(depth, envs) {
return false;
}
i += 1;
continue;
}
SyntaxKind::WHITESPACE | SyntaxKind::DOC_MARGIN | SyntaxKind::GUARD => {
i += 1;
continue;
}
SyntaxKind::L_BRACE if !self.plain_braces.contains(&i) => depth += 1,
SyntaxKind::R_BRACE if !self.plain_braces.contains(&i) => {
if depth == 0 {
return false;
}
depth -= 1;
}
SyntaxKind::CONTROL_SYMBOL if depth == 0 && t.text.as_str() == closer => {
return true;
}
SyntaxKind::CONTROL_WORD if !self.in_macro_code(i) => {
if t.text.as_str() == BEGIN_CMD && self.env_name_follows(i) {
envs += 1;
} else if t.text.as_str() == END_CMD && self.env_name_follows(i) {
if envs == 0 {
return false;
}
envs -= 1;
}
}
_ => {}
}
newlines = 0;
i += 1;
}
false
}
fn left_right_closes(&self, open: usize) -> bool {
#[derive(PartialEq)]
enum Ctx {
Brace,
Env,
Left,
}
let mut stack: Vec<Ctx> = Vec::new();
let mut newlines = 0;
let end = self
.macrocode_end
.unwrap_or(self.tokens.len())
.min(self.tokens.len());
let mut i = open + 1;
while i < end {
let t = &self.tokens[i];
if stack.last() == Some(&Ctx::Brace) {
match t.kind {
SyntaxKind::L_BRACE if !self.plain_braces.contains(&i) => {
stack.push(Ctx::Brace)
}
SyntaxKind::R_BRACE if !self.plain_braces.contains(&i) => {
stack.pop();
}
_ => {}
}
i += 1;
continue;
}
match t.kind {
SyntaxKind::NEWLINE => {
newlines += 1;
if newlines >= 2 && stack.is_empty() {
return false;
}
i += 1;
continue;
}
SyntaxKind::WHITESPACE | SyntaxKind::DOC_MARGIN | SyntaxKind::GUARD => {
i += 1;
continue;
}
SyntaxKind::L_BRACE if !self.plain_braces.contains(&i) => stack.push(Ctx::Brace),
SyntaxKind::R_BRACE if !self.plain_braces.contains(&i) => return false,
SyntaxKind::DOLLAR => return false,
SyntaxKind::CONTROL_SYMBOL if matches!(t.text.as_str(), "\\]" | "\\)") => {
return false;
}
SyntaxKind::CONTROL_WORD if !self.in_macro_code(i) => match t.text.as_str() {
LEFT_CMD => stack.push(Ctx::Left),
RIGHT_CMD => match stack.last() {
None => return true,
Some(Ctx::Left) => {
stack.pop();
}
_ => return false,
},
BEGIN_CMD if self.env_name_follows(i) => stack.push(Ctx::Env),
END_CMD if self.env_name_follows(i) => match stack.last() {
Some(Ctx::Env) => {
stack.pop();
}
_ => return false,
},
_ => {}
},
_ => {}
}
newlines = 0;
i += 1;
}
false
}
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);
self.math_depth += 1;
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.env_name_follows(self.pos) =>
{
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}` (a blank line ends math)"),
);
break;
}
self.math_element();
}
}
}
self.math_depth -= 1;
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);
self.math_depth += 1;
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.env_name_follows(self.pos) =>
{
self.error_at(opener_span, format!("unclosed `{opener}`"));
break;
}
_ => {
if self.at_paragraph_break() {
self.error_at(
opener_span,
format!("unclosed `{opener}` (a blank line ends math)"),
);
break;
}
self.math_element();
}
}
}
self.math_depth -= 1;
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.in_macro_code(self.pos)
&& self.at_command(BEGIN_CMD)
&& self.env_name_follows(self.pos)
{
self.environment();
} else if !self.in_macro_code(self.pos)
&& self.at_command(END_CMD)
&& self.env_name_follows(self.pos)
{
self.stray_end();
} else if self.at_command(LEFT_CMD) && self.left_right_closes(self.pos) {
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) && self.env_name_follows(self.pos)
}
_ => 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(); self.group_depth += 1;
self.group_opens.push(self.pos - 1);
loop {
match self.kind() {
None => {
self.error_at(opener, "unclosed `{`");
break;
}
Some(SyntaxKind::R_BRACE) => {
self.bump();
break;
}
_ => self.math_element(),
}
}
self.group_depth -= 1;
self.group_opens.pop();
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.env_name_follows(self.pos) =>
{
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.env_name_follows(self.pos))
|| 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_escapes_group(&self, open: usize) -> bool {
if self.group_depth == 0 {
return false;
}
if self.doc_margin_exempt(open) {
return false;
}
let mut depth = 0usize;
let mut envs = 0usize;
let end = self
.macrocode_end
.unwrap_or(self.tokens.len())
.min(self.tokens.len());
let mut i = open + 1;
while i < end {
let t = &self.tokens[i];
match t.kind {
SyntaxKind::L_BRACE if !self.plain_braces.contains(&i) => depth += 1,
SyntaxKind::R_BRACE if !self.plain_braces.contains(&i) => {
if depth == 0 {
return true;
}
depth -= 1;
}
SyntaxKind::CONTROL_WORD if depth == 0 && !self.in_macro_code(i) => {
if t.text.as_str() == BEGIN_CMD && self.env_name_follows(i) {
envs += 1;
} else if t.text.as_str() == END_CMD && self.env_name_follows(i) {
if envs == 0 {
return false;
}
envs -= 1;
}
}
_ => {}
}
i += 1;
}
false
}
fn environment(&mut self) {
self.open(SyntaxKind::ENVIRONMENT);
let begin_pos = self.pos;
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]);
let macrocode_frame = name
.as_deref()
.is_some_and(|n| matches!(n, "macrocode" | "macrocode*"))
&& self.frame_margin_before(begin_pos);
let bracket = if name.as_deref().is_some_and(is_math_environment) {
BracketPolicy::Tight
} else {
BracketPolicy::Greedy
};
if !macrocode_frame {
self.attach_arguments(bracket);
}
self.close();
if let Some(open) = name.as_deref() {
self.open_envs.push(open.to_owned());
}
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 if macrocode_frame {
self.macrocode_body(name.as_deref().expect("macrocode name"));
} else {
self.parse_block(Block::Environment);
}
if name.is_some() {
self.open_envs.pop();
}
self.finish_environment(&name, opener);
}
fn frame_margin_before(&self, pos: usize) -> bool {
let mut i = pos;
while i > 0 {
i -= 1;
match self.tokens[i].kind {
SyntaxKind::WHITESPACE => continue,
SyntaxKind::DOC_MARGIN => return true,
_ => return false,
}
}
false
}
fn macrocode_body(&mut self, name: &str) {
let mut end = self.tokens.len();
for i in self.pos..self.tokens.len() {
if self.tokens[i].kind == SyntaxKind::CONTROL_WORD
&& self.tokens[i].text == END_CMD
&& self.frame_margin_before(i)
&& peek_end_name(self.tokens, i).as_deref() == Some(name)
{
end = i;
break;
}
}
let saved_plain = std::mem::take(&mut self.plain_braces);
let saved_end = self.macrocode_end;
let saved_def = self.in_def_body;
let mut open_stack = Vec::new();
for i in self.pos..end {
match self.tokens[i].kind {
SyntaxKind::L_BRACE => open_stack.push(i),
SyntaxKind::R_BRACE if open_stack.pop().is_none() => {
self.plain_braces.insert(i);
}
_ => {}
}
}
self.plain_braces.extend(open_stack);
self.macrocode_end = Some(end);
self.in_def_body = true;
self.parse_block(Block::Macrocode);
self.plain_braces = saved_plain;
self.macrocode_end = saved_end;
self.in_def_body = saved_def;
}
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);
self.math_depth += 1;
while !self.at_block_end(Block::Environment) {
self.math_element();
}
self.math_depth -= 1;
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");
}
}