Properly recover from errors

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2026-03-05 17:33:19 +01:00
parent f4e8137e17
commit b07bef7904
4 changed files with 157 additions and 120 deletions

7
src/eval/mod.rs Normal file
View file

@ -0,0 +1,7 @@
pub struct EvalStack {
stack: Vec<Evaluation>,
}
pub enum Evaluation {
AppendContent { content: String },
}

View file

@ -6,6 +6,7 @@ use serde::Serialize;
use thiserror::Error; use thiserror::Error;
pub mod parser; pub mod parser;
pub mod eval;
#[derive(Debug, Error, Display)] #[derive(Debug, Error, Display)]
pub enum TempleError { pub enum TempleError {

View file

@ -8,13 +8,9 @@ use annotate_snippets::Snippet;
use winnow::LocatingSlice; use winnow::LocatingSlice;
use winnow::Parser; use winnow::Parser;
use winnow::RecoverableParser; use winnow::RecoverableParser;
use winnow::ascii::alpha1;
use winnow::ascii::multispace0;
use winnow::ascii::multispace1; use winnow::ascii::multispace1;
use winnow::combinator::alt; use winnow::combinator::alt;
use winnow::combinator::cut_err;
use winnow::combinator::eof; use winnow::combinator::eof;
use winnow::combinator::opt;
use winnow::combinator::peek; use winnow::combinator::peek;
use winnow::combinator::repeat_till; use winnow::combinator::repeat_till;
use winnow::combinator::terminated; use winnow::combinator::terminated;
@ -27,8 +23,10 @@ use winnow::stream::Location;
use winnow::stream::Recoverable; use winnow::stream::Recoverable;
use winnow::stream::Stream; use winnow::stream::Stream;
use winnow::token::any; use winnow::token::any;
use winnow::token::one_of;
use winnow::token::rest; use winnow::token::rest;
use winnow::token::take_until; use winnow::token::take_until;
use winnow::token::take_while;
type Input<'input> = Recoverable<LocatingSlice<&'input str>, ParseError>; type Input<'input> = Recoverable<LocatingSlice<&'input str>, ParseError>;
type PResult<'input, T> = Result<T, ParseError>; type PResult<'input, T> = Result<T, ParseError>;
@ -53,10 +51,11 @@ impl ParseFailure {
} }
pub fn to_report(&self) -> String { pub fn to_report(&self) -> String {
let mut report = String::new(); let reports = self
.errors
for error in &self.errors { .iter()
let rep = &[Level::ERROR .map(|error| {
Level::ERROR
.primary_title( .primary_title(
error error
.message .message
@ -68,20 +67,21 @@ impl ParseFailure {
AnnotationKind::Primary AnnotationKind::Primary
.span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)), .span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)),
), ),
)]; )
.elements(error.help.as_ref().map(|help| Level::HELP.message(help)))
})
.collect::<Vec<_>>();
let renderer = let renderer =
Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Unicode); Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Unicode);
report.push_str(&renderer.render(rep)); renderer.render(&reports)
}
report
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ParseError { pub struct ParseError {
pub(crate) message: Option<String>, pub(crate) message: Option<String>,
pub(crate) help: Option<String>,
pub(crate) span: Option<SourceSpan>, pub(crate) span: Option<SourceSpan>,
is_fatal: bool, is_fatal: bool,
@ -91,6 +91,7 @@ impl ParseError {
fn ctx() -> Self { fn ctx() -> Self {
ParseError { ParseError {
message: None, message: None,
help: None,
span: None, span: None,
is_fatal: false, is_fatal: false,
@ -101,6 +102,11 @@ impl ParseError {
self.message = Some(message.to_string()); self.message = Some(message.to_string());
self self
} }
fn help(mut self, help: &str) -> Self {
self.help = Some(help.to_string());
self
}
} }
impl ModalError for ParseError { impl ModalError for ParseError {
@ -138,6 +144,7 @@ impl<'input> AddContext<Input<'input>, ParseError> for ParseError {
context: ParseError, context: ParseError,
) -> Self { ) -> Self {
self.message = context.message.or(self.message); self.message = context.message.or(self.message);
self.help = context.help.or(self.help);
self self
} }
} }
@ -157,11 +164,7 @@ impl<'input> ParserError<Input<'input>> for ParseError {
type Inner = ParseError; type Inner = ParseError;
fn from_input(_input: &Input<'input>) -> Self { fn from_input(_input: &Input<'input>) -> Self {
ParseError { ParseError::ctx()
message: None,
span: None,
is_fatal: false,
}
} }
fn into_inner(self) -> winnow::Result<Self::Inner, Self> { fn into_inner(self) -> winnow::Result<Self::Inner, Self> {
@ -175,111 +178,131 @@ impl<'input> ParserError<Input<'input>> for ParseError {
#[derive(Debug)] #[derive(Debug)]
pub struct ParsedTemplate<'input> { pub struct ParsedTemplate<'input> {
content: Vec<TemplateChunk<'input>>, tokens: Vec<TemplateToken<'input>>,
} }
#[derive(Debug)] impl<'input> ParsedTemplate<'input> {
pub enum TemplateChunk<'input> { pub fn tokens(&self) -> &[TemplateToken<'input>] {
&self.tokens
}
}
#[derive(Debug, Clone)]
pub enum TemplateToken<'input> {
Content(&'input str), Content(&'input str),
Expression(InterpolateExpression<'input>), LeftDelim(&'input str),
} RightDelim(&'input str),
WantsOutput(&'input str),
#[derive(Debug)] Ident(&'input str),
pub struct InterpolateExpression<'input> { Whitespace(&'input str),
pub left_delim: &'input str, Invalid(&'input str),
pub wants_output: Option<&'input str>,
pub value: Box<TemplateExpression<'input>>,
pub right_delim: &'input str,
}
#[derive(Debug)]
pub struct TemplateExpression<'input> {
pub before_ws: &'input str,
pub expr: TemplateExpr<'input>,
pub after_ws: &'input str,
}
#[derive(Debug)]
pub enum TemplateExpr<'input> {
Variable(&'input str),
} }
pub fn parse(input: &str) -> Result<ParsedTemplate<'_>, ParseFailure> { pub fn parse(input: &str) -> Result<ParsedTemplate<'_>, ParseFailure> {
let (_remaining, val, errors) = parse_chunks.recoverable_parse(LocatingSlice::new(input)); let (_remaining, val, errors) = parse_tokens.recoverable_parse(LocatingSlice::new(input));
dbg!(&val);
if errors.is_empty() if errors.is_empty()
&& let Some(val) = val && let Some(val) = val
{ {
Ok(ParsedTemplate { content: val }) Ok(ParsedTemplate { tokens: val })
} else { } else {
Err(ParseFailure::from_errors(errors, input)) Err(ParseFailure::from_errors(errors, input))
} }
} }
fn parse_chunks<'input>(input: &mut Input<'input>) -> PResult<'input, Vec<TemplateChunk<'input>>> { fn parse_tokens<'input>(input: &mut Input<'input>) -> PResult<'input, Vec<TemplateToken<'input>>> {
repeat_till(0.., alt((parse_interpolate, parse_content)), eof) repeat_till(0.., alt((parse_interpolate, parse_content)), eof)
.map(|(v, _)| v) .map(|(v, _): (Vec<_>, _)| v.into_iter().flatten().collect())
.parse_next(input) .parse_next(input)
} }
fn parse_content<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateChunk<'input>> { fn parse_content<'input>(input: &mut Input<'input>) -> PResult<'input, Vec<TemplateToken<'input>>> {
alt((take_until(1.., "{{"), rest)) alt((take_until(1.., "{{"), rest))
.map(TemplateChunk::Content) .map(TemplateToken::Content)
.map(|v| vec![v])
.parse_next(input) .parse_next(input)
} }
fn parse_interpolate<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateChunk<'input>> { fn parse_interpolate<'input>(
let left_delim = "{{".parse_next(input)?; input: &mut Input<'input>,
let (wants_output, value, right_delim) = ) -> PResult<'input, Vec<TemplateToken<'input>>> {
cut_err((opt("="), parse_value.map(Box::new), "}}")).parse_next(input)?; let left_delim = "{{".map(TemplateToken::LeftDelim).parse_next(input)?;
Ok(TemplateChunk::Expression(InterpolateExpression { let get_tokens = repeat_till(1.., parse_interpolate_token, peek("}}"));
left_delim, let recover = take_until(0.., "}}").void();
wants_output,
value, let (inside_tokens, _): (Vec<_>, _) = get_tokens
right_delim, .resume_after(recover)
})) .with_taken()
.map(|(val, taken)| val.unwrap_or_else(|| (vec![TemplateToken::Invalid(taken)], "")))
.parse_next(input)?;
let right_delim = "}}".map(TemplateToken::RightDelim).parse_next(input)?;
let mut tokens = vec![left_delim];
tokens.extend(inside_tokens);
tokens.push(right_delim);
Ok(tokens)
} }
fn parse_value<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateExpression<'input>> { fn parse_interpolate_token<'input>(
let before_ws = multispace0(input)?; input: &mut Input<'input>,
) -> PResult<'input, TemplateToken<'input>> {
let expr = trace("parse_value", alt((parse_variable,))).parse_next(input)?; trace(
"parse_interpolate_token",
let after_ws = multispace0(input)?; alt((parse_ident, parse_whitespace)),
)
Ok(TemplateExpression { .parse_next(input)
before_ws,
expr,
after_ws,
})
} }
fn parse_variable<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateExpr<'input>> { fn parse_whitespace<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken<'input>> {
terminated(alpha1, ident_terminator_check) trace(
.map(TemplateExpr::Variable) "parse_whitespace",
.context(ParseError::ctx().msg("valid variables are alpha")) multispace1.map(TemplateToken::Whitespace),
)
.parse_next(input)
}
fn parse_ident<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken<'input>> {
let ident = ident.map(TemplateToken::Ident).parse_next(input)?;
ident_terminator_check
.context(
ParseError::ctx()
.msg("Invalid variable identifier")
.help("valid variable identifiers are alphanumeric"),
)
.value(ident)
.resume_after(bad_ident) .resume_after(bad_ident)
.map(|v| v.unwrap_or(TemplateExpr::Variable("BAD_VARIABLE"))) .with_taken()
.map(|(val, taken)| val.unwrap_or(TemplateToken::Invalid(taken)))
.parse_next(input) .parse_next(input)
} }
fn ident_terminator<'input>(input: &mut Input<'input>) -> PResult<'input, ()> { fn ident<'input>(input: &mut Input<'input>) -> PResult<'input, &'input str> {
alt((eof.void(), "{".void(), "}".void(), multispace1.void())).parse_next(input) take_while(1.., char::is_alphanumeric).parse_next(input)
}
fn ident_terminator_check<'input>(input: &mut Input<'input>) -> PResult<'input, ()> {
cut_err(peek(ident_terminator)).parse_next(input)
} }
fn bad_ident<'input>(input: &mut Input<'input>) -> PResult<'input, ()> { fn bad_ident<'input>(input: &mut Input<'input>) -> PResult<'input, ()> {
repeat_till(1.., any, peek(ident_terminator)) repeat_till(1.., any, ident_terminator_check)
.map(|((), _)| ()) .map(|((), _)| ())
.parse_next(input) .parse_next(input)
} }
fn ident_terminator_check<'input>(input: &mut Input<'input>) -> PResult<'input, ()> {
peek(ident_terminator).parse_next(input)
}
fn ident_terminator<'input>(input: &mut Input<'input>) -> PResult<'input, ()> {
alt((
eof.void(),
one_of(('{', '}')).void(),
one_of((' ', '\t', '\r', '\n')).void(),
))
.parse_next(input)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::parser::parse; use crate::parser::parse;
@ -292,7 +315,7 @@ mod tests {
insta::assert_debug_snapshot!(output, @r#" insta::assert_debug_snapshot!(output, @r#"
Ok( Ok(
ParsedTemplate { ParsedTemplate {
content: [ tokens: [
Content( Content(
"Hello There", "Hello There",
), ),
@ -310,23 +333,24 @@ mod tests {
insta::assert_debug_snapshot!(output, @r#" insta::assert_debug_snapshot!(output, @r#"
Ok( Ok(
ParsedTemplate { ParsedTemplate {
content: [ tokens: [
Content( Content(
"Hello ", "Hello ",
), ),
Expression( LeftDelim(
InterpolateExpression { "{{",
left_delim: "{{", ),
wants_output: None, Whitespace(
value: TemplateExpression { " ",
before_ws: " ", ),
expr: Variable( Ident(
"there", "there",
), ),
after_ws: " ", Whitespace(
}, " ",
right_delim: "}}", ),
}, RightDelim(
"}}",
), ),
], ],
}, },
@ -336,24 +360,27 @@ mod tests {
#[test] #[test]
fn parse_interpolate_bad() { fn parse_interpolate_bad() {
let input = "Hello {{ the2re }}"; let input = "Hello {{ the2re }} {{ the@re }}";
let output = parse(input); let output = parse(input);
insta::assert_debug_snapshot!(output, @r#" insta::assert_debug_snapshot!(output, @r#"
Err( Err(
ParseFailure { ParseFailure {
source: "Hello {{ the2re }}", source: "Hello {{ the2re }} {{ the@re }}",
errors: [ errors: [
ParseError { ParseError {
message: Some( message: Some(
"valid variables are alpha", "Invalid variable identifier",
),
help: Some(
"valid variable identifiers are alphanumeric",
), ),
span: Some( span: Some(
SourceSpan { SourceSpan {
range: 9..15, range: 25..28,
}, },
), ),
is_fatal: true, is_fatal: false,
}, },
], ],
}, },

View file

@ -2,7 +2,9 @@
source: src/parser/mod.rs source: src/parser/mod.rs
expression: error.to_report() expression: error.to_report()
--- ---
error: valid variables are alpha error: Invalid variable identifier
 ╭▸   ╭▸ 
1 │ Hello {{ the2re }} 1 │ Hello {{ the2re }} {{ the@re }}
╰╴ ━━━━━━ │ ━━━
│
╰ help: valid variable identifiers are alphanumeric