use super::Parser;
use super::trivia::{BLANK_LINE_NEWLINES, CommentMode};
use crate::semantic::expl3::{Expl3Slot, expl3_slots};
use crate::syntax::SyntaxKind;
use std::collections::HashMap;
pub(super) struct BraceMatches {
plain_braces: u32,
macrocode_end: Option<usize>,
from: usize,
ends: HashMap<usize, usize>,
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum PlanArg {
Command(usize),
Group(std::ops::Range<usize>),
Tokens(std::ops::Range<usize>),
}
impl PlanArg {
fn start(&self) -> usize {
match self {
PlanArg::Command(idx) => *idx,
PlanArg::Group(r) | PlanArg::Tokens(r) => r.start,
}
}
pub(super) fn end(&self) -> usize {
match self {
PlanArg::Command(idx) => idx + 1,
PlanArg::Group(r) | PlanArg::Tokens(r) => r.end,
}
}
}
#[derive(Debug)]
pub(super) struct Expl3Plan {
pub(super) args: Vec<PlanArg>,
pub(super) end: usize,
#[cfg_attr(not(test), allow(dead_code))]
pub(super) complete: bool,
}
enum Stop {
End,
Abort,
}
impl Parser<'_> {
pub(super) fn expl3_arity_slots(&self) -> Option<Vec<Expl3Slot>> {
let t = self.tokens.get(self.pos)?;
if t.kind != SyntaxKind::CONTROL_WORD {
return None;
}
let name = t.text.strip_prefix('\\')?;
if name.starts_with(':') {
return None;
}
expl3_slots(name)
}
pub(super) fn scan_expl3_unit(&self, slots: &[Expl3Slot]) -> Option<Expl3Plan> {
if self.in_math() {
return None;
}
let bound = [self.macrocode_end, self.alias_end]
.into_iter()
.flatten()
.fold(self.tokens.len(), usize::min);
let mut scan = UnitScan {
p: self,
i: self.pos + 1,
bound,
args: Vec::new(),
};
let mut complete = true;
for slot in slots {
let took = match slot {
Expl3Slot::SingleToken => scan.take_single_token(),
Expl3Slot::Group | Expl3Slot::Branch => scan.take_group(),
Expl3Slot::ParameterText => scan.take_parameter_text(),
};
match took {
Ok(()) => {}
Err(Stop::End) => {
complete = false;
break;
}
Err(Stop::Abort) => return None,
}
}
let end = scan.args.last().map_or(self.pos + 1, PlanArg::end);
Some(Expl3Plan {
args: scan.args,
end,
complete,
})
}
pub(super) fn attach_expl3_arguments(&mut self, plan: &Expl3Plan) {
for arg in &plan.args {
while self.pos < arg.start() {
self.bump();
}
match arg {
PlanArg::Command(idx) => {
debug_assert_eq!(self.pos, *idx);
self.command_bare();
}
PlanArg::Group(range) => {
debug_assert_eq!(self.pos, range.start);
self.group();
debug_assert_eq!(self.pos, range.end);
}
PlanArg::Tokens(range) => {
debug_assert_eq!(self.pos, range.start);
while self.pos < range.end {
self.bump();
}
}
}
}
debug_assert_eq!(self.pos, plan.end);
}
fn matching_brace(&self, open: usize) -> Option<usize> {
if let Some(table) = self.brace_matches.borrow().as_ref()
&& table.plain_braces == self.plain_braces_version
&& table.macrocode_end == self.macrocode_end
&& open >= table.from
{
return table.ends.get(&open).copied();
}
let mut ends = self
.brace_matches
.borrow_mut()
.take()
.map_or_else(HashMap::new, |stale| {
let mut map = stale.ends;
map.clear();
map
});
let mut stack: Vec<usize> = Vec::new();
for j in open..self.macrocode_end.unwrap_or(self.tokens.len()) {
self.tick_scan();
match self.tokens[j].kind {
SyntaxKind::L_BRACE if !self.plain_braces.contains(&j) => stack.push(j),
SyntaxKind::R_BRACE if !self.plain_braces.contains(&j) => {
if let Some(opened) = stack.pop() {
ends.insert(opened, j);
}
}
_ => {}
}
}
let answer = ends.get(&open).copied();
*self.brace_matches.borrow_mut() = Some(BraceMatches {
plain_braces: self.plain_braces_version,
macrocode_end: self.macrocode_end,
from: open,
ends,
});
answer
}
fn command_bare(&mut self) {
self.open(SyntaxKind::COMMAND);
self.bump();
self.close();
}
}
struct UnitScan<'p, 't> {
p: &'p Parser<'t>,
i: usize,
bound: usize,
args: Vec<PlanArg>,
}
impl UnitScan<'_, '_> {
fn advance(&mut self) -> Result<(usize, bool), Stop> {
let mut newlines = 0usize;
let mut crossed_comment = false;
let gap_start = self.i;
loop {
if self.i >= self.bound {
return Err(Stop::Abort);
}
self.p.tick_scan();
match self.p.tokens[self.i].kind {
SyntaxKind::NEWLINE => {
newlines += 1;
if newlines >= BLANK_LINE_NEWLINES {
return Err(self.gap_stop(gap_start));
}
self.i += 1;
}
SyntaxKind::WHITESPACE | SyntaxKind::TILDE => self.i += 1,
SyntaxKind::COMMENT => {
if self.p.binding_run(self.i).is_some() {
return Err(Stop::Abort);
}
if newlines > 0 {
crossed_comment = true;
}
newlines = 0;
self.i += 1;
}
SyntaxKind::GUARD | SyntaxKind::DOC_MARGIN => return Err(Stop::Abort),
_ => return Ok((self.i, crossed_comment)),
}
}
}
fn gap_stop(&self, gap_start: usize) -> Stop {
if !self.p.group_opens.is_empty() {
return Stop::End;
}
let s = self.p.scan_trivia(gap_start, CommentMode::Skip);
let reaches_terminator = s.next_kind.is_none()
|| self.p.macrocode_end.is_some_and(|end| s.next >= end)
|| self.p.alias_end.is_some_and(|end| s.next >= end)
|| (s.next_kind == Some(SyntaxKind::CONTROL_WORD) && self.p.env_end_at(s.next));
if s.saw_blank_line || reaches_terminator {
Stop::Abort
} else {
Stop::End
}
}
fn push_token(&mut self, idx: usize) {
match self.args.last_mut() {
Some(PlanArg::Tokens(r)) if r.end == idx => r.end = idx + 1,
_ => self.args.push(PlanArg::Tokens(idx..idx + 1)),
}
self.i = idx + 1;
}
fn control_word_forms_node(&self, idx: usize) -> bool {
let text = self.p.tokens[idx].text.as_str();
if (text == super::BEGIN_CMD || text == super::END_CMD) && !self.p.in_macro_code(idx) {
return true;
}
self.p.conditional_openers.contains(&idx)
|| self.p.alias_openers.contains_key(&idx)
|| self.p.alias_closers.contains_key(&idx)
}
fn control_symbol_forms_node(&self, idx: usize) -> bool {
match self.p.tokens[idx].text.as_str() {
"\\\\" => true,
"\\[" => self.p.delim_math_closes(idx, "\\]"),
"\\(" => self.p.delim_math_closes(idx, "\\)"),
_ => false,
}
}
fn take_single_token(&mut self) -> Result<(), Stop> {
let (idx, crossed) = self.advance()?;
let t = &self.p.tokens[idx];
if crossed && !(t.kind == SyntaxKind::L_BRACE && !self.p.plain_braces.contains(&idx)) {
return Err(Stop::End);
}
match t.kind {
SyntaxKind::CONTROL_WORD if !self.control_word_forms_node(idx) => {
self.args.push(PlanArg::Command(idx));
self.i = idx + 1;
Ok(())
}
SyntaxKind::CONTROL_SYMBOL if !self.control_symbol_forms_node(idx) => {
self.push_token(idx);
Ok(())
}
SyntaxKind::WORD if t.text.chars().count() == 1 => {
self.push_token(idx);
Ok(())
}
SyntaxKind::HASH => {
self.push_token(idx);
loop {
let (next, crossed) = self.advance()?;
let t = &self.p.tokens[next];
if crossed {
return Err(Stop::End);
}
match t.kind {
SyntaxKind::HASH => self.push_token(next),
SyntaxKind::WORD if is_param_digit_text(&t.text) => {
self.push_token(next);
return Ok(());
}
_ => return Err(Stop::Abort),
}
}
}
SyntaxKind::L_BRACE => {
let end = self.group_end(idx).ok_or(Stop::Abort)?;
self.args.push(PlanArg::Group(idx..end));
self.i = end;
Ok(())
}
_ => Err(Stop::Abort),
}
}
fn take_group(&mut self) -> Result<(), Stop> {
let (idx, crossed) = self.advance()?;
if self.p.tokens[idx].kind != SyntaxKind::L_BRACE {
return Err(if crossed { Stop::End } else { Stop::Abort });
}
let end = self.group_end(idx).ok_or(Stop::Abort)?;
self.args.push(PlanArg::Group(idx..end));
self.i = end;
Ok(())
}
fn take_parameter_text(&mut self) -> Result<(), Stop> {
loop {
let (idx, crossed) = self.advance()?;
let t = &self.p.tokens[idx];
if crossed && !(t.kind == SyntaxKind::L_BRACE && !self.p.plain_braces.contains(&idx)) {
return Err(Stop::End);
}
match t.kind {
SyntaxKind::L_BRACE if !self.p.plain_braces.contains(&idx) => return Ok(()),
SyntaxKind::R_BRACE if !self.p.plain_braces.contains(&idx) => {
return Err(Stop::Abort);
}
SyntaxKind::DOLLAR => return Err(Stop::Abort),
SyntaxKind::CONTROL_WORD => {
if self.control_word_forms_node(idx) {
return Err(Stop::Abort);
}
self.args.push(PlanArg::Command(idx));
self.i = idx + 1;
}
SyntaxKind::CONTROL_SYMBOL => {
if self.control_symbol_forms_node(idx) {
return Err(Stop::Abort);
}
self.push_token(idx);
}
_ => self.push_token(idx),
}
}
}
fn group_end(&self, open: usize) -> Option<usize> {
if self.p.plain_braces.contains(&open) {
return None;
}
let close = self.p.matching_brace(open)?;
if self
.p
.expl_toggles
.iter()
.any(|&(idx, _)| open < idx && idx < close)
{
return None;
}
(close < self.bound).then_some(close + 1)
}
}
fn is_param_digit_text(text: &str) -> bool {
matches!(text.as_bytes(), [b'1'..=b'9'])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::lexer::{ParseCtx, lex};
fn scan(input: &str, head: &str) -> Option<(Vec<String>, bool)> {
let tokens = lex(input);
let ctx = ParseCtx::default();
let mut p = Parser::new(&tokens, &ctx);
p.pos = tokens
.iter()
.position(|t| t.text == head)
.expect("head token present");
let slots = p.expl3_arity_slots()?;
let plan = p.scan_expl3_unit(&slots)?;
let text = |r: &std::ops::Range<usize>| {
tokens[r.clone()]
.iter()
.map(|t| t.text.as_str())
.collect::<String>()
};
let args = plan
.args
.iter()
.map(|arg| match arg {
PlanArg::Command(i) => format!("cmd:{}", tokens[*i].text),
PlanArg::Group(r) => format!("group:{}", text(r)),
PlanArg::Tokens(r) => format!("tok:{}", text(r)),
})
.collect();
Some((args, plan.complete))
}
const ON: &str = "\\ExplSyntaxOn\n";
#[test]
fn single_token_and_group_slots_resolve() {
let (args, complete) = scan(
&format!("{ON}\\tl_set:Nn \\l_tmpa_tl {{ x }}\n"),
"\\tl_set:Nn",
)
.expect("recognized");
assert_eq!(args, ["cmd:\\l_tmpa_tl", "group:{ x }"]);
assert!(complete);
}
#[test]
fn relation_word_satisfies_a_single_token_slot() {
let (args, _) = scan(
&format!("{ON}\\int_compare:nNnTF {{ a }} = {{ 1 }} {{ T }} {{ F }}\n"),
"\\int_compare:nNnTF",
)
.expect("recognized");
assert_eq!(
args,
[
"group:{ a }",
"tok:=",
"group:{ 1 }",
"group:{ T }",
"group:{ F }"
]
);
}
#[test]
fn multi_character_word_aborts_the_slot() {
assert!(
scan(
&format!("{ON}\\int_compare:nNnTF {{ a }} == {{ 1 }} {{ T }} {{ F }}\n"),
"\\int_compare:nNnTF",
)
.is_none()
);
}
#[test]
fn parameter_text_runs_to_the_first_explicit_brace() {
let (args, _) = scan(
&format!("{ON}\\cs_new:Npn \\foo:nn #1#2 {{ body }}\n"),
"\\cs_new:Npn",
)
.expect("recognized");
assert_eq!(args, ["cmd:\\foo:nn", "tok:#1#2", "group:{ body }"]);
}
#[test]
fn delimiting_control_word_is_parameter_text() {
let (args, _) = scan(
&format!("{ON}\\cs_new:Npn \\foo:w #1 \\q_stop {{ body }}\n"),
"\\cs_new:Npn",
)
.expect("recognized");
assert_eq!(
args,
["cmd:\\foo:w", "tok:#1", "cmd:\\q_stop", "group:{ body }"]
);
}
#[test]
fn hash_parameter_satisfies_a_single_token_slot() {
let (args, _) =
scan(&format!("{ON}\\tl_set:Nn #1 {{ x }}\n"), "\\tl_set:Nn").expect("recognized");
assert_eq!(args, ["tok:#1", "group:{ x }"]);
}
#[test]
fn braces_around_a_single_token_slot_are_grabbed_whole() {
let (args, _) = scan(
&format!("{ON}\\tl_set:Nn {{ \\l_tmpa_tl }} {{ x }}\n"),
"\\tl_set:Nn",
)
.expect("recognized");
assert_eq!(args, ["group:{ \\l_tmpa_tl }", "group:{ x }"]);
}
#[test]
fn blank_line_at_paragraph_level_aborts() {
assert!(
scan(
&format!("{ON}\\tl_set:Nn \\l_tmpa_tl\n\n{{ x }}\n"),
"\\tl_set:Nn",
)
.is_none()
);
}
#[test]
fn blank_line_in_a_group_body_commits_the_prefix() {
let input = format!("{ON}{{ \\tl_set:Nn \\l_tmpa_tl\n\n{{ x }} }}\n");
let tokens = lex(&input);
let ctx = ParseCtx::default();
let mut p = Parser::new(&tokens, &ctx);
p.pos = tokens
.iter()
.position(|t| t.text == "\\tl_set:Nn")
.expect("head token present");
p.group_opens.push(0);
let slots = p.expl3_arity_slots().expect("derivable");
let plan = p.scan_expl3_unit(&slots).expect("recognized");
assert_eq!(plan.args.len(), 1, "only the N slot is consumed");
assert!(!plan.complete);
}
#[test]
fn braced_candidate_past_an_own_line_comment_is_consumed() {
let (args, complete) = scan(
&format!("{ON}\\tl_set:Nn \\l_tmpa_tl\n% doc\n{{ x }}\n"),
"\\tl_set:Nn",
)
.expect("recognized");
assert_eq!(args, ["cmd:\\l_tmpa_tl", "group:{ x }"]);
assert!(complete);
}
#[test]
fn non_braced_candidate_past_an_own_line_comment_ends_the_unit() {
let (args, complete) = scan(
&format!("{ON}\\cs_new:Npn \\module_foo:n\n% doc\n#1 {{ x }}\n"),
"\\cs_new:Npn",
)
.expect("recognized");
assert_eq!(args, ["cmd:\\module_foo:n"]);
assert!(!complete);
}
#[test]
fn trailing_same_line_comment_is_transparent() {
let (args, complete) = scan(
&format!("{ON}\\tl_set:Nn \\l_tmpa_tl % why\n{{ x }}\n"),
"\\tl_set:Nn",
)
.expect("recognized");
assert_eq!(args, ["cmd:\\l_tmpa_tl", "group:{ x }"]);
assert!(complete);
}
#[test]
fn binding_comment_run_aborts_the_unit() {
assert!(
scan(
&format!("{ON}\\tl_set:Nn\n% doc\n\\l_tmpa_tl {{ x }}\n"),
"\\tl_set:Nn",
)
.is_none()
);
}
#[test]
fn tilde_is_skipped_like_the_space_it_is() {
let (args, _) = scan(
&format!("{ON}\\tl_set:Nn ~ \\l_tmpa_tl ~ {{ x }}\n"),
"\\tl_set:Nn",
)
.expect("recognized");
assert_eq!(args, ["cmd:\\l_tmpa_tl", "group:{ x }"]);
}
#[test]
fn group_slot_facing_a_bare_token_aborts() {
assert!(scan(&format!("{ON}\\tl_set:Nn \\l_a \\foo\n"), "\\tl_set:Nn").is_none());
}
#[test]
fn stream_ending_mid_unit_aborts() {
assert!(scan(&format!("{ON}\\tl_set:Nn \\l_a"), "\\tl_set:Nn").is_none());
}
#[test]
fn unclosed_group_aborts() {
assert!(scan(&format!("{ON}\\tl_set:Nn \\l_a {{ x\n"), "\\tl_set:Nn").is_none());
}
#[test]
fn enclosing_group_closing_mid_parameter_text_aborts() {
assert!(
scan(
&format!("{ON}{{ \\cs_new:Npn \\foo:n #1 }}\n"),
"\\cs_new:Npn"
)
.is_none()
);
}
#[test]
fn zero_arity_head_consumes_nothing() {
let (args, complete) =
scan(&format!("{ON}\\scan_stop: {{ x }}\n"), "\\scan_stop:").expect("recognized");
assert!(args.is_empty());
assert!(complete);
}
#[test]
fn expansion_drivers_are_excluded() {
assert!(scan(&format!("{ON}\\::n {{ x }}\n"), "\\::n").is_none());
}
#[test]
fn underivable_heads_have_no_slots() {
assert!(scan(&format!("{ON}\\exp_after:wN \\foo\n"), "\\exp_after:wN").is_none());
assert!(scan(&format!("{ON}\\l_tmpa_tl x\n"), "\\l_tmpa_tl").is_none());
}
#[test]
fn line_break_control_symbol_aborts_a_single_token_slot() {
assert!(scan(&format!("{ON}\\tl_set:Nn \\\\ {{ x }}\n"), "\\tl_set:Nn").is_none());
}
#[test]
fn plain_control_symbol_satisfies_a_single_token_slot() {
let (args, _) =
scan(&format!("{ON}\\tl_set:Nn \\) {{ x }}\n"), "\\tl_set:Nn").expect("recognized");
assert_eq!(args, ["tok:\\)", "group:{ x }"]);
}
}