use std::iter::Peekable;
use std::str::CharIndices;
use praxis_source::{DiagCode, Span};
use crate::ast::{TemplatePart, WsPolicy};
use crate::validate::ValidationError;
pub use praxis_syntax::MAX_TEMPLATE_NESTING as MAX_NESTING;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScanError {
InvalidEscape { byte_offset: usize, seq: String },
UnterminatedCapture { byte_offset: usize },
EmptyCapture { byte_offset: usize },
InvalidCaptureName { byte_offset: usize, name: String },
UnknownCaptureKind { byte_offset: usize, name: String },
UnknownConstructor { byte_offset: usize, name: String },
MalformedCaptureBody { byte_offset: usize, message: String },
CallShape(ValidationError),
NestingTooDeep {
byte_offset: usize,
what: &'static str,
},
}
impl ScanError {
#[must_use]
pub fn byte_offset(&self) -> usize {
match self {
ScanError::InvalidEscape { byte_offset, .. }
| ScanError::UnterminatedCapture { byte_offset }
| ScanError::EmptyCapture { byte_offset }
| ScanError::InvalidCaptureName { byte_offset, .. }
| ScanError::UnknownCaptureKind { byte_offset, .. }
| ScanError::UnknownConstructor { byte_offset, .. }
| ScanError::MalformedCaptureBody { byte_offset, .. }
| ScanError::NestingTooDeep { byte_offset, .. } => *byte_offset,
ScanError::CallShape(err) => err.span.start().to_u32() as usize,
}
}
#[must_use]
pub fn shifted(self, delta: usize) -> ScanError {
let bump = |at: usize| at + delta;
match self {
ScanError::InvalidEscape { byte_offset, seq } => ScanError::InvalidEscape {
byte_offset: bump(byte_offset),
seq,
},
ScanError::UnterminatedCapture { byte_offset } => ScanError::UnterminatedCapture {
byte_offset: bump(byte_offset),
},
ScanError::EmptyCapture { byte_offset } => ScanError::EmptyCapture {
byte_offset: bump(byte_offset),
},
ScanError::InvalidCaptureName { byte_offset, name } => ScanError::InvalidCaptureName {
byte_offset: bump(byte_offset),
name,
},
ScanError::UnknownCaptureKind { byte_offset, name } => ScanError::UnknownCaptureKind {
byte_offset: bump(byte_offset),
name,
},
ScanError::UnknownConstructor { byte_offset, name } => ScanError::UnknownConstructor {
byte_offset: bump(byte_offset),
name,
},
ScanError::MalformedCaptureBody {
byte_offset,
message,
} => ScanError::MalformedCaptureBody {
byte_offset: bump(byte_offset),
message,
},
ScanError::NestingTooDeep { byte_offset, what } => ScanError::NestingTooDeep {
byte_offset: bump(byte_offset),
what,
},
ScanError::CallShape(mut err) => {
err.span = err.span.shifted(delta as u32);
ScanError::CallShape(err)
}
}
}
#[must_use]
pub fn code(&self) -> DiagCode {
match self {
ScanError::InvalidCaptureName { .. } => DiagCode::InvalidCaptureName,
ScanError::UnknownCaptureKind { .. } => DiagCode::UnknownCaptureKind,
ScanError::UnknownConstructor { .. } => DiagCode::UnknownConstructor,
ScanError::CallShape(err) => err.code,
ScanError::InvalidEscape { .. }
| ScanError::UnterminatedCapture { .. }
| ScanError::EmptyCapture { .. }
| ScanError::MalformedCaptureBody { .. }
| ScanError::NestingTooDeep { .. } => DiagCode::TemplateScan,
}
}
#[must_use]
pub fn unknown_parser_name(&self) -> Option<&str> {
match self {
ScanError::UnknownCaptureKind { name, .. }
| ScanError::UnknownConstructor { name, .. } => Some(name),
ScanError::InvalidEscape { .. }
| ScanError::UnterminatedCapture { .. }
| ScanError::EmptyCapture { .. }
| ScanError::InvalidCaptureName { .. }
| ScanError::MalformedCaptureBody { .. }
| ScanError::CallShape(_)
| ScanError::NestingTooDeep { .. } => None,
}
}
}
impl std::fmt::Display for ScanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ScanError::InvalidEscape { byte_offset, seq } => {
write!(f, "invalid escape `{seq}` at byte {byte_offset}")
}
ScanError::UnterminatedCapture { byte_offset } => {
write!(f, "unterminated capture starting at byte {byte_offset}")
}
ScanError::EmptyCapture { byte_offset } => {
write!(f, "empty capture `{{}}` at byte {byte_offset}")
}
ScanError::InvalidCaptureName { byte_offset, name } => write!(
f,
"`{name}` at byte {byte_offset} is not a capture name: a capture name is an \
identifier"
),
ScanError::UnknownCaptureKind { byte_offset, name } => write!(
f,
"unknown parser `{name}` at byte {byte_offset}: no atomic or constructor is \
spelled that way"
),
ScanError::UnknownConstructor { byte_offset, name } => {
write!(
f,
"unknown parser constructor `{name}` at byte {byte_offset}"
)
}
ScanError::MalformedCaptureBody {
byte_offset,
message,
} => write!(f, "malformed capture body at byte {byte_offset}: {message}"),
ScanError::CallShape(err) => f.write_str(&err.message),
ScanError::NestingTooDeep { byte_offset, what } => write!(
f,
"{what} nesting is deeper than {MAX_NESTING} at byte {byte_offset}"
),
}
}
}
impl std::error::Error for ScanError {}
pub(crate) struct Scan<'a> {
src: &'a str,
iter: Peekable<CharIndices<'a>>,
pos: usize,
}
impl<'a> Scan<'a> {
pub(crate) fn new(src: &'a str) -> Self {
Scan {
src,
iter: src.char_indices().peekable(),
pos: 0,
}
}
pub(crate) fn peek(&mut self) -> Option<(usize, char)> {
self.iter.peek().copied()
}
pub(crate) fn peek_char(&mut self) -> Option<char> {
self.peek().map(|(_, c)| c)
}
pub(crate) fn bump(&mut self) -> Option<(usize, char)> {
let next = self.iter.next();
self.pos = match next {
Some((at, c)) => at + c.len_utf8(),
None => self.src.len(),
};
next
}
pub(crate) fn eat(&mut self, c: char) -> bool {
if self.peek_char() == Some(c) {
self.bump();
true
} else {
false
}
}
pub(crate) fn pos(&self) -> usize {
self.pos
}
pub(crate) fn src(&self) -> &'a str {
self.src
}
pub(crate) fn advance_to(&mut self, byte: usize) {
while self.pos < byte && self.bump().is_some() {}
}
}
pub fn scan_template(interior: &str) -> Result<Vec<TemplatePart>, ScanError> {
scan_template_at(interior, 0)
}
pub(crate) fn scan_template_at(
interior: &str,
depth: usize,
) -> Result<Vec<TemplatePart>, ScanError> {
if depth >= MAX_NESTING {
return Err(ScanError::NestingTooDeep {
byte_offset: 0,
what: "template",
});
}
let mut parts = Vec::new();
let mut lit = String::new();
let mut lit_run: Option<(usize, usize)> = None;
let mut cur = Scan::new(interior);
while let Some((at, c)) = cur.peek() {
match c {
'{' => {
flush(&mut lit, &mut lit_run, &mut parts);
let (name, body_text, body_at) = capture_extent(&mut cur, at)?;
let span = Span::new(at as u32, cur.pos() as u32);
let name_span = name.map(|(raw, raw_at)| trimmed_span(raw, raw_at));
let name = match name {
Some((raw, _)) => Some(capture_name(raw, at)?),
None => None,
};
let parser = crate::body::parse_capture_body(body_text, body_at, depth)?;
parts.push(TemplatePart::Capture {
name,
parser: Box::new(parser),
span,
name_span,
});
}
'\\' => {
cur.bump();
match escape(&mut cur, at)? {
Escape::Policy(ws) => {
flush(&mut lit, &mut lit_run, &mut parts);
parts.push(TemplatePart::Literal {
text: String::new(),
ws,
span: Span::new(at as u32, cur.pos() as u32),
});
}
Escape::Char(ch) => {
lit.push(ch);
extend_run(&mut lit_run, at, cur.pos());
}
}
}
_ => {
cur.bump();
lit.push(c);
extend_run(&mut lit_run, at, cur.pos());
}
}
}
flush(&mut lit, &mut lit_run, &mut parts);
Ok(parts)
}
fn extend_run(run: &mut Option<(usize, usize)>, at: usize, end: usize) {
match run {
Some((_, e)) => *e = end,
None => *run = Some((at, end)),
}
}
fn trimmed_span(raw: &str, base: usize) -> Span {
let lead = raw.len() - raw.trim_start().len();
let start = base + lead;
Span::new(start as u32, (start + raw.trim().len()) as u32)
}
fn flush(lit: &mut String, run: &mut Option<(usize, usize)>, parts: &mut Vec<TemplatePart>) {
if lit.is_empty() {
*run = None;
return;
}
let text = std::mem::take(lit);
let (run_start, run_end) = run.take().unwrap_or((0, 0));
let after_lead = text.trim_start_matches([' ', '\t']);
let lead = text.len() - after_lead.len();
let stripped = after_lead.trim_end_matches([' ', '\t']);
let trail = after_lead.len() - stripped.len();
let had_leading_run = lead > 0;
let had_trailing_run = !stripped.is_empty() && trail > 0;
let (text_start, text_end) = if stripped.is_empty() {
(run_start, run_end)
} else {
(run_start + lead, run_end - trail)
};
parts.push(TemplatePart::Literal {
text: stripped.to_string(),
ws: if had_leading_run {
WsPolicy::SpaceRun
} else {
WsPolicy::None
},
span: Span::new(text_start as u32, text_end as u32),
});
if had_trailing_run {
parts.push(TemplatePart::Literal {
text: String::new(),
ws: WsPolicy::SpaceRun,
span: Span::new(text_end as u32, run_end as u32),
});
}
}
enum Escape {
Policy(WsPolicy),
Char(char),
}
fn escape(cur: &mut Scan<'_>, at: usize) -> Result<Escape, ScanError> {
let invalid = |cur: &Scan<'_>| ScanError::InvalidEscape {
byte_offset: at,
seq: cur.src()[at..cur.pos()].to_string(),
};
let Some((_, c)) = cur.bump() else {
return Err(invalid(cur));
};
match c {
's' => {
if cur.eat('*') {
Ok(Escape::Policy(WsPolicy::ZeroOrMore))
} else if cur.eat('+') {
Ok(Escape::Policy(WsPolicy::OneOrMore))
} else {
cur.bump();
Err(invalid(cur))
}
}
'n' => Ok(Escape::Policy(WsPolicy::Newline)),
't' => Ok(Escape::Policy(WsPolicy::Tab)),
'x' => {
for _ in 0..2 {
if cur.peek().is_some() {
cur.bump();
}
}
if &cur.src()[at..cur.pos()] == "\\x20" {
Ok(Escape::Policy(WsPolicy::ExactSpace))
} else {
Err(invalid(cur))
}
}
'`' | '\\' => Ok(Escape::Char(c)),
_ => Err(invalid(cur)),
}
}
type CaptureExtent<'a> = (Option<(&'a str, usize)>, &'a str, usize);
fn capture_extent<'a>(cur: &mut Scan<'a>, open: usize) -> Result<CaptureExtent<'a>, ScanError> {
cur.bump(); let body_at = cur.pos();
let mut braces = 1usize;
let mut parens = 0usize;
let mut name_colon = None;
let close = loop {
let Some((at, c)) = cur.peek() else {
return Err(ScanError::UnterminatedCapture { byte_offset: open });
};
match c {
'"' => skip_string(cur)?,
'`' => {
take_template(cur)?;
}
'\\' => {
cur.bump();
cur.bump();
}
'{' => {
braces += 1;
if braces > MAX_NESTING {
return Err(ScanError::NestingTooDeep {
byte_offset: at,
what: "`{`",
});
}
cur.bump();
}
'}' => {
braces -= 1;
cur.bump();
if braces == 0 {
break at;
}
}
'(' => {
parens += 1;
if parens > MAX_NESTING {
return Err(ScanError::NestingTooDeep {
byte_offset: at,
what: "`(`",
});
}
cur.bump();
}
')' => {
if parens == 0 {
return Err(ScanError::MalformedCaptureBody {
byte_offset: at,
message: "unbalanced `)`".to_string(),
});
}
parens -= 1;
cur.bump();
}
':' => {
if braces == 1 && parens == 0 && name_colon.is_none() {
name_colon = Some(at);
}
cur.bump();
}
_ => {
cur.bump();
}
}
};
if parens != 0 {
return Err(ScanError::MalformedCaptureBody {
byte_offset: open,
message: "unbalanced `(`".to_string(),
});
}
let src = cur.src();
let body = &src[body_at..close];
if body.trim().is_empty() {
return Err(ScanError::EmptyCapture { byte_offset: open });
}
match name_colon {
Some(colon) => Ok((
Some((&src[body_at..colon], body_at)),
&src[colon + 1..close],
colon + 1,
)),
None => Ok((None, body, body_at)),
}
}
pub(crate) fn skip_string(cur: &mut Scan<'_>) -> Result<(), ScanError> {
let open = cur.pos();
match praxis_syntax::template::string_end(cur.src(), open) {
Some(end) => {
cur.advance_to(end);
Ok(())
}
None => Err(ScanError::MalformedCaptureBody {
byte_offset: open,
message: "unterminated string literal".to_string(),
}),
}
}
pub(crate) fn take_template<'a>(cur: &mut Scan<'a>) -> Result<&'a str, ScanError> {
let open = cur.pos();
match praxis_syntax::template::template_end(cur.src(), open) {
praxis_syntax::template::TemplateEnd::Closed(end) => {
cur.advance_to(end);
Ok(&cur.src()[open + 1..end - 1])
}
praxis_syntax::template::TemplateEnd::Unterminated(_) => {
Err(ScanError::MalformedCaptureBody {
byte_offset: open,
message: "unterminated nested template".to_string(),
})
}
}
}
fn capture_name(raw: &str, at: usize) -> Result<crate::ast::CaptureName, ScanError> {
crate::ast::CaptureName::parse(raw.trim()).map_err(|_| ScanError::InvalidCaptureName {
byte_offset: at,
name: raw.trim().to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{AtomicKind, ParserAst};
fn literals(parts: &[TemplatePart]) -> Vec<&str> {
parts
.iter()
.filter_map(|p| match p {
TemplatePart::Literal { text, .. } => Some(text.as_str()),
_ => None,
})
.collect()
}
fn policies(parts: &[TemplatePart]) -> Vec<WsPolicy> {
parts
.iter()
.filter_map(|p| match p {
TemplatePart::Literal { ws, .. } => Some(*ws),
_ => None,
})
.collect()
}
fn capture_kind(part: &TemplatePart) -> AtomicKind {
match part {
TemplatePart::Capture { parser, .. } => match parser.as_ref() {
ParserAst::Atomic { kind, .. } => *kind,
other => panic!("expected an atomic capture, got {other:?}"),
},
other => panic!("expected a capture, got {other:?}"),
}
}
#[test]
fn plain_literal_template() {
let parts = scan_template("hello").unwrap();
assert_eq!(parts.len(), 1);
match &parts[0] {
TemplatePart::Literal { text, .. } => assert_eq!(text, "hello"),
_ => panic!("expected literal"),
}
}
#[test]
fn single_anonymous_capture() {
let parts = scan_template("{int}").unwrap();
assert_eq!(parts.len(), 1);
match &parts[0] {
TemplatePart::Capture { name, .. } => assert!(name.is_none()),
_ => panic!("expected capture"),
}
}
#[test]
fn named_capture_with_literal() {
let parts = scan_template("{x:int},{y:int}").unwrap();
assert_eq!(parts.len(), 3);
match &parts[0] {
TemplatePart::Capture { name, .. } => {
assert_eq!(name.as_ref().map(|n| n.as_str()), Some("x"));
}
_ => panic!("expected capture"),
}
match &parts[1] {
TemplatePart::Literal { text, .. } => assert_eq!(text, ","),
_ => panic!("expected literal"),
}
match &parts[2] {
TemplatePart::Capture { name, .. } => {
assert_eq!(name.as_ref().map(|n| n.as_str()), Some("y"));
}
_ => panic!("expected capture"),
}
}
#[test]
fn a_literals_edge_whitespace_is_its_policy_and_not_its_text() {
let parts = scan_template("{x1:int},{y1:int} -> {x2:int},{y2:int}").unwrap();
assert_eq!(literals(&parts), vec![",", "->", "", ","]);
assert_eq!(
policies(&parts),
vec![
WsPolicy::None,
WsPolicy::SpaceRun,
WsPolicy::SpaceRun,
WsPolicy::None
],
"only the literals a run was written against carry SpaceRun"
);
let parts = scan_template("{a:int} a b {b:int}").unwrap();
match &parts[1] {
TemplatePart::Literal { text, ws, .. } => {
assert_eq!(text, "a b");
assert_eq!(*ws, WsPolicy::SpaceRun);
}
_ => panic!("expected literal"),
}
match &parts[2] {
TemplatePart::Literal { text, ws, .. } => {
assert!(text.is_empty());
assert_eq!(*ws, WsPolicy::SpaceRun);
}
_ => panic!("expected the trailing run's part"),
}
let parts = scan_template("x: {a:rest}").unwrap();
assert_eq!(literals(&parts), vec!["x:", ""]);
assert_eq!(
policies(&parts),
vec![WsPolicy::None, WsPolicy::SpaceRun],
"the run after `x:` is the policy, not text, and not nothing"
);
let parts = scan_template("{a:int} {b:int}").unwrap();
assert_eq!(parts.len(), 3);
match &parts[1] {
TemplatePart::Literal { text, ws, .. } => {
assert!(text.is_empty());
assert_eq!(*ws, WsPolicy::SpaceRun);
}
_ => panic!("expected literal"),
}
let parts = scan_template(r"{a:int}\s+{b:int}").unwrap();
match &parts[1] {
TemplatePart::Literal { text, ws, .. } => {
assert!(text.is_empty());
assert_eq!(*ws, WsPolicy::OneOrMore);
}
_ => panic!("expected ws literal"),
}
}
#[test]
fn whitespace_escape_policies() {
let parts = scan_template("a\\s*b").unwrap();
assert_eq!(parts.len(), 3);
match &parts[1] {
TemplatePart::Literal { ws, .. } => assert_eq!(*ws, WsPolicy::ZeroOrMore),
_ => panic!("expected ws literal"),
}
}
#[test]
fn unterminated_capture_errors() {
assert!(matches!(
scan_template("{int"),
Err(ScanError::UnterminatedCapture { .. })
));
}
#[test]
fn empty_capture_errors() {
assert!(matches!(
scan_template("{}"),
Err(ScanError::EmptyCapture { .. })
));
}
#[test]
fn escaped_backtick_is_literal() {
let parts = scan_template("a\\`b").unwrap();
assert_eq!(parts.len(), 1);
match &parts[0] {
TemplatePart::Literal { text, .. } => assert_eq!(text, "a`b"),
_ => panic!("expected literal"),
}
}
#[test]
fn regression_unicode_literal_text_is_preserved() {
let parts = scan_template("λ={int}").unwrap();
match &parts[0] {
TemplatePart::Literal { text, .. } => assert_eq!(text, "λ="),
_ => panic!("expected literal"),
}
}
#[test]
fn regression_trailing_backslash_is_an_invalid_escape() {
assert!(matches!(
scan_template("prefix\\"),
Err(ScanError::InvalidEscape { byte_offset: 6, .. })
));
}
#[test]
fn an_invalid_escape_reports_the_sequence_the_source_actually_wrote() {
for (src, expected) in [
(r"a\sq", r"\sq"),
(r"a\s", r"\s"),
(r"a\x2", r"\x2"),
(r"a\x21", r"\x21"),
(r"a\q", r"\q"),
("a\\", "\\"),
(r"a\λ", r"\λ"),
] {
match scan_template(src) {
Err(ScanError::InvalidEscape { seq, byte_offset }) => {
assert_eq!(seq, expected, "for {src:?}");
assert_eq!(
&src[byte_offset..byte_offset + seq.len()],
seq,
"`seq` must be the source's own text at `byte_offset`, for {src:?}"
);
}
other => panic!("{src:?} must be an invalid escape, got {other:?}"),
}
}
assert!(scan_template(r"a\x20b").is_ok());
assert!(scan_template(r"a\s*b").is_ok());
assert!(scan_template(r"a\s+b").is_ok());
}
#[test]
fn a_capture_name_is_the_languages_own_identifier() {
for (src, name) in [
("{λ:int}", "λ"),
("{日本語:int}", "日本語"),
("{_x9:int}", "_x9"),
] {
let parts = scan_template(src).unwrap();
match &parts[0] {
TemplatePart::Capture { name: got, .. } => {
assert_eq!(got.as_ref().map(|n| n.as_str()), Some(name), "for {src}");
}
other => panic!("{src} must be a named capture, got {other:?}"),
}
}
for src in ["{9x:int}", "{a b:int}", "{:int}", "{+:int}"] {
assert!(
matches!(
scan_template(src),
Err(ScanError::InvalidCaptureName { .. })
),
"{src} must report an invalid capture name"
);
}
let parts = scan_template("{int}").unwrap();
match &parts[0] {
TemplatePart::Capture { name, .. } => assert!(name.is_none()),
other => panic!("expected an anonymous capture, got {other:?}"),
}
}
#[test]
fn every_capture_keeps_its_own_parser() {
let parts = scan_template("{name:word},{port:int}").unwrap();
assert_eq!(capture_kind(&parts[0]), AtomicKind::Word);
assert_eq!(capture_kind(&parts[2]), AtomicKind::Int);
let parts = scan_template("{word} {int}").unwrap();
assert_eq!(capture_kind(&parts[0]), AtomicKind::Word);
assert_eq!(capture_kind(&parts[2]), AtomicKind::Int);
assert!(matches!(
scan_template("{value:intr}"),
Err(ScanError::UnknownCaptureKind { .. })
));
assert!(matches!(
scan_template("{intr}"),
Err(ScanError::UnknownCaptureKind { .. })
));
}
#[test]
fn a_capture_body_is_a_parser_expression() {
let parts = scan_template("Starting items: {items:csv(int)}").unwrap();
match &parts[2] {
TemplatePart::Capture { parser, .. } => {
assert!(matches!(parser.as_ref(), ParserAst::Csv { .. }));
}
other => panic!("expected a capture, got {other:?}"),
}
let parts = scan_template("{x:optional(int)}").unwrap();
match &parts[0] {
TemplatePart::Capture { parser, .. } => {
assert!(matches!(parser.as_ref(), ParserAst::Optional { .. }));
}
other => panic!("expected a capture, got {other:?}"),
}
let parts = scan_template(r#"{s:sep("-", int)}"#).unwrap();
match &parts[0] {
TemplatePart::Capture { name, parser, .. } => {
assert_eq!(name.as_ref().map(|n| n.as_str()), Some("s"));
match parser.as_ref() {
ParserAst::Sep { separator, .. } => assert_eq!(separator.as_str(), "-"),
other => panic!("expected Sep, got {other:?}"),
}
}
other => panic!("expected a capture, got {other:?}"),
}
for (body, expect) in [
(r#"{c:one_of("}")}"#, "}"),
(r#"{c:one_of("{")}"#, "{"),
(r#"{c:one_of("`")}"#, "`"),
] {
let parts = scan_template(body).unwrap();
match &parts[0] {
TemplatePart::Capture { parser, .. } => match parser.as_ref() {
ParserAst::OneOf { chars, .. } => assert_eq!(chars, expect, "{body}"),
other => panic!("expected OneOf, got {other:?}"),
},
other => panic!("expected a capture, got {other:?}"),
}
}
let parts = scan_template("{g:choice(A: word, B: int)}").unwrap();
match &parts[0] {
TemplatePart::Capture { name, parser, .. } => {
assert_eq!(name.as_ref().map(|n| n.as_str()), Some("g"));
match parser.as_ref() {
ParserAst::Choice { cases, .. } => assert_eq!(cases.len(), 2),
other => panic!("expected Choice, got {other:?}"),
}
}
other => panic!("expected a capture, got {other:?}"),
}
assert!(scan_template("{x:csv(int}").is_err());
assert!(scan_template("{x:csv(int, int)}").is_err());
assert!(scan_template("{x:frobnicate(int)}").is_err());
}
#[test]
fn every_span_is_the_text_it_names_even_inside_a_nested_template() {
fn text_at(interior: &str, span: praxis_source::Span) -> &str {
&interior[span.start().to_usize()..span.end().to_usize()]
}
fn capture_parser(part: &TemplatePart) -> &ParserAst {
match part {
TemplatePart::Capture { parser, .. } => parser,
other => panic!("expected a capture, got {other:?}"),
}
}
let interior = "x = {x:int}";
let parts = scan_template(interior).unwrap();
assert_eq!(
text_at(interior, capture_parser(&parts[2]).span()),
"int",
"a top-level capture's span"
);
let interior = "{g:choice(A: `{x:int}`, B: word)}";
let parts = scan_template(interior).unwrap();
let ParserAst::Choice { cases, span } = capture_parser(&parts[0]) else {
panic!("expected a choice");
};
assert_eq!(
text_at(interior, *span),
"choice(A: `{x:int}`, B: word)",
"the choice call's own span"
);
let ParserAst::Template {
parts: inner,
span: inner_span,
} = &cases[0].1
else {
panic!("expected a nested template");
};
assert_eq!(text_at(interior, *inner_span), "`{x:int}`");
assert_eq!(
text_at(interior, capture_parser(&inner[0]).span()),
"int",
"a capture inside a nested template — this is what was never rebased"
);
assert_eq!(
text_at(interior, cases[1].1.span()),
"word",
"the un-nested sibling, which was always right"
);
let interior = "{g:choice(A: `{x:csv(int, int)}`)}";
let err = scan_template(interior).unwrap_err();
assert_eq!(
err.byte_offset(),
interior.find("csv").unwrap(),
"the offset must name the `csv` that is wrong, not the `choice` around it"
);
}
#[test]
fn nesting_past_the_bound_is_an_error_and_not_a_stack_overflow() {
let deep = format!("{}{}", "{a:".repeat(2_000), "}".repeat(2_000));
assert!(
matches!(scan_template(&deep), Err(ScanError::NestingTooDeep { .. })),
"deep nesting must be refused before it recurses"
);
assert!(scan_template("{a:optional(csv(int))}").is_ok());
}
fn nested_interior(n: usize) -> String {
let mut interior = "{a:int}".to_string();
for _ in 1..n {
interior = format!("{{a:`{interior}`}}");
}
interior
}
#[test]
fn the_two_template_nesting_bounds_are_the_same_number_and_the_message_says_it() {
use praxis_syntax::template::{TemplateEnd, template_end};
let deepest = (1..=MAX_NESTING + 4)
.take_while(|n| scan_template(&nested_interior(*n)).is_ok())
.last()
.expect("one level at least");
assert_eq!(
deepest, MAX_NESTING,
"the scanner's effective limit must be MAX_NESTING, not half of it"
);
let err = scan_template(&nested_interior(MAX_NESTING + 1)).expect_err("one too deep");
assert!(
matches!(
err,
ScanError::NestingTooDeep {
what: "template",
..
}
),
"the {}-level nest must be refused as template nesting, got {err}",
MAX_NESTING + 1
);
let rendered = err.to_string();
let named: usize = rendered
.split_whitespace()
.find_map(|w| w.parse().ok())
.expect("the message names a limit");
assert_eq!(
named, deepest,
"the message says {named} and the checker enforces {deepest}: {rendered}"
);
assert_eq!(MAX_NESTING, praxis_syntax::MAX_TEMPLATE_NESTING);
let at_the_bound = format!("`{}`", nested_interior(MAX_NESTING));
assert_eq!(
template_end(&at_the_bound, 0),
TemplateEnd::Closed(at_the_bound.len()),
"the lexer delivers a {MAX_NESTING}-level template whole"
);
let parens = format!("{{a:{}int{}}}", "csv(".repeat(64), ")".repeat(64));
let err = scan_template(&parens).expect_err("too many parens");
assert!(
matches!(err, ScanError::NestingTooDeep { what: "`(`", .. }),
"a parenthesis bound must name parentheses, got {err}"
);
}
}