Compare commits

...

2 commits

Author SHA1 Message Date
7f7bf5c98d Start fixing error outputs
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-16 11:22:29 +01:00
d6ac7af36b Fix typo on errors mod
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-16 10:06:51 +01:00
16 changed files with 320 additions and 22 deletions

View file

@ -1,3 +1,5 @@
#![allow(missing_docs)]
use criterion::BenchmarkId; use criterion::BenchmarkId;
use criterion::Criterion; use criterion::Criterion;
use criterion::criterion_group; use criterion::criterion_group;

View file

@ -1,3 +1,5 @@
#![allow(missing_docs)]
use criterion::BenchmarkId; use criterion::BenchmarkId;
use criterion::Criterion; use criterion::Criterion;
use criterion::criterion_group; use criterion::criterion_group;

View file

@ -63,7 +63,7 @@ pub enum Instruction {
slot: VariableSlot, slot: VariableSlot,
}, },
PushScope { PushScope {
#[expect(unused)] #[allow(unused)]
inherit_parent: bool, inherit_parent: bool,
}, },
Abort, Abort,

View file

@ -5,11 +5,13 @@ use annotate_snippets::Level;
use annotate_snippets::Renderer; use annotate_snippets::Renderer;
use annotate_snippets::Snippet; use annotate_snippets::Snippet;
use thiserror::Error; use thiserror::Error;
use winnow::stream::Offset;
use crate::input::NomoInput; use crate::input::NomoInput;
use crate::lexer::ParseError; use crate::lexer::ParseError;
use crate::parser::AstError; use crate::parser::AstError;
/// An error occurred while producing an Ast
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub struct AstFailure { pub struct AstFailure {
errors: Vec<AstError>, errors: Vec<AstError>,
@ -26,6 +28,7 @@ impl AstFailure {
AstFailure { errors } AstFailure { errors }
} }
/// Create a CLI printable report
pub fn to_report(&self, source: &str) -> String { pub fn to_report(&self, source: &str) -> String {
let reports = self let reports = self
.errors .errors
@ -38,12 +41,14 @@ impl AstFailure {
.as_deref() .as_deref()
.unwrap_or("An error occurred while producing an Ast"), .unwrap_or("An error occurred while producing an Ast"),
) )
.element( .element(annotate_snippets::Snippet::source(source).annotation(
annotate_snippets::Snippet::source(source).annotation( annotate_snippets::AnnotationKind::Primary.span(
annotate_snippets::AnnotationKind::Primary constrain_without_whitespace(
.span(error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0)), source,
error.span.clone().map(|s| s.range).unwrap_or_else(|| 0..0),
), ),
) ),
))
.elements( .elements(
error error
.help .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)] #[derive(Debug, Error)]
pub struct ParseFailure { pub struct ParseFailure {
input: Arc<str>, input: Arc<str>,
@ -79,6 +96,7 @@ impl ParseFailure {
} }
} }
/// Produce a CLi printable report
pub fn to_report(&self) -> String { pub fn to_report(&self) -> String {
let reports = self let reports = self
.errors .errors

View file

@ -34,6 +34,7 @@ pub trait NomoFunction<T>: 'static + Send + Sync {
#[cfg(feature = "unstable-pub")] #[cfg(feature = "unstable-pub")]
#[derive(Default)] #[derive(Default)]
#[expect(missing_docs)]
pub struct FunctionMap { pub struct FunctionMap {
funcs: HashMap<String, ErasedNomoFunction>, funcs: HashMap<String, ErasedNomoFunction>,
} }
@ -45,6 +46,7 @@ pub(crate) struct FunctionMap {
} }
impl FunctionMap { impl FunctionMap {
#[expect(missing_docs)]
pub fn register<NF: NomoFunction<T>, T>(&mut self, name: impl Into<String>, func: NF) { pub fn register<NF: NomoFunction<T>, T>(&mut self, name: impl Into<String>, func: NF) {
self.funcs self.funcs
.insert(name.into(), ErasedNomoFunction::erase(func)); .insert(name.into(), ErasedNomoFunction::erase(func));

View file

@ -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::LocatingSlice;
use winnow::Parser; use winnow::Parser;
use winnow::RecoverableParser; use winnow::RecoverableParser;

View file

@ -113,6 +113,7 @@ macro_rules! unstable_pub {
($(#[$m:meta])* mod $name:ident) => { ($(#[$m:meta])* mod $name:ident) => {
$(#[$m])* $(#[$m])*
#[cfg(feature = "unstable-pub")] #[cfg(feature = "unstable-pub")]
#[allow(missing_docs)]
pub mod $name; pub mod $name;
#[cfg(not(feature = "unstable-pub"))] #[cfg(not(feature = "unstable-pub"))]
mod $name; mod $name;
@ -136,7 +137,7 @@ unstable_pub!(
mod parser mod parser
); );
/// Nomo Functions /// Errors in this library
pub mod errors; pub mod errors;
/// Nomo Functions /// Nomo Functions

View file

@ -104,13 +104,14 @@ impl FromRecoverableError<Input<'_>, AstError> for AstError {
.filter(|t| t.kind() != TokenKind::Whitespace); .filter(|t| t.kind() != TokenKind::Whitespace);
let last = tokens.next(); let last = tokens.next();
let first = tokens.last(); let first = tokens.last();
match (last, first) { match (last, first) {
(None, None) => None, (None, None) => None,
(None, Some(single)) | (Some(single), None) => Some(SourceSpan { (None, Some(single)) | (Some(single), None) => Some(SourceSpan {
range: single.source().get_range(), range: single.source().get_range(),
}), }),
(Some(last), Some(first)) => { (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; let end = last.source().get_range().end;
Some(SourceSpan { range: start..end }) Some(SourceSpan { range: start..end })
@ -212,7 +213,7 @@ pub enum TemplateAstExpr<'input> {
ElseConditional { ElseConditional {
expression: Option<Box<TemplateAstExpr<'input>>>, expression: Option<Box<TemplateAstExpr<'input>>>,
}, },
#[expect(unused)] #[allow(unused)]
Invalid(&'input [TemplateToken]), Invalid(&'input [TemplateToken]),
MathOperation { MathOperation {
op: TokenOperator, op: TokenOperator,
@ -311,7 +312,7 @@ fn parse_action<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'in
(parse_block( (parse_block(
cut_err(not(repeat_till( cut_err(not(repeat_till(
0.., 0..,
any, parse_expression,
peek((ws, TokenKind::RightDelim)), peek((ws, TokenKind::RightDelim)),
) )
.map(|((), _)| ()))) .map(|((), _)| ())))
@ -342,7 +343,7 @@ fn parse_for_chain<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<
let (content, taken) = resume_after_cut( let (content, taken) = resume_after_cut(
repeat_till(0.., parse_ast, loop_end), repeat_till(0.., parse_ast, loop_end),
repeat_till(0.., any, parse_end).map(|((), _)| ()), repeat_till(0.., parse_ast, parse_end).map(|((), _)| ()),
) )
.with_taken() .with_taken()
.parse_next(input)?; .parse_next(input)?;
@ -372,9 +373,11 @@ fn parse_for_loop<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'
ws, ws,
TokenKind::For, TokenKind::For,
ws, ws,
TokenKind::Ident, cut_err(TokenKind::Ident.context(AstError::ctx().msg("Expected identifier here"))),
ws, ws,
TokenKind::In, cut_err(
TokenKind::In.context(AstError::ctx().msg("Missing `in` in `for _ in <expr>`")),
),
ws, ws,
parse_expression.map(Box::new), parse_expression.map(Box::new),
) )
@ -601,11 +604,34 @@ where
fn parse_operand<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> { fn parse_operand<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
trace( trace(
"operand", "operand",
alt((parse_function, parse_variable_access, parse_literal)), alt((
parse_keywords_fail,
parse_function,
parse_variable_access,
parse_literal,
)),
) )
.parse_next(input) .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> { fn parse_literal<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
trace( trace(
"literal", "literal",
@ -895,7 +921,7 @@ mod tests {
help: None, help: None,
span: Some( span: Some(
SourceSpan { SourceSpan {
range: 0..6, range: 2..6,
}, },
), ),
is_fatal: false, is_fatal: false,

View file

@ -1,3 +1,5 @@
#![allow(missing_docs)]
#[test] #[test]
fn check_files() { fn check_files() {
let files = std::fs::read_dir("tests/checks/").unwrap(); let files = std::fs::read_dir("tests/checks/").unwrap();

View 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),
],
}

View 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: {}
---
error: Expected an expression after 'if'
 ╭▸ 
1 │ {{ if }} {{ end }}
│ ━━
╰╴
error: Expected an expression after 'if'
 ╭▸ 
2 │ {{ if if }} {{ end }}
╰╴ ━━
error: Expected an expression after 'if'
 ╭▸ 
3 │ {{ if if }} {{ for foo in bar }} {{ end }} {{ end }}
╰╴ ━━
error: Expected identifier here
 ╭▸ 
4 │ {{ for in bar }} {{ end }}
╰╴ ━━━━━━
error: Missing `in` in `for _ in <expr>`
 ╭▸ 
5 │ {{ for blah bar }} {{ end }}
╰╴ ━━━━━━━━
error: An error occurred while producing an Ast
 ╭▸ 
6 │ {{ else }}
╰╴ ━━━━━━━

View 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 }}

View 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),
],
}

View file

@ -0,0 +1,11 @@
---
source: tests/file_tests.rs
expression: ast
info:
input: "{{ if if }} {{ end }}"
context: {}
---
error: Expected an expression after 'if'
 ╭▸ 
1 │ {{ if if }} {{ end }}
╰╴ ━━

View file

@ -0,0 +1,3 @@
{}
---
{{ if if }} {{ end }}

View file

@ -1,3 +1,5 @@
#![allow(missing_docs)]
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; 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/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)] #[derive(serde::Serialize)]
struct Info { struct Info {
input: String, input: String,
@ -63,3 +67,67 @@ fn check_for_input([path]: [&Path; 1]) {
insta::assert_debug_snapshot!(format!("{basename}.4-output"), output); 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),
}
}