mod facts;
mod prescan;
mod trivia;
use std::borrow::Cow;
use crate::parser::conditional;
use crate::parser::core::SyntaxError;
use crate::parser::events::Event;
use crate::parser::lexer::{ParseCtx, Token};
use crate::syntax::SyntaxKind;
use facts::{BracketPolicy, is_big_delimiter_command, is_definition_body_command};
use prescan::PreScan;
use smol_str::SmolStr;
use trivia::{BLANK_LINE_NEWLINES, CommentMode};
pub use facts::is_def_prefix_command;
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,
Macrocode,
}
pub(crate) fn parse(tokens: &[Token], ctx: &ParseCtx) -> (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"
);
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct WalkKey {
macrocode_end: Option<usize>,
in_def_body: bool,
in_group: bool,
plain_braces: u32,
enclosing_math_is_dollar: bool,
}
struct GateBatch {
key: WalkKey,
verdicts: std::collections::HashMap<usize, Option<usize>>,
}
trait VerdictSink {
fn insert(&mut self, opener: usize, verdict: Option<usize>);
}
impl VerdictSink for std::collections::HashMap<usize, Option<usize>> {
fn insert(&mut self, opener: usize, verdict: Option<usize>) {
std::collections::HashMap::insert(self, opener, verdict);
}
}
struct SeedVerdict {
seed: usize,
verdict: Option<Option<usize>>,
}
impl VerdictSink for SeedVerdict {
fn insert(&mut self, opener: usize, verdict: Option<usize>) {
if opener == self.seed {
self.verdict = Some(verdict);
}
}
}
#[derive(PartialEq, Eq)]
enum StrayBrace {
RefutesInGroup,
ClosesInGroup,
RefutesAlways,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum MathAnchor {
None,
Opening,
Closing,
}
impl MathAnchor {
fn anchors(self, text: &str) -> bool {
match self {
MathAnchor::None => false,
MathAnchor::Opening => matches!(text, "\\[" | "\\("),
MathAnchor::Closing => matches!(text, "\\]" | "\\)"),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum DollarAnchor {
Content,
Refutes,
Transparent,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ParagraphAnchor {
None,
OwnLevel,
AnyDepth,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum EnvAnchor {
Counts,
Refutes,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Nesting {
Counted,
Interleaved,
}
trait GatePolicy {
const PARAGRAPH_ANCHOR: ParagraphAnchor;
const STRAY_BRACE: StrayBrace = StrayBrace::RefutesInGroup;
const PLAIN_BRACES_ARE_TOKENS: bool = true;
const MATH_ANCHOR: MathAnchor = MathAnchor::Opening;
const NESTING: Nesting = Nesting::Counted;
const OPENER_IS_ENV_BEGIN: bool = false;
const ANCHORS_AT_ANY_DEPTH: bool = false;
const ENV_ANCHOR: EnvAnchor = EnvAnchor::Counts;
const ENV_ANCHOR_IN_MACRO_CODE: bool = false;
const CLOSER_NEEDS_ENV_BALANCE: bool = true;
const MACROCODE_FRAME_ANCHORS: bool = true;
fn dollar_anchor(&self) -> DollarAnchor {
if Self::MATH_ANCHOR == MathAnchor::None {
DollarAnchor::Content
} else {
DollarAnchor::Refutes
}
}
fn last_closer(&self, p: &Parser<'_>) -> Option<usize>;
fn opens_at(&self, p: &Parser<'_>, i: usize) -> bool;
fn closes_at(&self, p: &Parser<'_>, i: usize) -> bool;
fn pairs(&self, p: &Parser<'_>, opener: usize, closer: usize) -> bool {
let _ = (p, opener, closer);
true
}
}
struct ConditionalGate;
impl GatePolicy for ConditionalGate {
const PARAGRAPH_ANCHOR: ParagraphAnchor = ParagraphAnchor::OwnLevel;
fn last_closer(&self, p: &Parser<'_>) -> Option<usize> {
p.last_fi
}
fn opens_at(&self, p: &Parser<'_>, i: usize) -> bool {
p.conditional_openers.contains(&i)
}
fn closes_at(&self, p: &Parser<'_>, i: usize) -> bool {
p.conditional_flow_at(i) == Some(conditional::FlowWord::Fi)
}
}
struct AliasGate;
impl GatePolicy for AliasGate {
const PARAGRAPH_ANCHOR: ParagraphAnchor = ParagraphAnchor::None;
fn last_closer(&self, p: &Parser<'_>) -> Option<usize> {
p.last_alias_closer
}
fn opens_at(&self, p: &Parser<'_>, i: usize) -> bool {
p.alias_openers.contains_key(&i) && !p.in_macro_code(i)
}
fn closes_at(&self, p: &Parser<'_>, i: usize) -> bool {
p.alias_closers.contains_key(&i) && !p.in_macro_code(i)
}
fn pairs(&self, p: &Parser<'_>, opener: usize, closer: usize) -> bool {
p.alias_closers.get(&closer) == p.alias_openers.get(&opener)
}
}
struct EnvGate;
impl GatePolicy for EnvGate {
const PARAGRAPH_ANCHOR: ParagraphAnchor = ParagraphAnchor::None;
const STRAY_BRACE: StrayBrace = StrayBrace::ClosesInGroup;
const MATH_ANCHOR: MathAnchor = MathAnchor::None;
const OPENER_IS_ENV_BEGIN: bool = true;
fn last_closer(&self, p: &Parser<'_>) -> Option<usize> {
p.last_r_brace
}
fn opens_at(&self, p: &Parser<'_>, i: usize) -> bool {
p.env_begin_at(i) && !p.in_macro_code(i)
}
fn closes_at(&self, _p: &Parser<'_>, _i: usize) -> bool {
false
}
}
struct DelimMathGate {
closer: &'static str,
}
impl GatePolicy for DelimMathGate {
const PARAGRAPH_ANCHOR: ParagraphAnchor = ParagraphAnchor::OwnLevel;
const STRAY_BRACE: StrayBrace = StrayBrace::RefutesAlways;
const MATH_ANCHOR: MathAnchor = MathAnchor::None;
const ANCHORS_AT_ANY_DEPTH: bool = true;
const CLOSER_NEEDS_ENV_BALANCE: bool = false;
const MACROCODE_FRAME_ANCHORS: bool = false;
fn last_closer(&self, p: &Parser<'_>) -> Option<usize> {
if self.closer == "\\]" {
p.last_display_math_closer
} else {
p.last_inline_math_closer
}
}
fn opens_at(&self, _p: &Parser<'_>, _i: usize) -> bool {
false
}
fn closes_at(&self, p: &Parser<'_>, i: usize) -> bool {
let t = &p.tokens[i];
t.kind == SyntaxKind::CONTROL_SYMBOL && t.text.as_str() == self.closer
}
}
struct DollarGate {
display: bool,
}
impl GatePolicy for DollarGate {
const PARAGRAPH_ANCHOR: ParagraphAnchor = ParagraphAnchor::OwnLevel;
const STRAY_BRACE: StrayBrace = StrayBrace::RefutesAlways;
const MATH_ANCHOR: MathAnchor = MathAnchor::None;
const ANCHORS_AT_ANY_DEPTH: bool = true;
const CLOSER_NEEDS_ENV_BALANCE: bool = false;
const MACROCODE_FRAME_ANCHORS: bool = false;
fn last_closer(&self, p: &Parser<'_>) -> Option<usize> {
p.last_dollar
}
fn opens_at(&self, _p: &Parser<'_>, _i: usize) -> bool {
false
}
fn closes_at(&self, p: &Parser<'_>, i: usize) -> bool {
p.tokens[i].kind == SyntaxKind::DOLLAR
&& (!self.display || p.tokens.get(i + 1).map(|t| t.kind) == Some(SyntaxKind::DOLLAR))
}
}
struct LeftRightGate;
impl GatePolicy for LeftRightGate {
const PARAGRAPH_ANCHOR: ParagraphAnchor = ParagraphAnchor::OwnLevel;
const STRAY_BRACE: StrayBrace = StrayBrace::RefutesAlways;
const MATH_ANCHOR: MathAnchor = MathAnchor::Closing;
const NESTING: Nesting = Nesting::Interleaved;
const MACROCODE_FRAME_ANCHORS: bool = false;
fn last_closer(&self, p: &Parser<'_>) -> Option<usize> {
p.last_right
}
fn opens_at(&self, p: &Parser<'_>, i: usize) -> bool {
let t = &p.tokens[i];
t.kind == SyntaxKind::CONTROL_WORD && t.text.as_str() == LEFT_CMD
}
fn closes_at(&self, p: &Parser<'_>, i: usize) -> bool {
let t = &p.tokens[i];
t.kind == SyntaxKind::CONTROL_WORD && t.text.as_str() == RIGHT_CMD
}
}
struct TextBracketGate;
impl GatePolicy for TextBracketGate {
const PARAGRAPH_ANCHOR: ParagraphAnchor = ParagraphAnchor::AnyDepth;
const STRAY_BRACE: StrayBrace = StrayBrace::RefutesAlways;
const MATH_ANCHOR: MathAnchor = MathAnchor::None;
const ANCHORS_AT_ANY_DEPTH: bool = true;
const ENV_ANCHOR: EnvAnchor = EnvAnchor::Refutes;
fn last_closer(&self, p: &Parser<'_>) -> Option<usize> {
p.last_r_bracket
}
fn opens_at(&self, p: &Parser<'_>, i: usize) -> bool {
p.bracket_abuts_command(i)
}
fn closes_at(&self, p: &Parser<'_>, i: usize) -> bool {
p.tokens[i].kind == SyntaxKind::R_BRACKET
}
}
struct MathBracketGate {
enclosing_is_dollar: bool,
}
impl GatePolicy for MathBracketGate {
const PARAGRAPH_ANCHOR: ParagraphAnchor = ParagraphAnchor::AnyDepth;
const STRAY_BRACE: StrayBrace = StrayBrace::RefutesAlways;
const MATH_ANCHOR: MathAnchor = MathAnchor::Closing;
const ANCHORS_AT_ANY_DEPTH: bool = true;
const ENV_ANCHOR: EnvAnchor = EnvAnchor::Refutes;
const ENV_ANCHOR_IN_MACRO_CODE: bool = true;
const PLAIN_BRACES_ARE_TOKENS: bool = false;
fn dollar_anchor(&self) -> DollarAnchor {
if self.enclosing_is_dollar {
DollarAnchor::Refutes
} else {
DollarAnchor::Transparent
}
}
fn last_closer(&self, p: &Parser<'_>) -> Option<usize> {
p.last_r_bracket
}
fn opens_at(&self, p: &Parser<'_>, i: usize) -> bool {
p.bracket_abuts_command(i)
}
fn closes_at(&self, p: &Parser<'_>, i: usize) -> bool {
p.tokens[i].kind == SyntaxKind::R_BRACKET
}
}
struct MacrocodeBracketGate;
impl GatePolicy for MacrocodeBracketGate {
const PARAGRAPH_ANCHOR: ParagraphAnchor = ParagraphAnchor::AnyDepth;
const STRAY_BRACE: StrayBrace = StrayBrace::RefutesAlways;
const MATH_ANCHOR: MathAnchor = MathAnchor::None;
const ANCHORS_AT_ANY_DEPTH: bool = true;
const ENV_ANCHOR: EnvAnchor = EnvAnchor::Refutes;
fn last_closer(&self, p: &Parser<'_>) -> Option<usize> {
p.last_r_bracket
}
fn opens_at(&self, _p: &Parser<'_>, _i: usize) -> bool {
false
}
fn closes_at(&self, p: &Parser<'_>, i: usize) -> bool {
p.tokens[i].kind == SyntaxKind::R_BRACKET
}
}
struct Parser<'t> {
tokens: &'t [Token],
ctx: &'t ParseCtx,
starts: Vec<usize>,
pos: usize,
events: Vec<Event>,
errors: Vec<SyntaxError>,
steps: std::cell::Cell<u32>,
last_step_pos: std::cell::Cell<usize>,
math_dollar: Vec<bool>,
in_def_body: bool,
demoted_envs: std::collections::HashSet<String>,
open_envs: Vec<String>,
group_opens: Vec<usize>,
macrocode_end: Option<usize>,
plain_braces: std::collections::HashSet<usize>,
plain_braces_version: u32,
expl_toggles: Vec<(usize, bool)>,
doc_margin_lines: Vec<(usize, usize)>,
conditional_openers: std::collections::HashSet<usize>,
alias_openers: std::collections::HashMap<usize, SmolStr>,
alias_closers: std::collections::HashMap<usize, SmolStr>,
last_alias_closer: Option<usize>,
last_r_bracket: Option<usize>,
last_display_math_closer: Option<usize>,
last_inline_math_closer: Option<usize>,
last_right: Option<usize>,
last_r_brace: Option<usize>,
last_fi: Option<usize>,
last_dollar: Option<usize>,
conditional_batch: std::cell::RefCell<Option<GateBatch>>,
#[cfg(test)]
scan_work: std::cell::Cell<usize>,
env_batch: std::cell::RefCell<Option<GateBatch>>,
alias_batch: std::cell::RefCell<Option<GateBatch>>,
left_right_batch: std::cell::RefCell<Option<GateBatch>>,
text_bracket_batch: std::cell::RefCell<Option<GateBatch>>,
math_bracket_batch: std::cell::RefCell<Option<GateBatch>>,
alias_end: Option<usize>,
}
impl<'t> Parser<'t> {
fn new(tokens: &'t [Token], ctx: &'t ParseCtx) -> Self {
let pre = PreScan::run(tokens, ctx);
Self {
tokens,
ctx,
starts: pre.starts,
pos: 0,
events: Vec::new(),
steps: std::cell::Cell::new(0),
last_step_pos: std::cell::Cell::new(0),
errors: Vec::new(),
math_dollar: Vec::new(),
in_def_body: false,
demoted_envs: std::collections::HashSet::new(),
open_envs: Vec::new(),
group_opens: Vec::new(),
macrocode_end: None,
plain_braces: std::collections::HashSet::new(),
plain_braces_version: 0,
expl_toggles: pre.expl_toggles,
doc_margin_lines: pre.doc_margin_lines,
conditional_openers: pre.conditional_openers,
last_alias_closer: pre.alias_closers.keys().copied().max(),
last_r_bracket: pre.last_r_bracket,
last_display_math_closer: pre.last_display_math_closer,
last_inline_math_closer: pre.last_inline_math_closer,
last_right: pre.last_right,
last_r_brace: pre.last_r_brace,
last_fi: pre.last_fi,
last_dollar: pre.last_dollar,
conditional_batch: std::cell::RefCell::new(None),
#[cfg(test)]
scan_work: std::cell::Cell::new(0),
alias_openers: pre.alias_openers,
alias_closers: pre.alias_closers,
alias_batch: std::cell::RefCell::new(None),
env_batch: std::cell::RefCell::new(None),
left_right_batch: std::cell::RefCell::new(None),
text_bracket_batch: std::cell::RefCell::new(None),
math_bracket_batch: std::cell::RefCell::new(None),
alias_end: None,
}
}
fn conditional_flow_at(&self, idx: usize) -> Option<conditional::FlowWord> {
let t = self.tokens.get(idx)?;
if t.kind != SyntaxKind::CONTROL_WORD || self.in_expl_region(idx) {
return None;
}
t.text.strip_prefix('\\').and_then(conditional::flow_word)
}
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 {
let n = self.doc_margin_lines.partition_point(|&(m, _)| m < idx);
n > 0 && self.doc_margin_lines[n - 1].1 >= idx
}
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.as_ref())
&& !self.open_envs.iter().any(|open| open == name.as_ref())
})
}
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_dollar.is_empty()
}
fn in_group(&self) -> bool {
!self.group_opens.is_empty()
}
#[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 at_env_begin(&self) -> bool {
self.at_command(BEGIN_CMD) && self.env_name_follows(self.pos)
}
fn at_env_end(&self) -> bool {
self.at_command(END_CMD) && self.env_name_follows(self.pos)
}
fn env_begin_at(&self, idx: usize) -> bool {
self.tokens[idx].text == BEGIN_CMD && self.env_name_follows(idx)
}
fn env_end_at(&self, idx: usize) -> bool {
self.tokens[idx].text == END_CMD && self.env_name_follows(idx)
}
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 precede(&mut self, checkpoint: usize, kind: SyntaxKind) {
self.events.insert(checkpoint, Event::Start(kind));
}
fn extend_back(&mut self, checkpoint: usize, at: usize) {
debug_assert!(checkpoint <= at, "extend_back must move a Start backwards");
if let Event::Start(kind) = self.events[at] {
self.events.remove(at);
self.events.insert(checkpoint, Event::Start(kind));
}
}
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 document(&mut self) {
self.parse_block(Block::Document);
}
fn starts_block_env(&self, idx: usize) -> bool {
if self.tokens.get(idx).is_some_and(|t| t.text == BEGIN_CMD) {
return peek_begin_name(self.tokens, idx)
.as_deref()
.is_some_and(|name| self.ctx.is_block_environment(name));
}
self.alias_openers.get(&idx).is_some_and(|target| {
self.ctx.is_block_environment(target) && self.alias_closer(idx).is_some()
})
}
fn doc_comment_bind(&mut self, comment_start: usize, construct_pos: usize) {
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 construct_start = self.events.len();
self.element();
self.extend_back(checkpoint, construct_start);
}
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) {
let starts_block_env = self.starts_block_env(construct_pos);
self.doc_comment_bind(comment_start, construct_pos);
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.starts_block_env(self.pos);
self.element();
if is_nontrivia {
nontrivia_count += 1;
lone_block_env = nontrivia_count == 1 && starts_block_env;
}
}
if !lone_block_env {
self.precede(checkpoint, SyntaxKind::PARAGRAPH);
self.close(); }
}
}
fn at_block_end(&self, block: Block) -> bool {
self.at_end()
|| match block {
Block::Document => false,
Block::Environment => {
self.alias_end.is_some_and(|end| self.pos >= end)
|| (self.at_env_end() && !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.env_end_at(s.next)
|| self.alias_end.is_some_and(|end| s.next >= end))
}
Some(_) => false,
}
}
fn element(&mut self) {
let Some(k) = self.kind() else { return };
match k {
k if Self::is_trivia(k) => self.bump(),
SyntaxKind::CONTROL_WORD => {
if !self.in_macro_code(self.pos) && self.at_env_begin() {
if self.environment_escapes_group(self.pos) {
if let Some(name) = peek_end_name(self.tokens, self.pos) {
self.demoted_envs.insert(name.into_owned());
}
self.command();
} else {
self.environment();
}
} else if !self.in_macro_code(self.pos) && self.at_env_end() {
if (self.in_group() && !self.doc_margin_exempt(self.pos))
|| self.end_orphans_a_demoted_begin(self.pos)
{
self.command();
} else {
self.stray_end();
}
} else if let Some((target, closer)) = (!self.in_macro_code(self.pos))
.then(|| {
let target = self.alias_openers.get(&self.pos)?.clone();
Some((target, self.alias_closer(self.pos)?))
})
.flatten()
{
self.alias_environment(&target, closer);
} else if let Some(closer) = self
.conditional_openers
.contains(&self.pos)
.then(|| self.conditional_closer(self.pos))
.flatten()
{
self.conditional(closer);
} 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_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_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_env_begin() || self.at_env_end()) =>
{
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();
}
#[cfg(test)]
fn tick_scan(&self) {
self.scan_work.set(self.scan_work.get() + 1);
}
#[cfg(not(test))]
fn tick_scan(&self) {}
fn bracket_closes_before_macrocode_end(&self, open: usize) -> bool {
if self.macrocode_end.is_none() {
return true;
}
self.gate_verdict(open, &MacrocodeBracketGate).is_some()
}
fn bracket_closes_before_math_end(&self, open: usize) -> bool {
let gate = MathBracketGate {
enclosing_is_dollar: self.enclosing_math_is_dollar(),
};
self.gated_closer(open, &gate, &self.math_bracket_batch)
.is_some()
}
fn bracket_closes_in_text(&self, open: usize) -> bool {
self.gated_closer(open, &TextBracketGate, &self.text_bracket_batch)
.is_some()
}
fn dollar_closes(&self, open: usize, display: bool) -> bool {
let seed = if display { open + 1 } else { open };
self.gate_verdict(seed, &DollarGate { display }).is_some()
}
fn delim_math_closes(&self, open: usize, closer: &'static str) -> bool {
self.gate_verdict(open, &DelimMathGate { closer }).is_some()
}
fn left_right_closes(&self, open: usize) -> bool {
self.gated_closer(open, &LeftRightGate, &self.left_right_batch)
.is_some()
}
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_dollar.push(true);
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_env_end() => {
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_dollar.pop();
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_dollar.push(false);
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_env_end() => {
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_dollar.pop();
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(k) if Self::is_trivia(k) => 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.precede(checkpoint, 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) {
debug_assert!(!self.at_end(), "math_atom at EOF: caller must guard first");
match self.kind() {
Some(SyntaxKind::L_BRACE) => self.math_group(),
Some(SyntaxKind::CONTROL_WORD) => {
if !self.in_macro_code(self.pos) && self.at_env_begin() {
self.environment();
} else if !self.in_macro_code(self.pos) && self.at_env_end() {
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_env_end(),
_ => 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_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_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_env_end() => {
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_env_end() || 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.in_group() {
return false;
}
if self.doc_margin_exempt(open) {
return false;
}
self.gated_closer(open, &EnvGate, &self.env_batch).is_some()
}
fn conditional_closer(&self, open: usize) -> Option<usize> {
self.gated_closer(open, &ConditionalGate, &self.conditional_batch)
}
fn walk_key(&self) -> WalkKey {
WalkKey {
macrocode_end: self.macrocode_end,
in_def_body: self.in_def_body,
in_group: self.in_group(),
plain_braces: self.plain_braces_version,
enclosing_math_is_dollar: self.enclosing_math_is_dollar(),
}
}
fn enclosing_math_is_dollar(&self) -> bool {
self.math_dollar.last().copied().unwrap_or(false)
}
fn bracket_abuts_command(&self, i: usize) -> bool {
self.tokens[i].kind == SyntaxKind::L_BRACKET
&& i > 0
&& matches!(
self.tokens[i - 1].kind,
SyntaxKind::CONTROL_WORD | SyntaxKind::CONTROL_SYMBOL
)
}
fn gated_closer<P: GatePolicy>(
&self,
open: usize,
policy: &P,
memo: &std::cell::RefCell<Option<GateBatch>>,
) -> Option<usize> {
policy.last_closer(self)?;
let key = self.walk_key();
if let Some(batch) = memo.borrow().as_ref()
&& batch.key == key
&& let Some(&verdict) = batch.verdicts.get(&open)
{
return verdict;
}
let mut verdicts =
memo.borrow_mut()
.take()
.map_or_else(std::collections::HashMap::new, |stale| {
let mut map = stale.verdicts;
map.clear();
map
});
self.gate_batch(open, policy, &mut verdicts);
let verdict = verdicts.get(&open).copied();
debug_assert!(verdict.is_some(), "the batch must settle its own seed");
*memo.borrow_mut() = Some(GateBatch { key, verdicts });
verdict.flatten()
}
fn gate_verdict<P: GatePolicy>(&self, open: usize, policy: &P) -> Option<usize> {
policy.last_closer(self)?;
let mut sink = SeedVerdict {
seed: open,
verdict: None,
};
self.gate_batch(open, policy, &mut sink);
debug_assert!(sink.verdict.is_some(), "the batch must settle its own seed");
sink.verdict.flatten()
}
fn gate_batch<P: GatePolicy, S: VerdictSink>(&self, open: usize, policy: &P, verdicts: &mut S) {
struct Entry {
opener: usize,
envs_at_push: usize,
settled: bool,
}
fn settle_level<S: VerdictSink>(
pending: &mut [Entry],
live: &mut Vec<usize>,
verdicts: &mut S,
envs: usize,
) {
while let Some(&idx) = live.last() {
let entry = &mut pending[idx];
if entry.envs_at_push != envs {
break;
}
entry.settled = true;
verdicts.insert(entry.opener, None);
live.pop();
}
}
fn settle_innermost<S: VerdictSink>(
pending: &mut [Entry],
live: &mut Vec<usize>,
verdicts: &mut S,
envs: usize,
) {
let Some(entry) = pending.last_mut() else {
return;
};
if entry.settled || entry.envs_at_push != envs {
return;
}
entry.settled = true;
verdicts.insert(entry.opener, None);
debug_assert_eq!(live.last().copied(), Some(pending.len() - 1));
live.pop();
}
let mut pending = vec![Entry {
opener: open,
envs_at_push: 0,
settled: false,
}];
let mut live = vec![0usize];
let mut depth = 0usize;
let mut envs = 0usize;
let mut newlines = 0;
let mut transparent = false;
let end = self
.macrocode_end
.unwrap_or(self.tokens.len())
.min(self.tokens.len())
.min(policy.last_closer(self).map_or(0, |last| last + 1));
let mut i = open + 1;
while i < end {
self.tick_scan();
let t = &self.tokens[i];
match t.kind {
SyntaxKind::NEWLINE => {
newlines += 1;
if newlines >= BLANK_LINE_NEWLINES
&& match P::PARAGRAPH_ANCHOR {
ParagraphAnchor::None => false,
ParagraphAnchor::OwnLevel => depth == 0,
ParagraphAnchor::AnyDepth => true,
}
{
if P::PARAGRAPH_ANCHOR == ParagraphAnchor::AnyDepth {
break;
}
match P::NESTING {
Nesting::Counted => {
settle_level(&mut pending, &mut live, verdicts, envs);
}
Nesting::Interleaved => {
settle_innermost(&mut pending, &mut live, verdicts, envs);
}
}
if live.is_empty() {
return;
}
}
i += 1;
continue;
}
SyntaxKind::WHITESPACE | SyntaxKind::DOC_MARGIN => {
i += 1;
continue;
}
SyntaxKind::GUARD => {
newlines = 0;
i += 1;
continue;
}
SyntaxKind::DOLLAR
if depth == 0 && policy.dollar_anchor() == DollarAnchor::Refutes =>
{
break;
}
SyntaxKind::DOLLAR
if depth == 0 && policy.dollar_anchor() == DollarAnchor::Transparent =>
{
transparent = !transparent;
}
SyntaxKind::CONTROL_SYMBOL
if (depth == 0 || P::ANCHORS_AT_ANY_DEPTH)
&& P::MATH_ANCHOR.anchors(t.text.as_str()) =>
{
break;
}
SyntaxKind::L_BRACE
if !P::PLAIN_BRACES_ARE_TOKENS || !self.plain_braces.contains(&i) =>
{
depth += 1
}
SyntaxKind::R_BRACE
if !P::PLAIN_BRACES_ARE_TOKENS || !self.plain_braces.contains(&i) =>
{
if depth == 0 {
match P::STRAY_BRACE {
StrayBrace::RefutesInGroup if self.in_group() => break,
StrayBrace::ClosesInGroup if self.in_group() => {
for &idx in &live {
verdicts.insert(pending[idx].opener, Some(i));
}
return;
}
StrayBrace::RefutesAlways => break,
_ => {}
}
} else {
depth -= 1;
}
}
_ => {
if !transparent && depth == 0 && policy.opens_at(self, i) {
if P::OPENER_IS_ENV_BEGIN {
envs += 1;
}
live.push(pending.len());
pending.push(Entry {
opener: i,
envs_at_push: envs,
settled: false,
});
} else if !transparent && depth == 0 && policy.closes_at(self, i) {
let entry = pending
.pop()
.expect("a live entry remains, so pending is non-empty");
if P::NESTING == Nesting::Interleaved && envs != entry.envs_at_push {
if !entry.settled {
live.pop();
verdicts.insert(entry.opener, None);
}
break;
}
if !entry.settled {
live.pop();
let balanced =
!P::CLOSER_NEEDS_ENV_BALANCE || envs == entry.envs_at_push;
let paired = balanced && policy.pairs(self, entry.opener, i);
verdicts.insert(entry.opener, paired.then_some(i));
if live.is_empty() {
return;
}
}
} else if t.kind == SyntaxKind::CONTROL_WORD
&& (depth == 0 || P::ANCHORS_AT_ANY_DEPTH)
&& (P::ENV_ANCHOR_IN_MACRO_CODE || !self.in_macro_code(i))
{
if self.env_begin_at(i) {
if P::MACROCODE_FRAME_ANCHORS
&& peek_begin_name(self.tokens, i).is_some_and(|n| {
matches!(n.as_ref(), "macrocode" | "macrocode*")
})
{
break;
}
if P::ENV_ANCHOR == EnvAnchor::Refutes {
break;
}
envs += 1;
} else if self.env_end_at(i) {
if P::ENV_ANCHOR == EnvAnchor::Refutes {
break;
}
match P::NESTING {
Nesting::Interleaved => {
if pending.last().is_some_and(|e| e.envs_at_push == envs) {
break;
}
}
Nesting::Counted => {
settle_level(&mut pending, &mut live, verdicts, envs);
if live.is_empty() {
return;
}
}
}
envs -= 1;
}
}
}
}
newlines = 0;
i += 1;
}
for &idx in &live {
verdicts.insert(pending[idx].opener, None);
}
}
fn alias_closer(&self, open: usize) -> Option<usize> {
self.alias_openers.get(&open)?;
self.gated_closer(open, &AliasGate, &self.alias_batch)
}
fn alias_environment(&mut self, target: &str, closer: usize) {
self.open(SyntaxKind::ENVIRONMENT);
self.open(SyntaxKind::BEGIN);
self.bump(); self.close();
let saved = self.alias_end.replace(closer);
self.open_envs.push(target.to_owned());
if self.ctx.is_verbatim_environment(target) {
self.verbatim_body(target);
} else if self.ctx.is_math_environment(target) {
self.math_environment_body();
} else {
self.parse_block(Block::Environment);
}
self.open_envs.pop();
self.alias_end = saved;
if self.pos == closer {
self.open(SyntaxKind::END);
self.bump();
self.close();
}
self.close(); }
fn conditional(&mut self, closer: usize) {
self.open(SyntaxKind::CONDITIONAL);
self.open(SyntaxKind::CONDITIONAL_BRANCH);
self.command(); loop {
if self.pos >= closer || self.at_block_end(Block::Macrocode) {
break;
}
match self.conditional_flow_at(self.pos) {
Some(conditional::FlowWord::Fi) => break,
Some(conditional::FlowWord::Else | conditional::FlowWord::Or) => {
self.close(); self.open(SyntaxKind::CONDITIONAL_BRANCH);
self.flow_command();
continue;
}
None => {}
}
if let Some((comment_start, construct_pos, _)) = self.binding_run(self.pos)
&& self.conditional_flow_at(construct_pos).is_none()
{
self.doc_comment_bind(comment_start, construct_pos);
continue;
}
self.element();
}
self.close(); if self.conditional_flow_at(self.pos) == Some(conditional::FlowWord::Fi) {
self.flow_command();
}
self.close(); }
fn flow_command(&mut self) {
self.open(SyntaxKind::COMMAND);
self.bump();
self.close();
}
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(|n| self.ctx.is_math_environment(n))
{
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(|n| self.ctx.is_math_environment(n))
{
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.plain_braces_version += 1;
self.macrocode_end = Some(end);
self.in_def_body = true;
self.parse_block(Block::Macrocode);
self.plain_braces = saved_plain;
self.plain_braces_version += 1;
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.as_deref() == end_name.as_deref() {
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_dollar.push(false);
while !self.at_block_end(Block::Environment) {
self.math_element();
}
self.math_dollar.pop();
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<Cow<'_, str>> {
peek_end_name(tokens, begin_pos)
}
fn peek_end_name(tokens: &[Token], end_pos: usize) -> Option<Cow<'_, str>> {
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 start = i;
while tokens.get(i).is_some_and(|t| t.kind != SyntaxKind::R_BRACE) {
i += 1;
}
Some(match &tokens[start..i] {
[] => Cow::Borrowed(""),
[t] => Cow::Borrowed(t.text.trim()),
many => {
let mut name = String::new();
for t in many {
name.push_str(&t.text);
}
Cow::Owned(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 = ParseCtx::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 = ParseCtx::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");
}
fn scan_work(input: &str) -> usize {
let tokens = lex(input);
let ctx = ParseCtx::default();
let mut p = Parser::new(&tokens, &ctx);
p.document();
p.scan_work.get()
}
#[track_caller]
fn assert_scan_work_linear(small: &str, doubled: &str) {
let (w1, w2) = (scan_work(small), scan_work(doubled));
assert!(
w2 < 3 * w1 + 64,
"gate-scan work grew superlinearly: {w1} -> {w2}"
);
}
#[test]
fn gate_scans_stay_linear_without_closers() {
let shape = "\\ifabc x\n";
assert_scan_work_linear(&shape.repeat(200), &shape.repeat(400));
let shape = "\\cmd[x\n";
assert_scan_work_linear(&shape.repeat(200), &shape.repeat(400));
let shape = "\\[ x\n";
assert_scan_work_linear(&shape.repeat(200), &shape.repeat(400));
}
#[test]
fn conditional_batch_keeps_shared_frame_openers_linear() {
let body = |n: usize| format!("{}\\fi\n", "\\ifabc x\n".repeat(n));
assert_scan_work_linear(&body(200), &body(400));
}
#[test]
fn alias_batch_keeps_shared_frame_openers_linear() {
let scan_work = |input: &str| {
let tokens = lex(input);
let mut ctx = ParseCtx::default();
ctx.insert_begin_alias(SmolStr::new("bc"), SmolStr::new("center"));
ctx.insert_end_alias(SmolStr::new("ec"), SmolStr::new("center"));
let mut p = Parser::new(&tokens, &ctx);
p.document();
p.scan_work.get()
};
let body = |n: usize| format!("{}\\ec\n", "\\bc x\n".repeat(n));
let (w1, w2) = (scan_work(&body(200)), scan_work(&body(400)));
assert!(
w2 < 3 * w1 + 64,
"gate-scan work grew superlinearly: {w1} -> {w2}"
);
}
#[test]
fn env_batch_keeps_shared_frame_openers_linear() {
let body = |n: usize| format!("{{\n{}", "\\begin{itemize}\n".repeat(n));
assert_scan_work_linear(&body(200), &body(400));
}
#[test]
fn left_right_batch_keeps_shared_frame_openers_linear() {
let body = |n: usize| format!("$ {}\\right)$\n", "\\left( x ".repeat(n));
assert_scan_work_linear(&body(200), &body(400));
}
#[test]
fn bracket_batch_keeps_shared_frame_openers_linear() {
let body = |n: usize| format!("{}]\n", "\\cmd[x\n".repeat(n));
assert_scan_work_linear(&body(200), &body(400));
let body = |n: usize| format!("$ {}]$\n", "\\cmd[x ".repeat(n));
assert_scan_work_linear(&body(200), &body(400));
}
#[test]
fn math_gate_scans_stay_linear_without_closers() {
let body = |n: usize| format!("$ {}$", "\\cmd[x ".repeat(n));
assert_scan_work_linear(&body(200), &body(400));
let body = |n: usize| format!("\\[\n{}\\]\n", "\\left( x\n".repeat(n));
assert_scan_work_linear(&body(200), &body(400));
}
#[test]
fn math_batch_stays_linear_with_one_closer_at_eof() {
let body = |n: usize| format!("{}\\]\n", "\\[ x\n".repeat(n));
assert_scan_work_linear(&body(200), &body(400));
let body = |n: usize| format!("{}\\)\n", "\\( x\n".repeat(n));
assert_scan_work_linear(&body(200), &body(400));
let body = |n: usize| format!("{}$\n", "$ x\n".repeat(n));
assert_scan_work_linear(&body(200), &body(400));
let body = |n: usize| format!("{}$$\n", "$$ x\n".repeat(n));
assert_scan_work_linear(&body(200), &body(400));
}
}