Start fixing error outputs
Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
parent
d6ac7af36b
commit
7f7bf5c98d
16 changed files with 319 additions and 21 deletions
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(missing_docs)]
|
||||
|
||||
use criterion::BenchmarkId;
|
||||
use criterion::Criterion;
|
||||
use criterion::criterion_group;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(missing_docs)]
|
||||
|
||||
use criterion::BenchmarkId;
|
||||
use criterion::Criterion;
|
||||
use criterion::criterion_group;
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ pub enum Instruction {
|
|||
slot: VariableSlot,
|
||||
},
|
||||
PushScope {
|
||||
#[expect(unused)]
|
||||
#[allow(unused)]
|
||||
inherit_parent: bool,
|
||||
},
|
||||
Abort,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ use annotate_snippets::Level;
|
|||
use annotate_snippets::Renderer;
|
||||
use annotate_snippets::Snippet;
|
||||
use thiserror::Error;
|
||||
use winnow::stream::Offset;
|
||||
|
||||
use crate::input::NomoInput;
|
||||
use crate::lexer::ParseError;
|
||||
use crate::parser::AstError;
|
||||
|
||||
/// An error occurred while producing an Ast
|
||||
#[derive(Debug, Error)]
|
||||
pub struct AstFailure {
|
||||
errors: Vec<AstError>,
|
||||
|
|
@ -26,6 +28,7 @@ impl AstFailure {
|
|||
AstFailure { errors }
|
||||
}
|
||||
|
||||
/// Create a CLI printable report
|
||||
pub fn to_report(&self, source: &str) -> String {
|
||||
let reports = self
|
||||
.errors
|
||||
|
|
@ -38,12 +41,14 @@ impl AstFailure {
|
|||
.as_deref()
|
||||
.unwrap_or("An error occurred while producing an Ast"),
|
||||
)
|
||||
.element(
|
||||
annotate_snippets::Snippet::source(source).annotation(
|
||||
annotate_snippets::AnnotationKind::Primary
|
||||
.span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)),
|
||||
.element(annotate_snippets::Snippet::source(source).annotation(
|
||||
annotate_snippets::AnnotationKind::Primary.span(
|
||||
constrain_without_whitespace(
|
||||
source,
|
||||
error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0),
|
||||
),
|
||||
),
|
||||
)
|
||||
))
|
||||
.elements(
|
||||
error
|
||||
.help
|
||||
|
|
@ -59,6 +64,18 @@ impl AstFailure {
|
|||
}
|
||||
}
|
||||
|
||||
fn constrain_without_whitespace(
|
||||
input: &str,
|
||||
range: std::ops::Range<usize>,
|
||||
) -> std::ops::Range<usize> {
|
||||
let trimmed = input[range].trim();
|
||||
let start = trimmed.offset_from(&input);
|
||||
let end = start + trimmed.len();
|
||||
|
||||
start..end
|
||||
}
|
||||
|
||||
/// An error occurred during lexing
|
||||
#[derive(Debug, Error)]
|
||||
pub struct ParseFailure {
|
||||
input: Arc<str>,
|
||||
|
|
@ -79,6 +96,7 @@ impl ParseFailure {
|
|||
}
|
||||
}
|
||||
|
||||
/// Produce a CLi printable report
|
||||
pub fn to_report(&self) -> String {
|
||||
let reports = self
|
||||
.errors
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ pub trait NomoFunction<T>: 'static + Send + Sync {
|
|||
|
||||
#[cfg(feature = "unstable-pub")]
|
||||
#[derive(Default)]
|
||||
#[expect(missing_docs)]
|
||||
pub struct FunctionMap {
|
||||
funcs: HashMap<String, ErasedNomoFunction>,
|
||||
}
|
||||
|
|
@ -45,6 +46,7 @@ pub(crate) struct FunctionMap {
|
|||
}
|
||||
|
||||
impl FunctionMap {
|
||||
#[expect(missing_docs)]
|
||||
pub fn register<NF: NomoFunction<T>, T>(&mut self, name: impl Into<String>, func: NF) {
|
||||
self.funcs
|
||||
.insert(name.into(), ErasedNomoFunction::erase(func));
|
||||
|
|
|
|||
|
|
@ -1,10 +1,3 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use annotate_snippets::AnnotationKind;
|
||||
use annotate_snippets::Level;
|
||||
use annotate_snippets::Renderer;
|
||||
use annotate_snippets::Snippet;
|
||||
use thiserror::Error;
|
||||
use winnow::LocatingSlice;
|
||||
use winnow::Parser;
|
||||
use winnow::RecoverableParser;
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ macro_rules! unstable_pub {
|
|||
($(#[$m:meta])* mod $name:ident) => {
|
||||
$(#[$m])*
|
||||
#[cfg(feature = "unstable-pub")]
|
||||
#[allow(missing_docs)]
|
||||
pub mod $name;
|
||||
#[cfg(not(feature = "unstable-pub"))]
|
||||
mod $name;
|
||||
|
|
|
|||
|
|
@ -104,13 +104,14 @@ impl FromRecoverableError<Input<'_>, AstError> for AstError {
|
|||
.filter(|t| t.kind() != TokenKind::Whitespace);
|
||||
let last = tokens.next();
|
||||
let first = tokens.last();
|
||||
|
||||
match (last, first) {
|
||||
(None, None) => None,
|
||||
(None, Some(single)) | (Some(single), None) => Some(SourceSpan {
|
||||
range: single.source().get_range(),
|
||||
}),
|
||||
(Some(last), Some(first)) => {
|
||||
let start = first.source().get_range().start;
|
||||
let start = first.source().get_range().end;
|
||||
let end = last.source().get_range().end;
|
||||
|
||||
Some(SourceSpan { range: start..end })
|
||||
|
|
@ -212,7 +213,7 @@ pub enum TemplateAstExpr<'input> {
|
|||
ElseConditional {
|
||||
expression: Option<Box<TemplateAstExpr<'input>>>,
|
||||
},
|
||||
#[expect(unused)]
|
||||
#[allow(unused)]
|
||||
Invalid(&'input [TemplateToken]),
|
||||
MathOperation {
|
||||
op: TokenOperator,
|
||||
|
|
@ -311,7 +312,7 @@ fn parse_action<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'in
|
|||
(parse_block(
|
||||
cut_err(not(repeat_till(
|
||||
0..,
|
||||
any,
|
||||
parse_expression,
|
||||
peek((ws, TokenKind::RightDelim)),
|
||||
)
|
||||
.map(|((), _)| ())))
|
||||
|
|
@ -342,7 +343,7 @@ fn parse_for_chain<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<
|
|||
|
||||
let (content, taken) = resume_after_cut(
|
||||
repeat_till(0.., parse_ast, loop_end),
|
||||
repeat_till(0.., any, parse_end).map(|((), _)| ()),
|
||||
repeat_till(0.., parse_ast, parse_end).map(|((), _)| ()),
|
||||
)
|
||||
.with_taken()
|
||||
.parse_next(input)?;
|
||||
|
|
@ -372,9 +373,11 @@ fn parse_for_loop<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'
|
|||
ws,
|
||||
TokenKind::For,
|
||||
ws,
|
||||
TokenKind::Ident,
|
||||
cut_err(TokenKind::Ident.context(AstError::ctx().msg("Expected identifier here"))),
|
||||
ws,
|
||||
TokenKind::In,
|
||||
cut_err(
|
||||
TokenKind::In.context(AstError::ctx().msg("Missing `in` in `for _ in <expr>`")),
|
||||
),
|
||||
ws,
|
||||
parse_expression.map(Box::new),
|
||||
)
|
||||
|
|
@ -601,11 +604,34 @@ where
|
|||
fn parse_operand<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
|
||||
trace(
|
||||
"operand",
|
||||
alt((parse_function, parse_variable_access, parse_literal)),
|
||||
alt((
|
||||
parse_keywords_fail,
|
||||
parse_function,
|
||||
parse_variable_access,
|
||||
parse_literal,
|
||||
)),
|
||||
)
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn parse_keywords_fail<'input>(
|
||||
input: &mut Input<'input>,
|
||||
) -> Result<TemplateAstExpr<'input>, AstError> {
|
||||
let value = alt((
|
||||
TokenKind::ConditionalIf,
|
||||
TokenKind::For,
|
||||
TokenKind::End,
|
||||
TokenKind::In,
|
||||
))
|
||||
.take()
|
||||
.map(TemplateAstExpr::Invalid)
|
||||
.parse_next(input)?;
|
||||
|
||||
cut_err(fail::<_, (), _>.context(AstError::ctx().msg("Found literal, expected expression")))
|
||||
.value(value)
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn parse_literal<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
|
||||
trace(
|
||||
"literal",
|
||||
|
|
@ -895,7 +921,7 @@ mod tests {
|
|||
help: None,
|
||||
span: Some(
|
||||
SourceSpan {
|
||||
range: 0..6,
|
||||
range: 2..6,
|
||||
},
|
||||
),
|
||||
is_fatal: false,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(missing_docs)]
|
||||
|
||||
#[test]
|
||||
fn check_files() {
|
||||
let files = std::fs::read_dir("tests/checks/").unwrap();
|
||||
|
|
|
|||
106
tests/errors/invalid_controls.1-parsed.snap
Normal file
106
tests/errors/invalid_controls.1-parsed.snap
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
---
|
||||
source: tests/file_tests.rs
|
||||
expression: parsed
|
||||
info:
|
||||
input: "{{ if }} {{ end }}\n{{ if if }} {{ end }}\n{{ if if }} {{ for foo in bar }} {{ end }} {{ end }}\n{{ for in bar }} {{ end }}\n{{ for blah bar }} {{ end }}\n{{ else }}"
|
||||
context: {}
|
||||
---
|
||||
ParsedTemplate {
|
||||
tokens: [
|
||||
[LeftDelim]"{{" (0..2),
|
||||
[Whitespace]" " (2..3),
|
||||
[ConditionalIf]"if" (3..5),
|
||||
[Whitespace]" " (5..6),
|
||||
[RightDelim]"}}" (6..8),
|
||||
[Whitespace]" " (8..9),
|
||||
[LeftDelim]"{{" (9..11),
|
||||
[Whitespace]" " (11..12),
|
||||
[End]"end" (12..15),
|
||||
[Whitespace]" " (15..16),
|
||||
[RightDelim]"}}" (16..18),
|
||||
[Whitespace]"\n" (18..19),
|
||||
[LeftDelim]"{{" (19..21),
|
||||
[Whitespace]" " (21..22),
|
||||
[ConditionalIf]"if" (22..24),
|
||||
[Whitespace]" " (24..25),
|
||||
[ConditionalIf]"if" (25..27),
|
||||
[Whitespace]" " (27..28),
|
||||
[RightDelim]"}}" (28..30),
|
||||
[Whitespace]" " (30..31),
|
||||
[LeftDelim]"{{" (31..33),
|
||||
[Whitespace]" " (33..34),
|
||||
[End]"end" (34..37),
|
||||
[Whitespace]" " (37..38),
|
||||
[RightDelim]"}}" (38..40),
|
||||
[Whitespace]"\n" (40..41),
|
||||
[LeftDelim]"{{" (41..43),
|
||||
[Whitespace]" " (43..44),
|
||||
[ConditionalIf]"if" (44..46),
|
||||
[Whitespace]" " (46..47),
|
||||
[ConditionalIf]"if" (47..49),
|
||||
[Whitespace]" " (49..50),
|
||||
[RightDelim]"}}" (50..52),
|
||||
[Whitespace]" " (52..53),
|
||||
[LeftDelim]"{{" (53..55),
|
||||
[Whitespace]" " (55..56),
|
||||
[For]"for" (56..59),
|
||||
[Whitespace]" " (59..60),
|
||||
[Ident]"foo" (60..63),
|
||||
[Whitespace]" " (63..64),
|
||||
[In]"in" (64..66),
|
||||
[Whitespace]" " (66..67),
|
||||
[Ident]"bar" (67..70),
|
||||
[Whitespace]" " (70..71),
|
||||
[RightDelim]"}}" (71..73),
|
||||
[Whitespace]" " (73..74),
|
||||
[LeftDelim]"{{" (74..76),
|
||||
[Whitespace]" " (76..77),
|
||||
[End]"end" (77..80),
|
||||
[Whitespace]" " (80..81),
|
||||
[RightDelim]"}}" (81..83),
|
||||
[Whitespace]" " (83..84),
|
||||
[LeftDelim]"{{" (84..86),
|
||||
[Whitespace]" " (86..87),
|
||||
[End]"end" (87..90),
|
||||
[Whitespace]" " (90..91),
|
||||
[RightDelim]"}}" (91..93),
|
||||
[Whitespace]"\n" (93..94),
|
||||
[LeftDelim]"{{" (94..96),
|
||||
[Whitespace]" " (96..97),
|
||||
[For]"for" (97..100),
|
||||
[Whitespace]" " (100..101),
|
||||
[In]"in" (101..103),
|
||||
[Whitespace]" " (103..104),
|
||||
[Ident]"bar" (104..107),
|
||||
[Whitespace]" " (107..108),
|
||||
[RightDelim]"}}" (108..110),
|
||||
[Whitespace]" " (110..111),
|
||||
[LeftDelim]"{{" (111..113),
|
||||
[Whitespace]" " (113..114),
|
||||
[End]"end" (114..117),
|
||||
[Whitespace]" " (117..118),
|
||||
[RightDelim]"}}" (118..120),
|
||||
[Whitespace]"\n" (120..121),
|
||||
[LeftDelim]"{{" (121..123),
|
||||
[Whitespace]" " (123..124),
|
||||
[For]"for" (124..127),
|
||||
[Whitespace]" " (127..128),
|
||||
[Ident]"blah" (128..132),
|
||||
[Whitespace]" " (132..133),
|
||||
[Ident]"bar" (133..136),
|
||||
[Whitespace]" " (136..137),
|
||||
[RightDelim]"}}" (137..139),
|
||||
[Whitespace]" " (139..140),
|
||||
[LeftDelim]"{{" (140..142),
|
||||
[Whitespace]" " (142..143),
|
||||
[End]"end" (143..146),
|
||||
[Whitespace]" " (146..147),
|
||||
[RightDelim]"}}" (147..149),
|
||||
[Whitespace]"\n" (149..150),
|
||||
[LeftDelim]"{{" (150..152),
|
||||
[Whitespace]" " (152..153),
|
||||
[ConditionalElse]"else" (153..157),
|
||||
[Whitespace]" " (157..158),
|
||||
[RightDelim]"}}" (158..160),
|
||||
],
|
||||
}
|
||||
32
tests/errors/invalid_controls.2-ast.snap
Normal file
32
tests/errors/invalid_controls.2-ast.snap
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
source: tests/file_tests.rs
|
||||
expression: ast
|
||||
info:
|
||||
input: "{{ if }} {{ end }}\n{{ if if }} {{ end }}\n{{ if if }} {{ for foo in bar }} {{ end }} {{ end }}\n{{ for in bar }} {{ end }}\n{{ for blah bar }} {{ end }}\n{{ else }}"
|
||||
context: {}
|
||||
---
|
||||
[1m[91merror[0m[1m: Expected an expression after 'if'[0m
|
||||
[1m[94m ╭▸ [0m
|
||||
[1m[94m1[0m [1m[94m│[0m {{ if }} {{ end }}
|
||||
[1m[94m│[0m [1m[91m━━[0m
|
||||
[1m[94m╰╴[0m
|
||||
[1m[91merror[0m[1m: Expected an expression after 'if'[0m
|
||||
[1m[94m ╭▸ [0m
|
||||
[1m[94m2[0m [1m[94m│[0m {{ if if }} {{ end }}
|
||||
[1m[94m╰╴[0m [1m[91m━━[0m
|
||||
[1m[91merror[0m[1m: Expected an expression after 'if'[0m
|
||||
[1m[94m ╭▸ [0m
|
||||
[1m[94m3[0m [1m[94m│[0m {{ if if }} {{ for foo in bar }} {{ end }} {{ end }}
|
||||
[1m[94m╰╴[0m [1m[91m━━[0m
|
||||
[1m[91merror[0m[1m: Expected identifier here[0m
|
||||
[1m[94m ╭▸ [0m
|
||||
[1m[94m4[0m [1m[94m│[0m {{ for in bar }} {{ end }}
|
||||
[1m[94m╰╴[0m [1m[91m━━━━━━[0m
|
||||
[1m[91merror[0m[1m: Missing `in` in `for _ in <expr>`[0m
|
||||
[1m[94m ╭▸ [0m
|
||||
[1m[94m5[0m [1m[94m│[0m {{ for blah bar }} {{ end }}
|
||||
[1m[94m╰╴[0m [1m[91m━━━━━━━━[0m
|
||||
[1m[91merror[0m[1m: An error occurred while producing an Ast[0m
|
||||
[1m[94m ╭▸ [0m
|
||||
[1m[94m6[0m [1m[94m│[0m {{ else }}
|
||||
[1m[94m╰╴[0m [1m[91m━━━━━━━[0m
|
||||
8
tests/errors/invalid_controls.nomo
Normal file
8
tests/errors/invalid_controls.nomo
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{}
|
||||
---
|
||||
{{ if }} {{ end }}
|
||||
{{ if if }} {{ end }}
|
||||
{{ if if }} {{ for foo in bar }} {{ end }} {{ end }}
|
||||
{{ for in bar }} {{ end }}
|
||||
{{ for blah bar }} {{ end }}
|
||||
{{ else }}
|
||||
24
tests/errors/invalid_if.1-parsed.snap
Normal file
24
tests/errors/invalid_if.1-parsed.snap
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
source: tests/file_tests.rs
|
||||
expression: parsed
|
||||
info:
|
||||
input: "{{ if if }} {{ end }}"
|
||||
context: {}
|
||||
---
|
||||
ParsedTemplate {
|
||||
tokens: [
|
||||
[LeftDelim]"{{" (0..2),
|
||||
[Whitespace]" " (2..3),
|
||||
[ConditionalIf]"if" (3..5),
|
||||
[Whitespace]" " (5..6),
|
||||
[ConditionalIf]"if" (6..8),
|
||||
[Whitespace]" " (8..9),
|
||||
[RightDelim]"}}" (9..11),
|
||||
[Whitespace]" " (11..12),
|
||||
[LeftDelim]"{{" (12..14),
|
||||
[Whitespace]" " (14..15),
|
||||
[End]"end" (15..18),
|
||||
[Whitespace]" " (18..19),
|
||||
[RightDelim]"}}" (19..21),
|
||||
],
|
||||
}
|
||||
11
tests/errors/invalid_if.2-ast.snap
Normal file
11
tests/errors/invalid_if.2-ast.snap
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
source: tests/file_tests.rs
|
||||
expression: ast
|
||||
info:
|
||||
input: "{{ if if }} {{ end }}"
|
||||
context: {}
|
||||
---
|
||||
[1m[91merror[0m[1m: Expected an expression after 'if'[0m
|
||||
[1m[94m ╭▸ [0m
|
||||
[1m[94m1[0m [1m[94m│[0m {{ if if }} {{ end }}
|
||||
[1m[94m╰╴[0m [1m[91m━━[0m
|
||||
3
tests/errors/invalid_if.nomo
Normal file
3
tests/errors/invalid_if.nomo
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{}
|
||||
---
|
||||
{{ if if }} {{ end }}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
#![allow(missing_docs)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
|
|
@ -6,6 +8,8 @@ use nomo::functions::FunctionMap;
|
|||
|
||||
test_each_file::test_each_path! { for ["nomo"] in "./tests/cases/" as cases => check_for_input }
|
||||
|
||||
test_each_file::test_each_path! { for ["nomo"] in "./tests/errors/" as error_cases => check_errors }
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct Info {
|
||||
input: String,
|
||||
|
|
@ -63,3 +67,67 @@ fn check_for_input([path]: [&Path; 1]) {
|
|||
|
||||
insta::assert_debug_snapshot!(format!("{basename}.4-output"), output);
|
||||
}
|
||||
|
||||
fn check_errors([path]: [&Path; 1]) {
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.set_snapshot_path("errors");
|
||||
settings.set_prepend_module_to_snapshot(false);
|
||||
|
||||
let basename = path.file_stem().unwrap().to_string_lossy();
|
||||
let input = std::fs::read_to_string(path).unwrap();
|
||||
|
||||
let (context, input) = input.split_once("\n---\n").unwrap_or_else(|| ("", &input));
|
||||
|
||||
let map = if !context.is_empty() {
|
||||
serde_json::from_str::<HashMap<String, serde_json::Value>>(context).unwrap()
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
settings.set_info(&Info {
|
||||
input: input.to_string(),
|
||||
context: map.clone(),
|
||||
});
|
||||
|
||||
let mut context = Context::new();
|
||||
|
||||
for (k, v) in map {
|
||||
context.try_insert(k, v).unwrap();
|
||||
}
|
||||
|
||||
let _guard = settings.bind_to_scope();
|
||||
|
||||
let parsed = nomo::lexer::parse(input.into()).map_err(|err| err.to_report());
|
||||
|
||||
match &parsed {
|
||||
Ok(parsed) => insta::assert_debug_snapshot!(format!("{basename}.1-parsed"), parsed),
|
||||
Err(parsed) => insta::assert_snapshot!(format!("{basename}.1-parsed"), parsed),
|
||||
}
|
||||
|
||||
let Ok(parsed) = parsed else {
|
||||
return;
|
||||
};
|
||||
|
||||
let ast = nomo::parser::parse(parsed.tokens()).map_err(|err| err.to_report(input));
|
||||
|
||||
match &ast {
|
||||
Ok(ast) => insta::assert_debug_snapshot!(format!("{basename}.2-ast"), ast),
|
||||
Err(ast) => insta::assert_snapshot!(format!("{basename}.2-ast"), ast),
|
||||
}
|
||||
|
||||
let Ok(ast) = ast else {
|
||||
return;
|
||||
};
|
||||
|
||||
let emit = nomo::compiler::emit_machine(ast);
|
||||
|
||||
insta::assert_debug_snapshot!(format!("{basename}.3-instructions"), emit);
|
||||
|
||||
let output = nomo::eval::execute(&FunctionMap::default(), &emit, &context)
|
||||
.map_err(|err| err.to_string());
|
||||
|
||||
match &output {
|
||||
Ok(output) => insta::assert_debug_snapshot!(format!("{basename}.4-output"), output),
|
||||
Err(output) => insta::assert_snapshot!(format!("{basename}.4-output"), output),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue