diff --git a/benches/asting.rs b/benches/asting.rs index 3e8d755..1528069 100644 --- a/benches/asting.rs +++ b/benches/asting.rs @@ -19,8 +19,8 @@ fn asting_benchmark(c: &mut Criterion) { parsing.throughput(criterion::Throughput::Bytes(input.len() as u64)); parsing.bench_with_input(BenchmarkId::from_parameter(size), &input, |b, input| { b.iter(|| { - let tokens = nomo::lexer::parse(input.clone()).unwrap(); - let _ast = nomo::parser::parse(tokens.tokens()).unwrap(); + let tokens = nomo::parser::parse(input.clone()).unwrap(); + let _ast = nomo::ast::parse(tokens.tokens()).unwrap(); }); }); } @@ -43,8 +43,8 @@ fn asting_nested(c: &mut Criterion) { parsing.throughput(criterion::Throughput::Bytes(input.len() as u64)); parsing.bench_with_input(BenchmarkId::from_parameter(size), &input, |b, input| { b.iter(|| { - let tokens = nomo::lexer::parse(input.clone()).unwrap(); - let _ast = nomo::parser::parse(tokens.tokens()).unwrap(); + let tokens = nomo::parser::parse(input.clone()).unwrap(); + let _ast = nomo::ast::parse(tokens.tokens()).unwrap(); }); }); } diff --git a/benches/parsing.rs b/benches/parsing.rs index 20548e6..35de7ae 100644 --- a/benches/parsing.rs +++ b/benches/parsing.rs @@ -18,7 +18,7 @@ fn parsing_benchmark(c: &mut Criterion) { parsing.throughput(criterion::Throughput::Bytes(input.len() as u64)); parsing.bench_with_input(BenchmarkId::from_parameter(size), &input, |b, input| { - b.iter(|| nomo::lexer::parse(input.clone()).unwrap()); + b.iter(|| nomo::parser::parse(input.clone()).unwrap()); }); } } @@ -39,7 +39,7 @@ fn parsing_nested(c: &mut Criterion) { parsing.throughput(criterion::Throughput::Bytes(input.len() as u64)); parsing.bench_with_input(BenchmarkId::from_parameter(size), &input, |b, input| { - b.iter(|| nomo::lexer::parse(input.clone()).unwrap()); + b.iter(|| nomo::parser::parse(input.clone()).unwrap()); }); } } diff --git a/fuzz/fuzz_targets/fuzz_target_1.rs b/fuzz/fuzz_targets/fuzz_target_1.rs index da1e6e2..56024b8 100644 --- a/fuzz/fuzz_targets/fuzz_target_1.rs +++ b/fuzz/fuzz_targets/fuzz_target_1.rs @@ -4,11 +4,11 @@ use libfuzzer_sys::Corpus; use libfuzzer_sys::fuzz_target; fuzz_target!(|data: String| -> Corpus { - let Ok(parsed) = nomo::lexer::parse(data.into()) else { + let Ok(parsed) = nomo::parser::parse(data.into()) else { return Corpus::Reject; }; - let Ok(ast) = nomo::parser::parse(parsed.tokens()) else { + let Ok(ast) = nomo::ast::parse(parsed.tokens()) else { return Corpus::Keep; }; diff --git a/src/ast/mod.rs b/src/ast/mod.rs new file mode 100644 index 0000000..55190b0 --- /dev/null +++ b/src/ast/mod.rs @@ -0,0 +1,1011 @@ +use thiserror::Error; +use winnow::Parser; +use winnow::RecoverableParser; +use winnow::combinator::Infix::Left; +use winnow::combinator::alt; +use winnow::combinator::cut_err; +use winnow::combinator::delimited; +use winnow::combinator::dispatch; +use winnow::combinator::expression; +use winnow::combinator::not; +use winnow::combinator::opt; +use winnow::combinator::peek; +use winnow::combinator::preceded; +use winnow::combinator::repeat; +use winnow::combinator::repeat_till; +use winnow::combinator::trace; +use winnow::error::AddContext; +use winnow::error::FromRecoverableError; +use winnow::error::ModalError; +use winnow::error::ParserError; +use winnow::stream::Offset; +use winnow::stream::Recoverable; +use winnow::stream::Stream; +use winnow::stream::TokenSlice; +use winnow::token::any; + +use crate::SourceSpan; +use crate::parser::TemplateToken; +use crate::parser::TokenKind; +use crate::parser::TokenOperator; +use crate::resume_after_cut; +use crate::value::NomoValue; + +#[derive(Debug, Clone)] +pub struct TemplateAst<'input> { + root: Vec>, +} + +impl TemplateAst<'_> { + pub fn root(&self) -> &[TemplateAstExpr<'_>] { + &self.root + } +} + +#[derive(Debug, Clone)] +pub struct AstError { + pub(crate) message: Option, + pub(crate) help: Option, + pub(crate) span: Option, + + is_fatal: bool, +} + +impl AstError { + fn ctx() -> Self { + AstError { + message: None, + help: None, + span: None, + + is_fatal: false, + } + } + + fn msg(mut self, message: &str) -> Self { + self.message = Some(message.to_string()); + self + } + + fn help(mut self, help: &str) -> Self { + self.help = Some(help.to_string()); + self + } +} + +impl ModalError for AstError { + fn cut(mut self) -> Self { + self.is_fatal = true; + self + } + + fn backtrack(mut self) -> Self { + self.is_fatal = false; + self + } +} + +impl FromRecoverableError, AstError> for AstError { + fn from_recoverable_error( + token_start: &::Checkpoint, + _err_start: &::Checkpoint, + input: &Input, + mut e: AstError, + ) -> Self { + e.span = e.span.or_else(|| { + let offset = input.offset_from(token_start); + + let mut tokens = input + .previous_tokens() + .take(offset) + .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 end = last.source().get_range().end; + + Some(SourceSpan { range: start..end }) + } + } + }); + + e + } +} + +impl AddContext, AstError> for AstError { + fn add_context( + mut self, + _input: &Input, + _token_start: &::Checkpoint, + context: AstError, + ) -> Self { + self.message = context.message.or(self.message); + self.help = context.help.or(self.help); + self + } +} + +impl ParserError> for AstError { + type Inner = AstError; + + fn from_input(_input: &Input<'_>) -> Self { + AstError::ctx() + } + + fn into_inner(self) -> winnow::Result { + Ok(self) + } + + fn is_backtrack(&self) -> bool { + !self.is_fatal + } +} + +#[derive(Debug, Error)] +pub struct AstFailure { + errors: Vec, +} + +impl std::fmt::Display for AstFailure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("TODO") + } +} + +impl AstFailure { + fn from_errors(errors: Vec) -> AstFailure { + AstFailure { errors } + } + + pub fn to_report(&self, source: &str) -> String { + let reports = self + .errors + .iter() + .map(|error| { + annotate_snippets::Level::ERROR + .primary_title( + error + .message + .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)), + ), + ) + .elements( + error + .help + .as_ref() + .map(|help| annotate_snippets::Level::HELP.message(help)), + ) + }) + .collect::>(); + + let renderer = annotate_snippets::Renderer::styled() + .decor_style(annotate_snippets::renderer::DecorStyle::Unicode); + renderer.render(&reports) + } +} + +type Input<'input> = Recoverable, AstError>; + +impl<'input> Parser, TemplateToken, AstError> for TokenKind { + fn parse_next(&mut self, input: &mut Input<'input>) -> winnow::Result { + winnow::token::literal(*self) + .parse_next(input) + .map(|t| t[0].clone()) + } +} + +pub fn parse(input: &[TemplateToken]) -> Result, AstFailure> { + let (_remaining, val, errors) = parse_asts.recoverable_parse(TokenSlice::new(input)); + + if errors.is_empty() + && let Some(val) = val + { + Ok(TemplateAst { root: val }) + } else { + Err(AstFailure::from_errors(errors)) + } +} + +#[derive(Debug, Clone)] +pub enum TemplateAstExpr<'input> { + StaticContent(TemplateToken), + Block { + prev_whitespace_content: Option, + expression: Box>, + post_whitespace_content: Option, + }, + Interpolation { + prev_whitespace_content: Option, + expression: Box>, + post_whitespace_content: Option, + }, + ConditionalChain { + chain: Vec>, + }, + ForChain { + for_block: Box>, + content: Vec>, + else_block: Option>>, + else_content: Option>>, + end_block: Box>, + }, + For { + value_ident: TemplateToken, + value_expression: Box>, + }, + ForElse, + VariableAccess(TemplateToken), + IfConditional { + expression: Box>, + }, + ConditionalContent { + content: Vec>, + }, + ElseConditional { + expression: Option>>, + }, + Invalid(&'input [TemplateToken]), + Operation { + op: TokenOperator, + lhs: Box>, + rhs: Box>, + }, + EndBlock, + Literal { + source: TemplateToken, + value: NomoValue, + }, +} + +fn parse_asts<'input>(input: &mut Input<'input>) -> Result>, AstError> { + repeat(0.., parse_ast).parse_next(input) +} +fn parse_ast<'input>(input: &mut Input<'input>) -> Result, AstError> { + alt(( + trace( + "content", + TokenKind::Content.map(TemplateAstExpr::StaticContent), + ), + trace("interpolation", parse_interpolation), + parse_action, + )) + .parse_next(input) +} + +fn parse_interpolation<'input>( + input: &mut Input<'input>, +) -> Result, AstError> { + let expr_parser = resume_after_cut( + parse_expression, + repeat_till( + 0.., + any, + peek(preceded( + opt(TokenKind::TrimWhitespace), + TokenKind::RightDelim, + )), + ) + .map(|((), _)| ()), + ) + .with_taken() + .map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken))); + let ( + prev_whitespace, + _left, + left_trim, + _wants_output, + (expression, right_trim, _right, post_whitespace), + ) = ( + opt(TokenKind::Whitespace), + TokenKind::LeftDelim, + opt(TokenKind::TrimWhitespace), + TokenKind::WantsOutput, + cut_err(( + surrounded(ws, expr_parser).map(Box::new), + opt(TokenKind::TrimWhitespace), + TokenKind::RightDelim, + opt(TokenKind::Whitespace), + )), + ) + .parse_next(input)?; + + Ok(TemplateAstExpr::Interpolation { + prev_whitespace_content: if left_trim.is_some() { + None + } else { + prev_whitespace + }, + expression, + post_whitespace_content: if right_trim.is_some() { + None + } else { + post_whitespace + }, + }) +} + +fn parse_action<'input>(input: &mut Input<'input>) -> Result, AstError> { + trace( + "action", + alt(( + parse_conditional_chain, + parse_for_chain, + (parse_block( + cut_err(not(repeat_till( + 0.., + any, + peek((ws, TokenKind::RightDelim)), + ) + .map(|((), _)| ()))) + .context( + AstError::ctx() + .msg("Standlone action block") + .help("If you want to output this expression, add a '=' to the block"), + ) + .take() + .map(TemplateAstExpr::Invalid), + )), + )), + ) + .parse_next(input) +} + +fn parse_for_chain<'input>(input: &mut Input<'input>) -> Result, AstError> { + trace("for_loop", |input: &mut Input<'input>| { + let for_block = parse_for_loop.map(Box::new).parse_next(input)?; + + let loop_end = ( + opt(( + parse_loop_else.map(Box::new), + repeat_till(0.., parse_ast, peek(parse_end)).map(|(val, _)| val), + )), + parse_end.map(Box::new), + ); + + let (content, taken) = resume_after_cut( + repeat_till(0.., parse_ast, loop_end), + repeat_till(0.., any, parse_end).map(|((), _)| ()), + ) + .with_taken() + .parse_next(input)?; + + let Some((content, (else_stuff, end_block))) = content else { + return Ok(TemplateAstExpr::Invalid(taken)); + }; + + let (else_block, else_content) = else_stuff.unzip(); + + Ok(TemplateAstExpr::ForChain { + for_block, + content, + else_block, + else_content, + end_block, + }) + }) + .parse_next(input) +} + +fn parse_for_loop<'input>(input: &mut Input<'input>) -> Result, AstError> { + trace( + "for_loop_inner", + parse_block( + ( + ws, + TokenKind::For, + ws, + TokenKind::Ident, + ws, + TokenKind::In, + ws, + parse_expression.map(Box::new), + ) + .map(|(_, _for, _, value_ident, _, _in, _, value_expression)| { + TemplateAstExpr::For { + value_ident, + value_expression, + } + }), + ), + ) + .parse_next(input) +} + +fn parse_conditional_chain<'input>( + input: &mut Input<'input>, +) -> Result, AstError> { + trace("conditional_chain", |input: &mut Input<'input>| { + let if_block = parse_conditional_if.parse_next(input)?; + let mut chain = vec![]; + + chain.push(if_block); + + let content = resume_after_cut( + cut_err(inner_conditional_chain), + repeat_till(0.., any, parse_end).map(|((), _)| ()), + ) + .parse_next(input)?; + + chain.extend(content.into_iter().flatten()); + + Ok(TemplateAstExpr::ConditionalChain { chain }) + }) + .parse_next(input) +} + +fn inner_conditional_chain<'input>( + input: &mut Input<'input>, +) -> Result>, AstError> { + let mut needs_end = false; + + let mut chain = vec![]; + + loop { + let (content, end_block): (Vec<_>, _) = repeat_till( + 0.., + parse_ast, + trace( + "conditional_chain_else/end", + alt((parse_end, parse_conditional_else)), + ), + ) + .parse_next(input)?; + + chain.push(TemplateAstExpr::ConditionalContent { content }); + + let is_end = if let TemplateAstExpr::Block { ref expression, .. } = end_block + && let TemplateAstExpr::EndBlock = &**expression + { + true + } else { + false + }; + + if !is_end && needs_end { + return Err(AstError::from_input(input)); + } + + if let TemplateAstExpr::Block { expression, .. } = &end_block + && let TemplateAstExpr::ElseConditional { expression: None } = &**expression + { + needs_end = true; + } + + chain.push(end_block); + + if is_end { + break; + } + } + + Ok(chain) +} + +fn parse_conditional_if<'input>( + input: &mut Input<'input>, +) -> Result, AstError> { + trace( + "conditional", + parse_block( + preceded( + TokenKind::ConditionalIf, + cut_err( + surrounded(ws, parse_expression) + .map(Box::new) + .context(AstError::ctx().msg("Expected an expression after 'if'")), + ), + ) + .map(|expression| TemplateAstExpr::IfConditional { expression }), + ), + ) + .parse_next(input) +} + +fn parse_conditional_else<'input>( + input: &mut Input<'input>, +) -> Result, AstError> { + trace( + "conditional_else", + parse_block( + preceded( + surrounded(ws, TokenKind::ConditionalElse), + opt(preceded( + TokenKind::ConditionalIf, + cut_err(surrounded(ws, parse_expression)).map(Box::new), + )), + ) + .map(|else_expr| TemplateAstExpr::ElseConditional { + expression: else_expr, + }), + ), + ) + .parse_next(input) +} + +fn parse_loop_else<'input>(input: &mut Input<'input>) -> Result, AstError> { + trace( + "end", + parse_block( + TokenKind::ConditionalElse + .value(TemplateAstExpr::ForElse) + .context(AstError::ctx().msg("Expected an else block here")), + ), + ) + .parse_next(input) +} + +fn parse_end<'input>(input: &mut Input<'input>) -> Result, AstError> { + trace( + "end", + parse_block( + TokenKind::End + .value(TemplateAstExpr::EndBlock) + .context(AstError::ctx().msg("Expected an end block here")), + ), + ) + .parse_next(input) +} + +fn parse_variable_access<'input>( + input: &mut Input<'input>, +) -> Result, AstError> { + trace( + "variable_access", + TokenKind::Ident.map(TemplateAstExpr::VariableAccess), + ) + .parse_next(input) +} + +fn parse_block<'input, ParseNext>( + parser: ParseNext, +) -> impl Parser, TemplateAstExpr<'input>, AstError> +where + ParseNext: Parser, TemplateAstExpr<'input>, AstError>, +{ + let expr_parser = resume_after_cut( + parser, + repeat_till(0.., any, peek(TokenKind::RightDelim)).map(|((), _)| ()), + ) + .with_taken() + .map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken))); + + ( + opt(TokenKind::Whitespace), + TokenKind::LeftDelim, + opt(TokenKind::TrimWhitespace), + not(TokenKind::WantsOutput), + ( + surrounded(ws, expr_parser.map(Box::new)), + opt(TokenKind::TrimWhitespace), + TokenKind::RightDelim, + opt(TokenKind::Whitespace), + ), + ) + .map( + |( + prev_whitespace, + _left, + left_trim, + _not_token, + (expression, right_trim, _right, post_whitespace), + )| { + TemplateAstExpr::Block { + prev_whitespace_content: if left_trim.is_some() { + None + } else { + prev_whitespace + }, + expression, + post_whitespace_content: if right_trim.is_some() { + None + } else { + post_whitespace + }, + } + }, + ) +} + +fn parse_operand<'input>(input: &mut Input<'input>) -> Result, AstError> { + trace("operand", alt((parse_variable_access, parse_literal))).parse_next(input) +} + +fn parse_literal<'input>(input: &mut Input<'input>) -> Result, AstError> { + trace( + "literal", + any.verify_map(|token: &TemplateToken| match token.kind() { + TokenKind::Literal(literal) => Some(TemplateAstExpr::Literal { + source: token.clone(), + value: match literal { + crate::parser::TokenLiteral::Bool(bool) => NomoValue::Bool { value: bool }, + crate::parser::TokenLiteral::Integer(int) => NomoValue::Integer { value: int }, + }, + }), + _ => None, + }), + ) + .parse_next(input) +} + +fn parse_operator<'input>(input: &mut Input<'input>) -> Result { + trace( + "operator", + any.verify_map(|t: &'input TemplateToken| match t.kind() { + TokenKind::Operator(op) => Some(op), + _ => None, + }), + ) + .parse_next(input) +} + +fn parse_expression<'input>( + input: &mut Input<'input>, +) -> Result, AstError> { + macro_rules! infix { + ($parser:expr => [ $($side:tt $val:tt => $prec:expr),* $(,)? ]) => { + dispatch! { surrounded(ws, parse_operator); + $( + TokenOperator::$val => $side($prec, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::$val, lhs: Box::new(lhs), rhs: Box::new(rhs) })) + ),* + } + }; + } + trace( + "expression", + expression(surrounded(ws, parse_operand)).infix(infix! { + surrounded(ws, parse_operator) => [ + Left Plus => 18, + Left Minus => 18, + Left Times => 20, + Left Divide => 20, + Left And => 10, + Left Or => 7, + Left Equal => 12, + Left NotEqual => 12, + Left Greater => 15, + Left GreaterOrEqual => 15, + Left Lesser => 15, + Left LesserOrEqual => 15, + ] + }), + ) + .parse_next(input) +} + +fn ws<'input>(input: &mut Input<'input>) -> Result<(), AstError> { + repeat(.., TokenKind::Whitespace).parse_next(input) +} + +fn surrounded( + ignored: IgnoredParser, + parser: ParseNext, +) -> impl Parser +where + Input: Stream, + Error: ParserError, + IgnoredParser: Parser, + IgnoredParser: Clone, + ParseNext: Parser, +{ + delimited(ignored.clone(), parser, ignored) +} + +#[cfg(test)] +mod tests { + use winnow::Parser; + use winnow::combinator::alt; + use winnow::combinator::fail; + use winnow::stream::TokenSlice; + + use crate::ast::AstError; + use crate::ast::AstFailure; + use crate::ast::TemplateAst; + use crate::ast::TemplateAstExpr; + use crate::ast::parse; + use crate::ast::parse_block; + use crate::ast::parse_end; + use crate::parser::TokenKind; + + fn panic_pretty<'a>( + input: &'_ str, + tokens: Result, AstFailure>, + ) -> TemplateAst<'a> { + match tokens { + Ok(ast) => ast, + Err(failure) => { + panic!("{}", failure.to_report(input)); + } + } + } + + #[test] + fn check_only_content() { + let input = "Hello World"; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let ast = parse(parsed.tokens()).unwrap(); + + insta::assert_debug_snapshot!(ast, @r#" + TemplateAst { + root: [ + StaticContent( + [Content]"Hello World" (0..11), + ), + ], + } + "#); + } + + #[test] + fn check_simple_variable_interpolation() { + let input = "Hello {{= world }}"; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let ast = parse(parsed.tokens()).unwrap(); + + insta::assert_debug_snapshot!(ast, @r#" + TemplateAst { + root: [ + StaticContent( + [Content]"Hello" (0..5), + ), + Interpolation { + prev_whitespace_content: Some( + [Whitespace]" " (5..6), + ), + expression: VariableAccess( + [Ident]"world" (10..15), + ), + post_whitespace_content: None, + }, + ], + } + "#); + } + + #[test] + fn check_simple_if() { + let input = "{{ if foo }} Hiii {{ end }}"; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let ast = panic_pretty(input, parse(parsed.tokens())); + + insta::assert_debug_snapshot!(ast, @r#" + TemplateAst { + root: [ + ConditionalChain { + chain: [ + Block { + prev_whitespace_content: None, + expression: IfConditional { + expression: VariableAccess( + [Ident]"foo" (6..9), + ), + }, + post_whitespace_content: Some( + [Whitespace]" " (12..13), + ), + }, + ConditionalContent { + content: [ + StaticContent( + [Content]"Hiii" (13..17), + ), + ], + }, + Block { + prev_whitespace_content: Some( + [Whitespace]" " (17..18), + ), + expression: EndBlock, + post_whitespace_content: None, + }, + ], + }, + ], + } + "#); + } + + #[test] + fn check_invalid_action() { + let input = r#"{{ value }} + {{ value }} + {{ value }} + {{ value }} + {{ value }}"#; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let ast = parse(parsed.tokens()).unwrap_err(); + + insta::assert_snapshot!(ast.to_report(input)); + } + + #[test] + fn check_nested_simple_if() { + let input = r#"{{ if foo }} + {{ if bar }} + Hiii + {{ end }} + {{ end }} + + {{= value }} + "#; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + insta::assert_debug_snapshot!("simple_if_tokens", parsed); + + let ast = panic_pretty(input, parse(parsed.tokens())); + + insta::assert_debug_snapshot!("simple_if_ast", ast); + } + + #[test] + fn check_parsing_block() { + use winnow::RecoverableParser; + + let input = "{{ foo }}"; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let result = alt(( + parse_end, + parse_block( + (TokenKind::Ident.void(), fail::<_, (), _>) + .void() + .context(AstError::ctx().msg("No ident allowed")) + .take() + .map(TemplateAstExpr::Invalid), + ), + )) + .recoverable_parse(TokenSlice::new(parsed.tokens())); + + insta::assert_debug_snapshot!(result, @r#" + ( + [ + [LeftDelim]"{{" (0..2), + [Whitespace]" " (2..3), + [Ident]"foo" (3..6), + [Whitespace]" " (6..7), + [RightDelim]"}}" (7..9), + ], + None, + [ + AstError { + message: Some( + "No ident allowed", + ), + help: None, + span: Some( + SourceSpan { + range: 0..6, + }, + ), + is_fatal: false, + }, + ], + ) + "#); + } + + #[test] + fn check_empty_if_output() { + let input = "{{ if foo }}{{ end }}"; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let ast = panic_pretty(input, parse(parsed.tokens())); + + insta::assert_debug_snapshot!(ast, @r#" + TemplateAst { + root: [ + ConditionalChain { + chain: [ + Block { + prev_whitespace_content: None, + expression: IfConditional { + expression: VariableAccess( + [Ident]"foo" (6..9), + ), + }, + post_whitespace_content: None, + }, + ConditionalContent { + content: [], + }, + Block { + prev_whitespace_content: None, + expression: EndBlock, + post_whitespace_content: None, + }, + ], + }, + ], + } + "#); + } + + #[test] + fn check_if_else() { + let input = "{{ if foo }} foo {{ else }} bar {{ end }}"; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let ast = panic_pretty(input, parse(parsed.tokens())); + + insta::assert_debug_snapshot!(ast); + } + + #[test] + fn check_if_else_if() { + let input = "{{ if foo }} foo {{ else if bar }} bar {{ end }}"; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let ast = panic_pretty(input, parse(parsed.tokens())); + + insta::assert_debug_snapshot!(ast); + } + + #[test] + fn check_trim_whitespace() { + let input = "{{ if foo -}} foo {{- else if bar -}} bar {{- end }}"; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let ast = panic_pretty(input, parse(parsed.tokens())); + + insta::assert_debug_snapshot!(ast); + } + + #[test] + fn check_for_loop() { + let input = "{{ for value in array }} Hi: {{= value }} {{ else }} No Content :C {{ end }}"; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let ast = panic_pretty(input, parse(parsed.tokens())); + + insta::assert_debug_snapshot!(ast); + } + + #[test] + fn check_math_expression() { + let input = "{{= 5 * 3 + 2 / 3 }}"; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let ast = panic_pretty(input, parse(parsed.tokens())); + + insta::assert_debug_snapshot!(ast); + } + + #[test] + fn check_logical_expression() { + let input = "{{= true && false || 3 >= 2 && 5 == 2 }}"; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let ast = panic_pretty(input, parse(parsed.tokens())); + + insta::assert_debug_snapshot!(ast); + } +} diff --git a/src/parser/snapshots/nomo__parser__tests__check_for_loop.snap b/src/ast/snapshots/nomo__ast__tests__check_for_loop.snap similarity index 98% rename from src/parser/snapshots/nomo__parser__tests__check_for_loop.snap rename to src/ast/snapshots/nomo__ast__tests__check_for_loop.snap index 521c5fb..737e2c0 100644 --- a/src/parser/snapshots/nomo__parser__tests__check_for_loop.snap +++ b/src/ast/snapshots/nomo__ast__tests__check_for_loop.snap @@ -1,5 +1,5 @@ --- -source: src/parser/mod.rs +source: src/ast/mod.rs expression: ast --- TemplateAst { diff --git a/src/parser/snapshots/nomo__parser__tests__check_if_else.snap b/src/ast/snapshots/nomo__ast__tests__check_if_else.snap similarity index 98% rename from src/parser/snapshots/nomo__parser__tests__check_if_else.snap rename to src/ast/snapshots/nomo__ast__tests__check_if_else.snap index b989ca3..0a3b739 100644 --- a/src/parser/snapshots/nomo__parser__tests__check_if_else.snap +++ b/src/ast/snapshots/nomo__ast__tests__check_if_else.snap @@ -1,5 +1,5 @@ --- -source: src/parser/mod.rs +source: src/ast/mod.rs expression: ast --- TemplateAst { diff --git a/src/parser/snapshots/nomo__parser__tests__check_if_else_if.snap b/src/ast/snapshots/nomo__ast__tests__check_if_else_if.snap similarity index 98% rename from src/parser/snapshots/nomo__parser__tests__check_if_else_if.snap rename to src/ast/snapshots/nomo__ast__tests__check_if_else_if.snap index 0c2eef2..3912e60 100644 --- a/src/parser/snapshots/nomo__parser__tests__check_if_else_if.snap +++ b/src/ast/snapshots/nomo__ast__tests__check_if_else_if.snap @@ -1,5 +1,5 @@ --- -source: src/parser/mod.rs +source: src/ast/mod.rs expression: ast --- TemplateAst { diff --git a/src/parser/snapshots/nomo__parser__tests__check_invalid_action.snap b/src/ast/snapshots/nomo__ast__tests__check_invalid_action.snap similarity index 98% rename from src/parser/snapshots/nomo__parser__tests__check_invalid_action.snap rename to src/ast/snapshots/nomo__ast__tests__check_invalid_action.snap index 0587130..c8f8f50 100644 --- a/src/parser/snapshots/nomo__parser__tests__check_invalid_action.snap +++ b/src/ast/snapshots/nomo__ast__tests__check_invalid_action.snap @@ -1,5 +1,5 @@ --- -source: src/parser/mod.rs +source: src/ast/mod.rs expression: ast.to_report(input) --- error: Standlone action block diff --git a/src/parser/snapshots/nomo__parser__tests__check_logical_expression.snap b/src/ast/snapshots/nomo__ast__tests__check_logical_expression.snap similarity index 98% rename from src/parser/snapshots/nomo__parser__tests__check_logical_expression.snap rename to src/ast/snapshots/nomo__ast__tests__check_logical_expression.snap index aca365a..e9caca2 100644 --- a/src/parser/snapshots/nomo__parser__tests__check_logical_expression.snap +++ b/src/ast/snapshots/nomo__ast__tests__check_logical_expression.snap @@ -1,5 +1,5 @@ --- -source: src/parser/mod.rs +source: src/ast/mod.rs expression: ast --- TemplateAst { diff --git a/src/parser/snapshots/nomo__parser__tests__check_math_expression.snap b/src/ast/snapshots/nomo__ast__tests__check_math_expression.snap similarity index 98% rename from src/parser/snapshots/nomo__parser__tests__check_math_expression.snap rename to src/ast/snapshots/nomo__ast__tests__check_math_expression.snap index 7ba6415..7265e0a 100644 --- a/src/parser/snapshots/nomo__parser__tests__check_math_expression.snap +++ b/src/ast/snapshots/nomo__ast__tests__check_math_expression.snap @@ -1,5 +1,5 @@ --- -source: src/parser/mod.rs +source: src/ast/mod.rs expression: ast --- TemplateAst { diff --git a/src/parser/snapshots/nomo__parser__tests__check_trim_whitespace.snap b/src/ast/snapshots/nomo__ast__tests__check_trim_whitespace.snap similarity index 98% rename from src/parser/snapshots/nomo__parser__tests__check_trim_whitespace.snap rename to src/ast/snapshots/nomo__ast__tests__check_trim_whitespace.snap index 7308528..bce8aa2 100644 --- a/src/parser/snapshots/nomo__parser__tests__check_trim_whitespace.snap +++ b/src/ast/snapshots/nomo__ast__tests__check_trim_whitespace.snap @@ -1,5 +1,5 @@ --- -source: src/parser/mod.rs +source: src/ast/mod.rs expression: ast --- TemplateAst { diff --git a/src/parser/snapshots/nomo__parser__tests__simple_if_ast.snap b/src/ast/snapshots/nomo__ast__tests__simple_if_ast.snap similarity index 99% rename from src/parser/snapshots/nomo__parser__tests__simple_if_ast.snap rename to src/ast/snapshots/nomo__ast__tests__simple_if_ast.snap index ef4803b..3701cc2 100644 --- a/src/parser/snapshots/nomo__parser__tests__simple_if_ast.snap +++ b/src/ast/snapshots/nomo__ast__tests__simple_if_ast.snap @@ -1,5 +1,5 @@ --- -source: src/parser/mod.rs +source: src/ast/mod.rs expression: ast --- TemplateAst { diff --git a/src/parser/snapshots/nomo__parser__tests__simple_if_tokens.snap b/src/ast/snapshots/nomo__ast__tests__simple_if_tokens.snap similarity index 98% rename from src/parser/snapshots/nomo__parser__tests__simple_if_tokens.snap rename to src/ast/snapshots/nomo__ast__tests__simple_if_tokens.snap index e485e6b..ffc7d97 100644 --- a/src/parser/snapshots/nomo__parser__tests__simple_if_tokens.snap +++ b/src/ast/snapshots/nomo__ast__tests__simple_if_tokens.snap @@ -1,5 +1,5 @@ --- -source: src/parser/mod.rs +source: src/ast/mod.rs expression: parsed --- ParsedTemplate { diff --git a/src/emit/mod.rs b/src/emit/mod.rs index cf33cfa..aa3e6c0 100644 --- a/src/emit/mod.rs +++ b/src/emit/mod.rs @@ -1,9 +1,9 @@ use std::collections::BTreeMap; -use crate::parser::TemplateAstExpr; +use crate::ast::TemplateAstExpr; use crate::input::NomoInput; -use crate::lexer::TemplateToken; -use crate::lexer::TokenOperator; +use crate::parser::TemplateToken; +use crate::parser::TokenOperator; use crate::value::NomoValue; pub struct EmitMachine { @@ -102,11 +102,6 @@ pub enum Instruction { right_slot: VariableSlot, result_slot: VariableSlot, }, - FunctionCall { - name: NomoInput, - args: Vec, - slot: VariableSlot, - }, } #[derive(Debug, Clone)] @@ -115,7 +110,7 @@ pub struct VMInstructions { pub instructions: Vec, } -pub fn emit_machine(input: crate::parser::TemplateAst<'_>) -> VMInstructions { +pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> VMInstructions { let mut eval = vec![]; let mut machine = EmitMachine { @@ -171,7 +166,7 @@ fn emit_ast_expr( let end_label = machine.reserve_label(); let mut end_indices = vec![]; - let mut previous_post_whitespace_content: &Option = &None; + let mut previous_post_whitespace_content: &Option = &None; let mut previous_jump: Option = None; loop { @@ -422,7 +417,6 @@ fn emit_ast_expr( | TemplateAstExpr::ForElse | TemplateAstExpr::Invalid { .. } | TemplateAstExpr::Literal { .. } - | TemplateAstExpr::FunctionCall { .. } | TemplateAstExpr::Operation { .. } | TemplateAstExpr::VariableAccess { .. } => eval.push(Instruction::Abort), } @@ -461,20 +455,6 @@ fn emit_expr_load( result_slot: emit_slot, }); } - TemplateAstExpr::FunctionCall { name, args } => { - let mut arg_slots = vec![]; - for arg in args { - let slot = machine.reserve_slot(); - emit_expr_load(machine, eval, slot, arg); - arg_slots.push(slot); - } - - eval.push(Instruction::FunctionCall { - name: name.source(), - args: arg_slots, - slot: emit_slot, - }); - } TemplateAstExpr::Invalid { .. } => eval.push(Instruction::Abort), TemplateAstExpr::StaticContent { .. } | TemplateAstExpr::Interpolation { .. } => { unreachable!("Invalid AST here") @@ -500,9 +480,9 @@ mod tests { fn check_simple_variable_interpolation() { let input = "Hello {{= world }}"; - let parsed = crate::lexer::parse(input.into()).unwrap(); + let parsed = crate::parser::parse(input.into()).unwrap(); - let ast = crate::parser::parse(parsed.tokens()).unwrap(); + let ast = crate::ast::parse(parsed.tokens()).unwrap(); let emit = emit_machine(ast); @@ -536,22 +516,9 @@ mod tests { fn check_if_else_if() { let input = "{{ if foo }} foo {{ else if bar }} bar {{ else }} foobar {{ end }}"; - let parsed = crate::lexer::parse(input.into()).unwrap(); + let parsed = crate::parser::parse(input.into()).unwrap(); - let ast = crate::parser::parse(parsed.tokens()).unwrap(); - - let emit = emit_machine(ast); - - insta::assert_debug_snapshot!(emit); - } - - #[test] - fn check_function_call() { - let input = "{{ if foo(23) }} bar {{ else }} foobar {{ end }}"; - - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let ast = crate::parser::parse(parsed.tokens()).unwrap(); + let ast = crate::ast::parse(parsed.tokens()).unwrap(); let emit = emit_machine(ast); diff --git a/src/emit/snapshots/nomo__emit__tests__check_function_call.snap b/src/emit/snapshots/nomo__emit__tests__check_function_call.snap deleted file mode 100644 index 3594efe..0000000 --- a/src/emit/snapshots/nomo__emit__tests__check_function_call.snap +++ /dev/null @@ -1,79 +0,0 @@ ---- -source: src/emit/mod.rs -expression: emit ---- -VMInstructions { - labels: { - LabelSlot { - index: 0, - }: 13, - LabelSlot { - index: 3, - }: 8, - }, - instructions: [ - LoadLiteralToSlot { - source: [Literal(Integer(23))]"23" (10..12), - value: Integer { - value: 23, - }, - slot: VariableSlot { - index: 2, - }, - }, - FunctionCall { - name: "foo" (6..9), - args: [ - VariableSlot { - index: 2, - }, - ], - slot: VariableSlot { - index: 1, - }, - }, - JumpIfNotTrue { - emit_slot: VariableSlot { - index: 1, - }, - jump: LabelSlot { - index: 3, - }, - }, - AppendContent { - content: " " (16..17), - }, - AppendContent { - content: "bar" (17..20), - }, - AppendContent { - content: " " (20..21), - }, - Jump { - jump: LabelSlot { - index: 0, - }, - }, - AppendContent { - content: " " (16..17), - }, - AppendContent { - content: " " (31..32), - }, - AppendContent { - content: "foobar" (32..38), - }, - AppendContent { - content: " " (38..39), - }, - Jump { - jump: LabelSlot { - index: 0, - }, - }, - AppendContent { - content: " " (31..32), - }, - NoOp, - ], -} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index e0d864b..8d32123 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -7,7 +7,6 @@ use crate::Context; use crate::emit::Instruction; use crate::emit::VMInstructions; use crate::emit::VariableSlot; -use crate::functions::FunctionMap; use crate::input::NomoInput; use crate::value::NomoValue; @@ -70,11 +69,7 @@ impl Scope { clippy::unnecessary_to_owned, reason = "We cannot do the suggested way as the lifetimes would not match up" )] -pub fn execute( - available_functions: &FunctionMap, - vm: &VMInstructions, - global_context: &Context, -) -> Result { +pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result { let mut output = String::new(); let mut scopes = Scope { @@ -202,28 +197,17 @@ pub fn execute( let right_value = scopes.get(right_slot); let result = match op { - crate::lexer::TokenOperator::Plus => left_value.try_add(right_value), - crate::lexer::TokenOperator::Minus => left_value.try_sub(right_value), - crate::lexer::TokenOperator::Times => left_value.try_mul(right_value), - crate::lexer::TokenOperator::Divide => left_value.try_div(right_value), - crate::lexer::TokenOperator::And => left_value.try_and(right_value), - crate::lexer::TokenOperator::Or => left_value.try_or(right_value), + crate::parser::TokenOperator::Plus => left_value.try_add(right_value), + crate::parser::TokenOperator::Minus => left_value.try_sub(right_value), + crate::parser::TokenOperator::Times => left_value.try_mul(right_value), + crate::parser::TokenOperator::Divide => left_value.try_div(right_value), + crate::parser::TokenOperator::And => left_value.try_and(right_value), + crate::parser::TokenOperator::Or => left_value.try_or(right_value), _ => todo!(), }; scopes.insert_into_slot(*result_slot, result.unwrap()); } - Instruction::FunctionCall { name, args, slot } => { - let args = args.iter().map(|slot| scopes.get(slot)).cloned().collect(); - - let value = available_functions - .get(name.as_str()) - .unwrap() - .call(args) - .unwrap(); - - scopes.insert_into_slot(*slot, value); - } } ip += 1; @@ -236,22 +220,20 @@ pub fn execute( mod tests { use crate::Context; use crate::eval::execute; - use crate::functions::FunctionMap; - use crate::functions::NomoFunctionError; #[test] fn check_simple_variable_interpolation() { let input = "Hello {{= world }}"; - let parsed = crate::lexer::parse(input.into()).unwrap(); + let parsed = crate::parser::parse(input.into()).unwrap(); - let ast = crate::parser::parse(parsed.tokens()).unwrap(); + let ast = crate::ast::parse(parsed.tokens()).unwrap(); let emit = crate::emit::emit_machine(ast); let mut context = Context::new(); context.insert("world", "World"); - let output = execute(&FunctionMap::default(), &emit, &context); + let output = execute(&emit, &context); insta::assert_debug_snapshot!(output, @r#" Ok( @@ -259,31 +241,4 @@ mod tests { ) "#); } - - #[test] - fn check_method_call() { - let input = "Hello {{= foo(world) }}"; - - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let ast = crate::parser::parse(parsed.tokens()).unwrap(); - - let emit = crate::emit::emit_machine(ast); - - let mut context = Context::new(); - context.insert("world", "World"); - let mut function_map = FunctionMap::default(); - - function_map.register("foo", |arg: String| -> Result { - Ok(arg.to_uppercase()) - }); - - let output = execute(&function_map, &emit, &context); - - insta::assert_debug_snapshot!(output, @r#" - Ok( - "Hello WORLD", - ) - "#); - } } diff --git a/src/functions.rs b/src/functions.rs deleted file mode 100644 index c15031c..0000000 --- a/src/functions.rs +++ /dev/null @@ -1,175 +0,0 @@ -use std::any::Any; -use std::collections::HashMap; - -use displaydoc::Display; -use thiserror::Error; - -use crate::NomoValueError; -use crate::value::NomoValue; - -#[derive(Debug, Error, Display)] -pub enum NomoFunctionError { - /// Received {received} arguments, but this function only takes {expected} - WrongArgumentCount { received: usize, expected: usize }, - - /// The argument at this position is of the wrong type - InvalidArgumentType { index: usize }, - - /// A user-provided error - #[error(transparent)] - CustomError { - custom: Box, - }, -} - -pub trait NomoFunction: 'static + Send + Sync { - fn call(&self, args: Vec) -> Result; -} - -#[derive(Default)] -pub struct FunctionMap { - funcs: HashMap, -} - -impl FunctionMap { - pub fn register, T>(&mut self, name: impl Into, func: NF) { - self.funcs - .insert(name.into(), ErasedNomoFunction::erase(func)); - } - - pub fn get(&self, name: impl AsRef) -> Option<&ErasedNomoFunction> { - self.funcs.get(name.as_ref()) - } -} - -pub struct ErasedNomoFunction { - func: Box, - call_fn: fn(&dyn Any, Vec) -> Result, -} - -impl ErasedNomoFunction { - pub fn call(&self, args: Vec) -> Result { - (self.call_fn)(&*self.func, args) - } - - pub fn erase(f: NF) -> ErasedNomoFunction - where - NF: NomoFunction, - { - ErasedNomoFunction { - func: Box::new(f), - call_fn: |nf, args| { - let nf: &NF = nf.downcast_ref().unwrap(); - - nf.call(args) - }, - } - } -} - -#[rustfmt::skip] -macro_rules! for_all_tuples { - ($name:ident) => { - $name!([], T1); - $name!([T1], T2); - $name!([T1, T2], T3); - $name!([T1, T2, T3], T4); - $name!([T1, T2, T3, T4], T5); - $name!([T1, T2, T3, T4, T5], T6); - $name!([T1, T2, T3, T4, T5, T6], T7); - $name!([T1, T2, T3, T4, T5, T6, T7], T8); - $name!([T1, T2, T3, T4, T5, T6, T7, T8], T9); - $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9], T10); - $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10], T11); - $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11], T12); - $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12], T13); - $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13], T14); - $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14], T15); - $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15], T16); - }; -} - -macro_rules! impl_nomo_function { - ([$($ty:ident),*], $last:ident) => { - impl NomoFunction<($($ty,)* $last,)> for F - where - F: Fn($($ty,)* $last,) -> Result, - F: Send + Sync + 'static, - Res: Into, - Error: Into, - $( $ty: TryFrom, )* - $last: TryFrom, - { - fn call(&self, args: Vec) -> Result { - let arg_count = args.len(); - - let total_count = 1 $(+ impl_nomo_function!(count $ty))*; - - if arg_count != total_count { - return Err(NomoFunctionError::WrongArgumentCount { - expected: total_count, - received: arg_count, - }); - } - - let mut args = args.into_iter(); - - #[allow(unused_mut)] - let mut idx = 0; - - let val = (self)( - $({ - let val = $ty::try_from(args.next().unwrap()).map_err(|_| NomoFunctionError::InvalidArgumentType { index: idx })?; - idx += 1; - val - },)* - $last::try_from(args.next().unwrap()).map_err(|_| NomoFunctionError::InvalidArgumentType { index: idx })?, - ); - - val.map(Into::into).map_err(Into::into) - } - } - }; - - (count $ty:ident) => { - 1 - } -} - -for_all_tuples!(impl_nomo_function); - -impl NomoFunction<()> for F -where - F: Fn() -> Result + Send + Sync + 'static, - Res: Into, - Error: Into, -{ - fn call(&self, args: Vec) -> Result { - let arg_count = args.len(); - - if arg_count != 0 { - return Err(NomoFunctionError::WrongArgumentCount { - received: arg_count, - expected: 0, - }); - } - - let val = (self)(); - - val.map(Into::into).map_err(Into::into) - } -} - -#[cfg(test)] -mod tests { - use crate::functions::ErasedNomoFunction; - use crate::functions::NomoFunctionError; - - #[expect(dead_code, reason = "This is a compile test")] - fn check_various_methods() { - ErasedNomoFunction::erase(|name: String| -> Result { Ok(name) }); - ErasedNomoFunction::erase(|left: u64, right: u64| -> Result { - Ok(left + right) - }); - } -} diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs deleted file mode 100644 index a35e096..0000000 --- a/src/lexer/mod.rs +++ /dev/null @@ -1,908 +0,0 @@ -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; -use winnow::ascii::alpha1; -use winnow::ascii::digit1; -use winnow::ascii::multispace0; -use winnow::ascii::multispace1; -use winnow::combinator::alt; -use winnow::combinator::cut_err; -use winnow::combinator::dispatch; -use winnow::combinator::empty; -use winnow::combinator::eof; -use winnow::combinator::fail; -use winnow::combinator::not; -use winnow::combinator::opt; -use winnow::combinator::peek; -use winnow::combinator::preceded; -use winnow::combinator::repeat_till; -use winnow::combinator::terminated; -use winnow::combinator::trace; -use winnow::error::AddContext; -use winnow::error::FromRecoverableError; -use winnow::error::ModalError; -use winnow::error::ParserError; -use winnow::stream::Location; -use winnow::stream::Recoverable; -use winnow::stream::Stream; -use winnow::token::any; -use winnow::token::one_of; -use winnow::token::rest; -use winnow::token::take_until; -use winnow::token::take_while; - -use crate::SourceSpan; -use crate::input::NomoInput; -use crate::resume_after_cut; - -type Input<'input> = Recoverable, ParseError>; -type PResult<'input, T> = Result; - -#[derive(Debug, Error)] -pub struct ParseFailure { - input: Arc, - errors: Vec, -} - -impl std::fmt::Display for ParseFailure { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.to_report()) - } -} - -impl ParseFailure { - fn from_errors(errors: Vec, input: NomoInput) -> ParseFailure { - ParseFailure { - input: Arc::from(input.to_string()), - errors, - } - } - - pub fn to_report(&self) -> String { - let reports = self - .errors - .iter() - .map(|error| { - Level::ERROR - .primary_title( - error - .message - .as_deref() - .unwrap_or("An error occurred while parsing"), - ) - .element( - Snippet::source(self.input.as_ref()).annotation( - AnnotationKind::Primary - .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::>(); - - let renderer = - Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Unicode); - renderer.render(&reports) - } -} - -#[derive(Debug, Clone)] -pub struct ParseError { - pub(crate) message: Option, - pub(crate) help: Option, - pub(crate) span: Option, - - is_fatal: bool, -} - -impl ParseError { - fn ctx() -> Self { - ParseError { - message: None, - help: None, - span: None, - - is_fatal: false, - } - } - - fn msg(mut self, message: &str) -> Self { - self.message = Some(message.to_string()); - self - } - - fn help(mut self, help: &str) -> Self { - self.help = Some(help.to_string()); - self - } -} - -impl ModalError for ParseError { - fn cut(mut self) -> Self { - self.is_fatal = true; - self - } - - fn backtrack(mut self) -> Self { - self.is_fatal = false; - self - } -} - -impl<'input> FromRecoverableError, ParseError> for ParseError { - fn from_recoverable_error( - token_start: & as winnow::stream::Stream>::Checkpoint, - _err_start: & as winnow::stream::Stream>::Checkpoint, - input: &Input<'input>, - mut e: ParseError, - ) -> Self { - e.span = e - .span - .or_else(|| Some(span_from_checkpoint(input, token_start))); - - e - } -} - -impl<'input> AddContext, ParseError> for ParseError { - fn add_context( - mut self, - _input: &Input<'input>, - _token_start: & as Stream>::Checkpoint, - context: ParseError, - ) -> Self { - self.message = context.message.or(self.message); - self.help = context.help.or(self.help); - self - } -} - -fn span_from_checkpoint( - input: &I, - token_start: &::Checkpoint, -) -> SourceSpan { - let offset = input.offset_from(token_start); - - SourceSpan { - range: (input.current_token_start() - offset)..input.current_token_start(), - } -} - -impl<'input> ParserError> for ParseError { - type Inner = ParseError; - - fn from_input(_input: &Input<'input>) -> Self { - ParseError::ctx() - } - - fn into_inner(self) -> winnow::Result { - Ok(self) - } - - fn is_backtrack(&self) -> bool { - !self.is_fatal - } -} - -#[derive(Debug)] -pub struct ParsedTemplate { - tokens: Vec, -} - -impl ParsedTemplate { - pub fn tokens(&self) -> &[TemplateToken] { - &self.tokens - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum TokenKind { - Content, - LeftDelim, - RightDelim, - TrimWhitespace, - WantsOutput, - Ident, - LeftArgList, - RightArgList, - ArgSeperator, - Whitespace, - Invalid, - ConditionalIf, - ConditionalElse, - For, - In, - End, - Literal(TokenLiteral), - Operator(TokenOperator), -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum TokenOperator { - Plus, - Minus, - Times, - Divide, - And, - Or, - Equal, - NotEqual, - Greater, - GreaterOrEqual, - Lesser, - LesserOrEqual, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum TokenLiteral { - Bool(bool), - Integer(u64), -} - -impl PartialEq for TemplateToken { - fn eq(&self, other: &TokenKind) -> bool { - self.kind == *other - } -} - -impl winnow::stream::ContainsToken<&'_ TemplateToken> for TokenKind { - fn contains_token(&self, token: &'_ TemplateToken) -> bool { - *self == token.kind - } -} - -impl winnow::stream::ContainsToken<&'_ TemplateToken> for &'_ [TokenKind] { - fn contains_token(&self, token: &'_ TemplateToken) -> bool { - self.contains(&token.kind) - } -} - -impl winnow::stream::ContainsToken<&'_ TemplateToken> for &'_ [TokenKind; LEN] { - fn contains_token(&self, token: &'_ TemplateToken) -> bool { - self.contains(&token.kind) - } -} - -impl winnow::stream::ContainsToken<&'_ TemplateToken> for [TokenKind; LEN] { - fn contains_token(&self, token: &'_ TemplateToken) -> bool { - self.contains(&token.kind) - } -} - -#[derive(Clone, PartialEq, Eq)] -pub struct TemplateToken { - kind: TokenKind, - source: NomoInput, -} - -impl std::fmt::Debug for TemplateToken { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "[{:?}]{:?}", self.kind(), self.source()) - } -} - -impl Location for TemplateToken { - fn previous_token_end(&self) -> usize { - NomoInput::get_range(&self.source).start - } - - fn current_token_start(&self) -> usize { - NomoInput::get_range(&self.source).start - } -} - -macro_rules! impl_token_kind_builders { - ($($name:ident => $kind:expr),+ $(,)?) => { - $( - fn $name(source: NomoInput) -> Self { - TemplateToken { - kind: $kind, - source, - } - } - )+ - }; -} - -impl TemplateToken { - impl_token_kind_builders! { - content => TokenKind::Content, - left_delim => TokenKind::LeftDelim, - right_delim => TokenKind::RightDelim, - trim_whitespace => TokenKind::TrimWhitespace, - wants_output => TokenKind::WantsOutput, - ident => TokenKind::Ident, - left_arg_list => TokenKind::LeftArgList, - right_arg_list => TokenKind::RightArgList, - arg_seperator => TokenKind::ArgSeperator, - whitespace => TokenKind::Whitespace, - invalid => TokenKind::Invalid, - conditional_if => TokenKind::ConditionalIf, - conditional_else => TokenKind::ConditionalElse, - keyword_for => TokenKind::For, - keyword_in => TokenKind::In, - end => TokenKind::End, - } - - pub fn literal(literal: TokenLiteral, source: NomoInput) -> Self { - TemplateToken { - kind: TokenKind::Literal(literal), - source, - } - } - - pub fn operator(operator: TokenOperator, source: NomoInput) -> Self { - TemplateToken { - kind: TokenKind::Operator(operator), - source, - } - } - - pub fn kind(&self) -> TokenKind { - self.kind - } - - pub fn source(&self) -> NomoInput { - self.source.clone() - } -} - -pub fn parse(input: NomoInput) -> Result { - let (_remaining, val, errors) = - parse_tokens.recoverable_parse(LocatingSlice::new(input.clone())); - - if errors.is_empty() - && let Some(val) = val - { - Ok(ParsedTemplate { tokens: val }) - } else { - Err(ParseFailure::from_errors(errors, input)) - } -} - -fn parse_tokens<'input>(input: &mut Input<'input>) -> PResult<'input, Vec> { - repeat_till(0.., alt((parse_interpolate, parse_content)), eof) - .map(|(v, _): (Vec<_>, _)| v.into_iter().flatten().collect()) - .parse_next(input) -} - -fn parse_content<'input>(input: &mut Input<'input>) -> PResult<'input, Vec> { - alt(( - repeat_till(1.., any, peek((multispace0, "{{"))).map(|((), _)| ()), - rest.void(), - )) - .take() - .map(TemplateToken::content) - .map(|v| vec![v]) - .parse_next(input) -} - -fn parse_interpolate<'input>(input: &mut Input<'input>) -> PResult<'input, Vec> { - let prev_whitespace = opt(parse_whitespace).parse_next(input)?; - let left_delim = "{{".map(TemplateToken::left_delim).parse_next(input)?; - let left_trim = opt("-".map(TemplateToken::trim_whitespace)).parse_next(input)?; - let wants_output = opt("=".map(TemplateToken::wants_output)).parse_next(input)?; - - let get_tokens = repeat_till(1.., parse_block_token, peek(preceded(opt("-"), "}}"))); - let recover = take_until(0.., "}}").void(); - - let (inside_tokens, _): (Vec<_>, _) = get_tokens - .resume_after(recover) - .with_taken() - .map(|(val, taken)| { - val.unwrap_or_else(|| (vec![TemplateToken::invalid(taken)], NomoInput::from(""))) - }) - .parse_next(input)?; - - let right_trim = opt("-".map(TemplateToken::trim_whitespace)).parse_next(input)?; - let right_delim = "}}".map(TemplateToken::right_delim).parse_next(input)?; - let post_whitespace = opt(parse_whitespace).parse_next(input)?; - - let mut tokens = vec![]; - tokens.extend(prev_whitespace); - tokens.push(left_delim); - tokens.extend(left_trim); - tokens.extend(wants_output); - tokens.extend(inside_tokens); - tokens.extend(right_trim); - tokens.push(right_delim); - tokens.extend(post_whitespace); - - Ok(tokens) -} - -fn parse_block_token<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { - trace( - "parse_block_token", - alt(( - parse_ident, - parse_function, - parse_keyword, - parse_whitespace, - parse_operator, - )), - ) - .parse_next(input) -} - -fn parse_literal<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { - trace( - "parse_literal", - alt((parse_boolean, parse_number)) - .with_taken() - .map(|(lit, span)| TemplateToken::literal(lit, span)), - ) - .parse_next(input) -} - -fn parse_number<'input>(input: &mut Input<'input>) -> PResult<'input, TokenLiteral> { - digit1 - .verify_map(|digits: NomoInput| digits.parse::().ok()) - .map(TokenLiteral::Integer) - .parse_next(input) -} - -fn parse_boolean<'input>(input: &mut Input<'input>) -> PResult<'input, TokenLiteral> { - alt(( - "true".value(TokenLiteral::Bool(true)), - "false".value(TokenLiteral::Bool(false)), - )) - .parse_next(input) -} - -fn parse_condition_if<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { - trace( - "parse_condition_if", - "if".map(TemplateToken::conditional_if), - ) - .parse_next(input) -} - -fn parse_condition_else<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { - trace( - "parse_condition_else", - "else".map(TemplateToken::conditional_else), - ) - .parse_next(input) -} - -fn parse_end<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { - trace("parse_end", "end".map(TemplateToken::end)).parse_next(input) -} - -fn parse_for<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { - trace("parse_for", "for".map(TemplateToken::keyword_for)).parse_next(input) -} - -fn parse_in<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { - trace("parse_in", "in".map(TemplateToken::keyword_in)).parse_next(input) -} - -fn parse_keyword<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { - alt(( - terminated(parse_literal, ident_terminator_check), - terminated(parse_condition_if, ident_terminator_check), - terminated(parse_condition_else, ident_terminator_check), - terminated(parse_for, ident_terminator_check), - terminated(parse_in, ident_terminator_check), - terminated(parse_end, ident_terminator_check), - )) - .parse_next(input) -} - -fn parse_whitespace<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { - trace( - "parse_whitespace", - multispace1.map(TemplateToken::whitespace), - ) - .parse_next(input) -} - -fn parse_function<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { - trace( - "parse_function", - alt(( - "(".map(TemplateToken::left_arg_list), - ")".map(TemplateToken::right_arg_list), - ",".map(TemplateToken::arg_seperator), - )), - ) - .parse_next(input) -} - -fn parse_ident<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { - resume_after_cut( - terminated( - ident.map(TemplateToken::ident), - cut_err(ident_terminator_check), - ) - .context( - ParseError::ctx() - .msg("Invalid variable identifier") - .help("valid variable identifiers are alphanumeric"), - ), - bad_ident, - ) - .with_taken() - .map(|(val, taken)| val.unwrap_or(TemplateToken::invalid(taken))) - .parse_next(input) -} - -fn parse_operator<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { - let (operator, source) = trace( - "operator", - dispatch! {any; - '+' => empty.value(TokenOperator::Plus), - '-' => empty.value(TokenOperator::Minus), - '*' => empty.value(TokenOperator::Times), - '/' => empty.value(TokenOperator::Divide), - '&' => alt(( - "&".value(TokenOperator::And), - cut_err(fail), - )), - '|' => alt(( - "|".value(TokenOperator::Or), - cut_err(fail), - )), - '<' => alt(( - "=".value(TokenOperator::LesserOrEqual), - empty.value(TokenOperator::Lesser), - )), - '>' => alt(( - "=".value(TokenOperator::GreaterOrEqual), - empty.value(TokenOperator::Greater), - )), - '!' => alt(( - "=".value(TokenOperator::NotEqual), - cut_err(fail), - )), - '=' => alt(( - "=".value(TokenOperator::Equal), - cut_err(fail), - )), - _ => fail, - }, - ) - .with_taken() - .parse_next(input)?; - - Ok(TemplateToken::operator(operator, source)) -} - -fn ident<'input>(input: &mut Input<'input>) -> PResult<'input, NomoInput> { - peek(not(parse_keyword)) - .context(ParseError::ctx().msg("Expected an ident, but found a literal instead")) - .parse_next(input)?; - - let literal_start = alt((alpha1, "_")); - - ( - literal_start, - take_while(0.., |c: char| c.is_alphanumeric() || "_".contains(c)), - ) - .take() - .parse_next(input) -} - -fn bad_ident<'input>(input: &mut Input<'input>) -> PResult<'input, ()> { - repeat_till(1.., any, ident_terminator_check) - .map(|((), _)| ()) - .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(('(', ',', ')')).void(), - one_of((' ', '\t', '\r', '\n')).void(), - )) - .parse_next(input) -} - -#[cfg(test)] -mod tests { - use crate::lexer::parse; - - #[test] - fn parse_simple() { - let input = "Hello There"; - let output = parse(input.into()); - - insta::assert_debug_snapshot!(output, @r#" - Ok( - ParsedTemplate { - tokens: [ - [Content]"Hello There" (0..11), - ], - }, - ) - "#); - } - - #[test] - fn parse_interpolate() { - let input = "Hello {{ there }}"; - let output = parse(input.into()); - - insta::assert_debug_snapshot!(output, @r#" - Ok( - ParsedTemplate { - tokens: [ - [Content]"Hello" (0..5), - [Whitespace]" " (5..6), - [LeftDelim]"{{" (6..8), - [Whitespace]" " (8..9), - [Ident]"there" (9..14), - [Whitespace]" " (14..15), - [RightDelim]"}}" (15..17), - ], - }, - ) - "#); - } - - #[test] - fn parse_interpolate_bad() { - let input = "Hello {{ the2re }} {{ the@re }}"; - let output = parse(input.into()); - - insta::assert_debug_snapshot!(output, @r#" - Err( - ParseFailure { - input: "Hello {{ the2re }} {{ the@re }}", - errors: [ - ParseError { - message: Some( - "Invalid variable identifier", - ), - help: Some( - "valid variable identifiers are alphanumeric", - ), - span: Some( - SourceSpan { - range: 22..28, - }, - ), - is_fatal: true, - }, - ], - }, - ) - "#); - - let error = output.unwrap_err(); - - insta::assert_snapshot!(error.to_report()); - } - - #[test] - fn parse_simple_condition() { - let input = "{{ if true }} Hello! {{ else }} Bye {{ end }}"; - let output = parse(input.into()); - - insta::assert_debug_snapshot!(output, @r#" - Ok( - ParsedTemplate { - tokens: [ - [LeftDelim]"{{" (0..2), - [Whitespace]" " (2..3), - [ConditionalIf]"if" (3..5), - [Whitespace]" " (5..6), - [Literal(Bool(true))]"true" (6..10), - [Whitespace]" " (10..11), - [RightDelim]"}}" (11..13), - [Whitespace]" " (13..14), - [Content]"Hello!" (14..20), - [Whitespace]" " (20..21), - [LeftDelim]"{{" (21..23), - [Whitespace]" " (23..24), - [ConditionalElse]"else" (24..28), - [Whitespace]" " (28..29), - [RightDelim]"}}" (29..31), - [Whitespace]" " (31..32), - [Content]"Bye" (32..35), - [Whitespace]" " (35..36), - [LeftDelim]"{{" (36..38), - [Whitespace]" " (38..39), - [End]"end" (39..42), - [Whitespace]" " (42..43), - [RightDelim]"}}" (43..45), - ], - }, - ) - "#); - } - - #[test] - fn parse_trim_whitespace() { - let input = "\n\n{{-= hello -}} \n\n"; - let output = parse(input.into()); - - insta::assert_debug_snapshot!(output, @r#" - Ok( - ParsedTemplate { - tokens: [ - [Whitespace]"\n\n" (0..2), - [LeftDelim]"{{" (2..4), - [TrimWhitespace]"-" (4..5), - [WantsOutput]"=" (5..6), - [Whitespace]" " (6..7), - [Ident]"hello" (7..12), - [Whitespace]" " (12..13), - [TrimWhitespace]"-" (13..14), - [RightDelim]"}}" (14..16), - [Whitespace]" \n\n" (16..19), - ], - }, - ) - "#); - } - - #[test] - fn parse_for_loop() { - let input = "{{ for value in array }} Hi: {{= value }} {{ end }}"; - let output = parse(input.into()); - - insta::assert_debug_snapshot!(output, @r#" - Ok( - ParsedTemplate { - tokens: [ - [LeftDelim]"{{" (0..2), - [Whitespace]" " (2..3), - [For]"for" (3..6), - [Whitespace]" " (6..7), - [Ident]"value" (7..12), - [Whitespace]" " (12..13), - [In]"in" (13..15), - [Whitespace]" " (15..16), - [Ident]"array" (16..21), - [Whitespace]" " (21..22), - [RightDelim]"}}" (22..24), - [Whitespace]" " (24..25), - [Content]"Hi:" (25..28), - [Whitespace]" " (28..29), - [LeftDelim]"{{" (29..31), - [WantsOutput]"=" (31..32), - [Whitespace]" " (32..33), - [Ident]"value" (33..38), - [Whitespace]" " (38..39), - [RightDelim]"}}" (39..41), - [Whitespace]" " (41..42), - [LeftDelim]"{{" (42..44), - [Whitespace]" " (44..45), - [End]"end" (45..48), - [Whitespace]" " (48..49), - [RightDelim]"}}" (49..51), - ], - }, - ) - "#); - } - - #[test] - fn parse_operations() { - let input = "{{= 5 * 14 + 3 / 2 - 1 }}{{ if foo && bar || baz && 2 != 3 || 4 > 2 || 43 <= 5 }}{{ end }}"; - let output = parse(input.into()); - - insta::assert_debug_snapshot!(output, @r#" - Ok( - ParsedTemplate { - tokens: [ - [LeftDelim]"{{" (0..2), - [WantsOutput]"=" (2..3), - [Whitespace]" " (3..4), - [Literal(Integer(5))]"5" (4..5), - [Whitespace]" " (5..6), - [Operator(Times)]"*" (6..7), - [Whitespace]" " (7..8), - [Literal(Integer(14))]"14" (8..10), - [Whitespace]" " (10..11), - [Operator(Plus)]"+" (11..12), - [Whitespace]" " (12..13), - [Literal(Integer(3))]"3" (13..14), - [Whitespace]" " (14..15), - [Operator(Divide)]"/" (15..16), - [Whitespace]" " (16..17), - [Literal(Integer(2))]"2" (17..18), - [Whitespace]" " (18..19), - [Operator(Minus)]"-" (19..20), - [Whitespace]" " (20..21), - [Literal(Integer(1))]"1" (21..22), - [Whitespace]" " (22..23), - [RightDelim]"}}" (23..25), - [LeftDelim]"{{" (25..27), - [Whitespace]" " (27..28), - [ConditionalIf]"if" (28..30), - [Whitespace]" " (30..31), - [Ident]"foo" (31..34), - [Whitespace]" " (34..35), - [Operator(And)]"&&" (35..37), - [Whitespace]" " (37..38), - [Ident]"bar" (38..41), - [Whitespace]" " (41..42), - [Operator(Or)]"||" (42..44), - [Whitespace]" " (44..45), - [Ident]"baz" (45..48), - [Whitespace]" " (48..49), - [Operator(And)]"&&" (49..51), - [Whitespace]" " (51..52), - [Literal(Integer(2))]"2" (52..53), - [Whitespace]" " (53..54), - [Operator(NotEqual)]"!=" (54..56), - [Whitespace]" " (56..57), - [Literal(Integer(3))]"3" (57..58), - [Whitespace]" " (58..59), - [Operator(Or)]"||" (59..61), - [Whitespace]" " (61..62), - [Literal(Integer(4))]"4" (62..63), - [Whitespace]" " (63..64), - [Operator(Greater)]">" (64..65), - [Whitespace]" " (65..66), - [Literal(Integer(2))]"2" (66..67), - [Whitespace]" " (67..68), - [Operator(Or)]"||" (68..70), - [Whitespace]" " (70..71), - [Literal(Integer(43))]"43" (71..73), - [Whitespace]" " (73..74), - [Operator(LesserOrEqual)]"<=" (74..76), - [Whitespace]" " (76..77), - [Literal(Integer(5))]"5" (77..78), - [Whitespace]" " (78..79), - [RightDelim]"}}" (79..81), - [LeftDelim]"{{" (81..83), - [Whitespace]" " (83..84), - [End]"end" (84..87), - [Whitespace]" " (87..88), - [RightDelim]"}}" (88..90), - ], - }, - ) - "#); - } - - #[test] - fn parse_function() { - let input = "{{= foo(23, 4, 3 * 5) }}"; - let output = parse(input.into()); - - insta::assert_debug_snapshot!(output, @r#" - Ok( - ParsedTemplate { - tokens: [ - [LeftDelim]"{{" (0..2), - [WantsOutput]"=" (2..3), - [Whitespace]" " (3..4), - [Ident]"foo" (4..7), - [LeftArgList]"(" (7..8), - [Literal(Integer(23))]"23" (8..10), - [ArgSeperator]"," (10..11), - [Whitespace]" " (11..12), - [Literal(Integer(4))]"4" (12..13), - [ArgSeperator]"," (13..14), - [Whitespace]" " (14..15), - [Literal(Integer(3))]"3" (15..16), - [Whitespace]" " (16..17), - [Operator(Times)]"*" (17..18), - [Whitespace]" " (18..19), - [Literal(Integer(5))]"5" (19..20), - [RightArgList]")" (20..21), - [Whitespace]" " (21..22), - [RightDelim]"}}" (22..24), - ], - }, - ) - "#); - } -} diff --git a/src/lib.rs b/src/lib.rs index fbe8ecf..8d177ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,17 +4,15 @@ use displaydoc::Display; use thiserror::Error; use crate::emit::VMInstructions; -use crate::functions::FunctionMap; use crate::input::NomoInput; use crate::value::NomoValue; use crate::value::NomoValueError; -pub mod parser; +pub mod ast; pub mod emit; pub mod eval; -pub mod functions; pub mod input; -pub mod lexer; +pub mod parser; pub mod value; #[derive(Debug, Error, Display)] @@ -22,12 +20,12 @@ pub enum NomoError { /// Could not parse the given template ParseError { #[from] - source: lexer::ParseFailure, + source: parser::ParseFailure, }, /// Invalid Template AstError { #[from] - source: parser::AstFailure, + source: ast::AstFailure, }, /// An error occurred while evaluating @@ -42,7 +40,6 @@ pub enum NomoError { pub struct Nomo { templates: HashMap, - function_map: FunctionMap, } impl Default for Nomo { @@ -55,7 +52,6 @@ impl Nomo { pub fn new() -> Nomo { Nomo { templates: HashMap::new(), - function_map: FunctionMap::default(), } } @@ -65,8 +61,8 @@ impl Nomo { value: impl Into, ) -> Result<(), NomoError> { let source = value.into(); - let parse = lexer::parse(source.clone())?; - let ast = parser::parse(parse.tokens())?; + let parse = parser::parse(source.clone())?; + let ast = ast::parse(parse.tokens())?; let instructions = emit::emit_machine(ast); @@ -82,7 +78,7 @@ impl Nomo { .get(name) .ok_or_else(|| NomoError::UnknownTemplate(name.to_string()))?; - let res = eval::execute(&self.function_map, &template.instructions, ctx)?; + let res = eval::execute(&template.instructions, ctx)?; Ok(res) } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ed4456f..37e2757 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1,50 +1,100 @@ +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; -use winnow::combinator::Infix::Left; +use winnow::ascii::alpha1; +use winnow::ascii::digit1; +use winnow::ascii::multispace0; +use winnow::ascii::multispace1; use winnow::combinator::alt; use winnow::combinator::cut_err; -use winnow::combinator::delimited; use winnow::combinator::dispatch; -use winnow::combinator::expression; +use winnow::combinator::empty; +use winnow::combinator::eof; +use winnow::combinator::fail; use winnow::combinator::not; use winnow::combinator::opt; use winnow::combinator::peek; use winnow::combinator::preceded; -use winnow::combinator::repeat; use winnow::combinator::repeat_till; -use winnow::combinator::separated; +use winnow::combinator::terminated; use winnow::combinator::trace; use winnow::error::AddContext; use winnow::error::FromRecoverableError; use winnow::error::ModalError; use winnow::error::ParserError; -use winnow::stream::Offset; +use winnow::stream::Location; use winnow::stream::Recoverable; use winnow::stream::Stream; -use winnow::stream::TokenSlice; use winnow::token::any; +use winnow::token::one_of; +use winnow::token::rest; +use winnow::token::take_until; +use winnow::token::take_while; use crate::SourceSpan; -use crate::lexer::TemplateToken; -use crate::lexer::TokenKind; -use crate::lexer::TokenOperator; +use crate::input::NomoInput; use crate::resume_after_cut; -use crate::value::NomoValue; -#[derive(Debug, Clone)] -pub struct TemplateAst<'input> { - root: Vec>, +type Input<'input> = Recoverable, ParseError>; +type PResult<'input, T> = Result; + +#[derive(Debug, Error)] +pub struct ParseFailure { + input: Arc, + errors: Vec, } -impl TemplateAst<'_> { - pub fn root(&self) -> &[TemplateAstExpr<'_>] { - &self.root +impl std::fmt::Display for ParseFailure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.to_report()) + } +} + +impl ParseFailure { + fn from_errors(errors: Vec, input: NomoInput) -> ParseFailure { + ParseFailure { + input: Arc::from(input.to_string()), + errors, + } + } + + pub fn to_report(&self) -> String { + let reports = self + .errors + .iter() + .map(|error| { + Level::ERROR + .primary_title( + error + .message + .as_deref() + .unwrap_or("An error occurred while parsing"), + ) + .element( + Snippet::source(self.input.as_ref()).annotation( + AnnotationKind::Primary + .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::>(); + + let renderer = + Renderer::styled().decor_style(annotate_snippets::renderer::DecorStyle::Unicode); + renderer.render(&reports) } } #[derive(Debug, Clone)] -pub struct AstError { +pub struct ParseError { pub(crate) message: Option, pub(crate) help: Option, pub(crate) span: Option, @@ -52,9 +102,9 @@ pub struct AstError { is_fatal: bool, } -impl AstError { +impl ParseError { fn ctx() -> Self { - AstError { + ParseError { message: None, help: None, span: None, @@ -74,7 +124,7 @@ impl AstError { } } -impl ModalError for AstError { +impl ModalError for ParseError { fn cut(mut self) -> Self { self.is_fatal = true; self @@ -86,46 +136,27 @@ impl ModalError for AstError { } } -impl FromRecoverableError, AstError> for AstError { +impl<'input> FromRecoverableError, ParseError> for ParseError { fn from_recoverable_error( - token_start: &::Checkpoint, - _err_start: &::Checkpoint, - input: &Input, - mut e: AstError, + token_start: & as winnow::stream::Stream>::Checkpoint, + _err_start: & as winnow::stream::Stream>::Checkpoint, + input: &Input<'input>, + mut e: ParseError, ) -> Self { - e.span = e.span.or_else(|| { - let offset = input.offset_from(token_start); - - let mut tokens = input - .previous_tokens() - .take(offset) - .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 end = last.source().get_range().end; - - Some(SourceSpan { range: start..end }) - } - } - }); + e.span = e + .span + .or_else(|| Some(span_from_checkpoint(input, token_start))); e } } -impl AddContext, AstError> for AstError { +impl<'input> AddContext, ParseError> for ParseError { fn add_context( mut self, - _input: &Input, - _token_start: &::Checkpoint, - context: AstError, + _input: &Input<'input>, + _token_start: & as Stream>::Checkpoint, + context: ParseError, ) -> Self { self.message = context.message.or(self.message); self.help = context.help.or(self.help); @@ -133,11 +164,22 @@ impl AddContext, AstError> for AstError { } } -impl ParserError> for AstError { - type Inner = AstError; +fn span_from_checkpoint( + input: &I, + token_start: &::Checkpoint, +) -> SourceSpan { + let offset = input.offset_from(token_start); - fn from_input(_input: &Input<'_>) -> Self { - AstError::ctx() + SourceSpan { + range: (input.current_token_start() - offset)..input.current_token_start(), + } +} + +impl<'input> ParserError> for ParseError { + type Inner = ParseError; + + fn from_input(_input: &Input<'input>) -> Self { + ParseError::ctx() } fn into_inner(self) -> winnow::Result { @@ -149,901 +191,659 @@ impl ParserError> for AstError { } } -#[derive(Debug, Error)] -pub struct AstFailure { - errors: Vec, +#[derive(Debug)] +pub struct ParsedTemplate { + tokens: Vec, } -impl std::fmt::Display for AstFailure { +impl ParsedTemplate { + pub fn tokens(&self) -> &[TemplateToken] { + &self.tokens + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TokenKind { + Content, + LeftDelim, + RightDelim, + TrimWhitespace, + WantsOutput, + Ident, + Whitespace, + Invalid, + ConditionalIf, + ConditionalElse, + For, + In, + End, + Literal(TokenLiteral), + Operator(TokenOperator), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TokenOperator { + Plus, + Minus, + Times, + Divide, + And, + Or, + Equal, + NotEqual, + Greater, + GreaterOrEqual, + Lesser, + LesserOrEqual, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TokenLiteral { + Bool(bool), + Integer(u64), +} + +impl PartialEq for TemplateToken { + fn eq(&self, other: &TokenKind) -> bool { + self.kind == *other + } +} + +impl winnow::stream::ContainsToken<&'_ TemplateToken> for TokenKind { + fn contains_token(&self, token: &'_ TemplateToken) -> bool { + *self == token.kind + } +} + +impl winnow::stream::ContainsToken<&'_ TemplateToken> for &'_ [TokenKind] { + fn contains_token(&self, token: &'_ TemplateToken) -> bool { + self.contains(&token.kind) + } +} + +impl winnow::stream::ContainsToken<&'_ TemplateToken> for &'_ [TokenKind; LEN] { + fn contains_token(&self, token: &'_ TemplateToken) -> bool { + self.contains(&token.kind) + } +} + +impl winnow::stream::ContainsToken<&'_ TemplateToken> for [TokenKind; LEN] { + fn contains_token(&self, token: &'_ TemplateToken) -> bool { + self.contains(&token.kind) + } +} + +#[derive(Clone, PartialEq, Eq)] +pub struct TemplateToken { + kind: TokenKind, + source: NomoInput, +} + +impl std::fmt::Debug for TemplateToken { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("TODO") + write!(f, "[{:?}]{:?}", self.kind(), self.source()) } } -impl AstFailure { - fn from_errors(errors: Vec) -> AstFailure { - AstFailure { errors } +impl Location for TemplateToken { + fn previous_token_end(&self) -> usize { + NomoInput::get_range(&self.source).start } - pub fn to_report(&self, source: &str) -> String { - let reports = self - .errors - .iter() - .map(|error| { - annotate_snippets::Level::ERROR - .primary_title( - error - .message - .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)), - ), - ) - .elements( - error - .help - .as_ref() - .map(|help| annotate_snippets::Level::HELP.message(help)), - ) - }) - .collect::>(); - - let renderer = annotate_snippets::Renderer::styled() - .decor_style(annotate_snippets::renderer::DecorStyle::Unicode); - renderer.render(&reports) + fn current_token_start(&self) -> usize { + NomoInput::get_range(&self.source).start } } -type Input<'input> = Recoverable, AstError>; +macro_rules! impl_token_kind_builders { + ($($name:ident => $kind:expr),+ $(,)?) => { + $( + fn $name(source: NomoInput) -> Self { + TemplateToken { + kind: $kind, + source, + } + } + )+ + }; +} -impl<'input> Parser, TemplateToken, AstError> for TokenKind { - fn parse_next(&mut self, input: &mut Input<'input>) -> winnow::Result { - winnow::token::literal(*self) - .parse_next(input) - .map(|t| t[0].clone()) +impl TemplateToken { + impl_token_kind_builders! { + content => TokenKind::Content, + left_delim => TokenKind::LeftDelim, + right_delim => TokenKind::RightDelim, + trim_whitespace => TokenKind::TrimWhitespace, + wants_output => TokenKind::WantsOutput, + ident => TokenKind::Ident, + whitespace => TokenKind::Whitespace, + invalid => TokenKind::Invalid, + conditional_if => TokenKind::ConditionalIf, + conditional_else => TokenKind::ConditionalElse, + keyword_for => TokenKind::For, + keyword_in => TokenKind::In, + end => TokenKind::End, + } + + pub fn literal(literal: TokenLiteral, source: NomoInput) -> Self { + TemplateToken { + kind: TokenKind::Literal(literal), + source, + } + } + + pub fn operator(operator: TokenOperator, source: NomoInput) -> Self { + TemplateToken { + kind: TokenKind::Operator(operator), + source, + } + } + + pub fn kind(&self) -> TokenKind { + self.kind + } + + pub fn source(&self) -> NomoInput { + self.source.clone() } } -pub fn parse(input: &[TemplateToken]) -> Result, AstFailure> { - let (_remaining, val, errors) = parse_asts.recoverable_parse(TokenSlice::new(input)); +pub fn parse(input: NomoInput) -> Result { + let (_remaining, val, errors) = + parse_tokens.recoverable_parse(LocatingSlice::new(input.clone())); if errors.is_empty() && let Some(val) = val { - Ok(TemplateAst { root: val }) + Ok(ParsedTemplate { tokens: val }) } else { - Err(AstFailure::from_errors(errors)) + Err(ParseFailure::from_errors(errors, input)) } } -#[derive(Debug, Clone)] -pub enum TemplateAstExpr<'input> { - StaticContent(TemplateToken), - Block { - prev_whitespace_content: Option, - expression: Box>, - post_whitespace_content: Option, - }, - Interpolation { - prev_whitespace_content: Option, - expression: Box>, - post_whitespace_content: Option, - }, - ConditionalChain { - chain: Vec>, - }, - ForChain { - for_block: Box>, - content: Vec>, - else_block: Option>>, - else_content: Option>>, - end_block: Box>, - }, - For { - value_ident: TemplateToken, - value_expression: Box>, - }, - ForElse, - VariableAccess(TemplateToken), - IfConditional { - expression: Box>, - }, - ConditionalContent { - content: Vec>, - }, - ElseConditional { - expression: Option>>, - }, - Invalid(&'input [TemplateToken]), - Operation { - op: TokenOperator, - lhs: Box>, - rhs: Box>, - }, - EndBlock, - Literal { - source: TemplateToken, - value: NomoValue, - }, - FunctionCall { - name: TemplateToken, - args: Vec>, - }, +fn parse_tokens<'input>(input: &mut Input<'input>) -> PResult<'input, Vec> { + repeat_till(0.., alt((parse_interpolate, parse_content)), eof) + .map(|(v, _): (Vec<_>, _)| v.into_iter().flatten().collect()) + .parse_next(input) } -fn parse_asts<'input>(input: &mut Input<'input>) -> Result>, AstError> { - repeat(0.., parse_ast).parse_next(input) -} -fn parse_ast<'input>(input: &mut Input<'input>) -> Result, AstError> { +fn parse_content<'input>(input: &mut Input<'input>) -> PResult<'input, Vec> { alt(( - trace( - "content", - TokenKind::Content.map(TemplateAstExpr::StaticContent), - ), - trace("interpolation", parse_interpolation), - parse_action, + repeat_till(1.., any, peek((multispace0, "{{"))).map(|((), _)| ()), + rest.void(), + )) + .take() + .map(TemplateToken::content) + .map(|v| vec![v]) + .parse_next(input) +} + +fn parse_interpolate<'input>(input: &mut Input<'input>) -> PResult<'input, Vec> { + let prev_whitespace = opt(parse_whitespace).parse_next(input)?; + let left_delim = "{{".map(TemplateToken::left_delim).parse_next(input)?; + let left_trim = opt("-".map(TemplateToken::trim_whitespace)).parse_next(input)?; + let wants_output = opt("=".map(TemplateToken::wants_output)).parse_next(input)?; + + let get_tokens = repeat_till(1.., parse_block_token, peek(preceded(opt("-"), "}}"))); + let recover = take_until(0.., "}}").void(); + + let (inside_tokens, _): (Vec<_>, _) = get_tokens + .resume_after(recover) + .with_taken() + .map(|(val, taken)| { + val.unwrap_or_else(|| (vec![TemplateToken::invalid(taken)], NomoInput::from(""))) + }) + .parse_next(input)?; + + let right_trim = opt("-".map(TemplateToken::trim_whitespace)).parse_next(input)?; + let right_delim = "}}".map(TemplateToken::right_delim).parse_next(input)?; + let post_whitespace = opt(parse_whitespace).parse_next(input)?; + + let mut tokens = vec![]; + tokens.extend(prev_whitespace); + tokens.push(left_delim); + tokens.extend(left_trim); + tokens.extend(wants_output); + tokens.extend(inside_tokens); + tokens.extend(right_trim); + tokens.push(right_delim); + tokens.extend(post_whitespace); + + Ok(tokens) +} + +fn parse_block_token<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { + trace( + "parse_block_token", + alt((parse_ident, parse_keyword, parse_whitespace, parse_operator)), + ) + .parse_next(input) +} + +fn parse_literal<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { + trace( + "parse_literal", + alt((parse_boolean, parse_number)) + .with_taken() + .map(|(lit, span)| TemplateToken::literal(lit, span)), + ) + .parse_next(input) +} + +fn parse_number<'input>(input: &mut Input<'input>) -> PResult<'input, TokenLiteral> { + digit1 + .verify_map(|digits: NomoInput| digits.parse::().ok()) + .map(TokenLiteral::Integer) + .parse_next(input) +} + +fn parse_boolean<'input>(input: &mut Input<'input>) -> PResult<'input, TokenLiteral> { + alt(( + "true".value(TokenLiteral::Bool(true)), + "false".value(TokenLiteral::Bool(false)), )) .parse_next(input) } -fn parse_interpolation<'input>( - input: &mut Input<'input>, -) -> Result, AstError> { - let expr_parser = resume_after_cut( - parse_expression, - repeat_till( - 0.., - any, - peek(preceded( - opt(TokenKind::TrimWhitespace), - TokenKind::RightDelim, - )), +fn parse_condition_if<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { + trace( + "parse_condition_if", + "if".map(TemplateToken::conditional_if), + ) + .parse_next(input) +} + +fn parse_condition_else<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { + trace( + "parse_condition_else", + "else".map(TemplateToken::conditional_else), + ) + .parse_next(input) +} + +fn parse_end<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { + trace("parse_end", "end".map(TemplateToken::end)).parse_next(input) +} + +fn parse_for<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { + trace("parse_for", "for".map(TemplateToken::keyword_for)).parse_next(input) +} + +fn parse_in<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { + trace("parse_in", "in".map(TemplateToken::keyword_in)).parse_next(input) +} + +fn parse_keyword<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { + alt(( + terminated(parse_literal, ident_terminator_check), + terminated(parse_condition_if, ident_terminator_check), + terminated(parse_condition_else, ident_terminator_check), + terminated(parse_for, ident_terminator_check), + terminated(parse_in, ident_terminator_check), + terminated(parse_end, ident_terminator_check), + )) + .parse_next(input) +} + +fn parse_whitespace<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { + trace( + "parse_whitespace", + multispace1.map(TemplateToken::whitespace), + ) + .parse_next(input) +} + +fn parse_ident<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { + resume_after_cut( + terminated( + ident.map(TemplateToken::ident), + cut_err(ident_terminator_check), ) - .map(|((), _)| ()), + .context( + ParseError::ctx() + .msg("Invalid variable identifier") + .help("valid variable identifiers are alphanumeric"), + ), + bad_ident, ) .with_taken() - .map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken))); - let ( - prev_whitespace, - _left, - left_trim, - _wants_output, - (expression, right_trim, _right, post_whitespace), - ) = ( - opt(TokenKind::Whitespace), - TokenKind::LeftDelim, - opt(TokenKind::TrimWhitespace), - TokenKind::WantsOutput, - cut_err(( - surrounded(ws, expr_parser).map(Box::new), - opt(TokenKind::TrimWhitespace), - TokenKind::RightDelim, - opt(TokenKind::Whitespace), - )), - ) - .parse_next(input)?; - - Ok(TemplateAstExpr::Interpolation { - prev_whitespace_content: if left_trim.is_some() { - None - } else { - prev_whitespace - }, - expression, - post_whitespace_content: if right_trim.is_some() { - None - } else { - post_whitespace - }, - }) + .map(|(val, taken)| val.unwrap_or(TemplateToken::invalid(taken))) + .parse_next(input) } -fn parse_action<'input>(input: &mut Input<'input>) -> Result, AstError> { - trace( - "action", - alt(( - parse_conditional_chain, - parse_for_chain, - (parse_block( - cut_err(not(repeat_till( - 0.., - any, - peek((ws, TokenKind::RightDelim)), - ) - .map(|((), _)| ()))) - .context( - AstError::ctx() - .msg("Standlone action block") - .help("If you want to output this expression, add a '=' to the block"), - ) - .take() - .map(TemplateAstExpr::Invalid), +fn parse_operator<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { + let (operator, source) = trace( + "operator", + dispatch! {any; + '+' => empty.value(TokenOperator::Plus), + '-' => empty.value(TokenOperator::Minus), + '*' => empty.value(TokenOperator::Times), + '/' => empty.value(TokenOperator::Divide), + '&' => alt(( + "&".value(TokenOperator::And), + cut_err(fail), )), - )), - ) - .parse_next(input) -} - -fn parse_for_chain<'input>(input: &mut Input<'input>) -> Result, AstError> { - trace("for_loop", |input: &mut Input<'input>| { - let for_block = parse_for_loop.map(Box::new).parse_next(input)?; - - let loop_end = ( - opt(( - parse_loop_else.map(Box::new), - repeat_till(0.., parse_ast, peek(parse_end)).map(|(val, _)| val), + '|' => alt(( + "|".value(TokenOperator::Or), + cut_err(fail), )), - parse_end.map(Box::new), - ); - - let (content, taken) = resume_after_cut( - repeat_till(0.., parse_ast, loop_end), - repeat_till(0.., any, parse_end).map(|((), _)| ()), - ) - .with_taken() - .parse_next(input)?; - - let Some((content, (else_stuff, end_block))) = content else { - return Ok(TemplateAstExpr::Invalid(taken)); - }; - - let (else_block, else_content) = else_stuff.unzip(); - - Ok(TemplateAstExpr::ForChain { - for_block, - content, - else_block, - else_content, - end_block, - }) - }) - .parse_next(input) -} - -fn parse_for_loop<'input>(input: &mut Input<'input>) -> Result, AstError> { - trace( - "for_loop_inner", - parse_block( - ( - ws, - TokenKind::For, - ws, - TokenKind::Ident, - ws, - TokenKind::In, - ws, - parse_expression.map(Box::new), - ) - .map(|(_, _for, _, value_ident, _, _in, _, value_expression)| { - TemplateAstExpr::For { - value_ident, - value_expression, - } - }), - ), - ) - .parse_next(input) -} - -fn parse_conditional_chain<'input>( - input: &mut Input<'input>, -) -> Result, AstError> { - trace("conditional_chain", |input: &mut Input<'input>| { - let if_block = parse_conditional_if.parse_next(input)?; - let mut chain = vec![]; - - chain.push(if_block); - - let content = resume_after_cut( - cut_err(inner_conditional_chain), - repeat_till(0.., any, parse_end).map(|((), _)| ()), - ) - .parse_next(input)?; - - chain.extend(content.into_iter().flatten()); - - Ok(TemplateAstExpr::ConditionalChain { chain }) - }) - .parse_next(input) -} - -fn inner_conditional_chain<'input>( - input: &mut Input<'input>, -) -> Result>, AstError> { - let mut needs_end = false; - - let mut chain = vec![]; - - loop { - let (content, end_block): (Vec<_>, _) = repeat_till( - 0.., - parse_ast, - trace( - "conditional_chain_else/end", - alt((parse_end, parse_conditional_else)), - ), - ) - .parse_next(input)?; - - chain.push(TemplateAstExpr::ConditionalContent { content }); - - let is_end = if let TemplateAstExpr::Block { ref expression, .. } = end_block - && let TemplateAstExpr::EndBlock = &**expression - { - true - } else { - false - }; - - if !is_end && needs_end { - return Err(AstError::from_input(input)); - } - - if let TemplateAstExpr::Block { expression, .. } = &end_block - && let TemplateAstExpr::ElseConditional { expression: None } = &**expression - { - needs_end = true; - } - - chain.push(end_block); - - if is_end { - break; - } - } - - Ok(chain) -} - -fn parse_conditional_if<'input>( - input: &mut Input<'input>, -) -> Result, AstError> { - trace( - "conditional", - parse_block( - preceded( - TokenKind::ConditionalIf, - cut_err( - surrounded(ws, parse_expression) - .map(Box::new) - .context(AstError::ctx().msg("Expected an expression after 'if'")), - ), - ) - .map(|expression| TemplateAstExpr::IfConditional { expression }), - ), - ) - .parse_next(input) -} - -fn parse_conditional_else<'input>( - input: &mut Input<'input>, -) -> Result, AstError> { - trace( - "conditional_else", - parse_block( - preceded( - surrounded(ws, TokenKind::ConditionalElse), - opt(preceded( - TokenKind::ConditionalIf, - cut_err(surrounded(ws, parse_expression)).map(Box::new), - )), - ) - .map(|else_expr| TemplateAstExpr::ElseConditional { - expression: else_expr, - }), - ), - ) - .parse_next(input) -} - -fn parse_loop_else<'input>(input: &mut Input<'input>) -> Result, AstError> { - trace( - "end", - parse_block( - TokenKind::ConditionalElse - .value(TemplateAstExpr::ForElse) - .context(AstError::ctx().msg("Expected an else block here")), - ), - ) - .parse_next(input) -} - -fn parse_end<'input>(input: &mut Input<'input>) -> Result, AstError> { - trace( - "end", - parse_block( - TokenKind::End - .value(TemplateAstExpr::EndBlock) - .context(AstError::ctx().msg("Expected an end block here")), - ), - ) - .parse_next(input) -} - -fn parse_function<'input>(input: &mut Input<'input>) -> Result, AstError> { - trace( - "variable_access", - ( - TokenKind::Ident, - TokenKind::LeftArgList, - separated(0.., parse_expression, TokenKind::ArgSeperator), - TokenKind::RightArgList, - ) - .map(|(name, _left, args, _right)| TemplateAstExpr::FunctionCall { name, args }), - ) - .parse_next(input) -} - -fn parse_variable_access<'input>( - input: &mut Input<'input>, -) -> Result, AstError> { - trace( - "variable_access", - TokenKind::Ident.map(TemplateAstExpr::VariableAccess), - ) - .parse_next(input) -} - -fn parse_block<'input, ParseNext>( - parser: ParseNext, -) -> impl Parser, TemplateAstExpr<'input>, AstError> -where - ParseNext: Parser, TemplateAstExpr<'input>, AstError>, -{ - let expr_parser = resume_after_cut( - parser, - repeat_till(0.., any, peek(TokenKind::RightDelim)).map(|((), _)| ()), + '<' => alt(( + "=".value(TokenOperator::LesserOrEqual), + empty.value(TokenOperator::Lesser), + )), + '>' => alt(( + "=".value(TokenOperator::GreaterOrEqual), + empty.value(TokenOperator::Greater), + )), + '!' => alt(( + "=".value(TokenOperator::NotEqual), + cut_err(fail), + )), + '=' => alt(( + "=".value(TokenOperator::Equal), + cut_err(fail), + )), + _ => fail, + }, ) .with_taken() - .map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken))); + .parse_next(input)?; + + Ok(TemplateToken::operator(operator, source)) +} + +fn ident<'input>(input: &mut Input<'input>) -> PResult<'input, NomoInput> { + peek(not(parse_keyword)) + .context(ParseError::ctx().msg("Expected an ident, but found a literal instead")) + .parse_next(input)?; + + let literal_start = alt((alpha1, "_")); ( - opt(TokenKind::Whitespace), - TokenKind::LeftDelim, - opt(TokenKind::TrimWhitespace), - not(TokenKind::WantsOutput), - ( - surrounded(ws, expr_parser.map(Box::new)), - opt(TokenKind::TrimWhitespace), - TokenKind::RightDelim, - opt(TokenKind::Whitespace), - ), + literal_start, + take_while(0.., |c: char| c.is_alphanumeric() || "_".contains(c)), ) - .map( - |( - prev_whitespace, - _left, - left_trim, - _not_token, - (expression, right_trim, _right, post_whitespace), - )| { - TemplateAstExpr::Block { - prev_whitespace_content: if left_trim.is_some() { - None - } else { - prev_whitespace - }, - expression, - post_whitespace_content: if right_trim.is_some() { - None - } else { - post_whitespace - }, - } - }, - ) + .take() + .parse_next(input) } -fn parse_operand<'input>(input: &mut Input<'input>) -> Result, AstError> { - trace( - "operand", - alt((parse_function, parse_variable_access, parse_literal)), - ) +fn bad_ident<'input>(input: &mut Input<'input>) -> PResult<'input, ()> { + repeat_till(1.., any, ident_terminator_check) + .map(|((), _)| ()) + .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) } -fn parse_literal<'input>(input: &mut Input<'input>) -> Result, AstError> { - trace( - "literal", - any.verify_map(|token: &TemplateToken| match token.kind() { - TokenKind::Literal(literal) => Some(TemplateAstExpr::Literal { - source: token.clone(), - value: match literal { - crate::lexer::TokenLiteral::Bool(bool) => NomoValue::Bool { value: bool }, - crate::lexer::TokenLiteral::Integer(int) => NomoValue::Integer { value: int }, - }, - }), - _ => None, - }), - ) - .parse_next(input) -} - -fn parse_operator<'input>(input: &mut Input<'input>) -> Result { - trace( - "operator", - any.verify_map(|t: &'input TemplateToken| match t.kind() { - TokenKind::Operator(op) => Some(op), - _ => None, - }), - ) - .parse_next(input) -} - -fn parse_expression<'input>( - input: &mut Input<'input>, -) -> Result, AstError> { - macro_rules! infix { - ($parser:expr => [ $($side:tt $val:tt => $prec:expr),* $(,)? ]) => { - dispatch! { surrounded(ws, parse_operator); - $( - TokenOperator::$val => $side($prec, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { - op: TokenOperator::$val, - lhs: Box::new(lhs), - rhs: Box::new(rhs) - })) - ),* - } - }; - } - trace( - "expression", - expression(surrounded(ws, parse_operand)).infix(infix! { - surrounded(ws, parse_operator) => [ - Left Plus => 18, - Left Minus => 18, - Left Times => 20, - Left Divide => 20, - Left And => 10, - Left Or => 7, - Left Equal => 12, - Left NotEqual => 12, - Left Greater => 15, - Left GreaterOrEqual => 15, - Left Lesser => 15, - Left LesserOrEqual => 15, - ] - }), - ) - .parse_next(input) -} - -fn ws<'input>(input: &mut Input<'input>) -> Result<(), AstError> { - repeat(.., TokenKind::Whitespace).parse_next(input) -} - -fn surrounded( - ignored: IgnoredParser, - parser: ParseNext, -) -> impl Parser -where - Input: Stream, - Error: ParserError, - IgnoredParser: Parser, - IgnoredParser: Clone, - ParseNext: Parser, -{ - delimited(ignored.clone(), parser, ignored) -} - #[cfg(test)] mod tests { - use winnow::Parser; - use winnow::combinator::alt; - use winnow::combinator::fail; - use winnow::stream::TokenSlice; - - use crate::parser::AstError; - use crate::parser::AstFailure; - use crate::parser::TemplateAst; - use crate::parser::TemplateAstExpr; use crate::parser::parse; - use crate::parser::parse_block; - use crate::parser::parse_end; - use crate::lexer::TokenKind; - - fn panic_pretty<'a>( - input: &'_ str, - tokens: Result, AstFailure>, - ) -> TemplateAst<'a> { - match tokens { - Ok(ast) => ast, - Err(failure) => { - panic!("{}", failure.to_report(input)); - } - } - } #[test] - fn check_only_content() { - let input = "Hello World"; + fn parse_simple() { + let input = "Hello There"; + let output = parse(input.into()); - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let ast = parse(parsed.tokens()).unwrap(); - - insta::assert_debug_snapshot!(ast, @r#" - TemplateAst { - root: [ - StaticContent( - [Content]"Hello World" (0..11), - ), - ], - } - "#); - } - - #[test] - fn check_simple_variable_interpolation() { - let input = "Hello {{= world }}"; - - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let ast = parse(parsed.tokens()).unwrap(); - - insta::assert_debug_snapshot!(ast, @r#" - TemplateAst { - root: [ - StaticContent( - [Content]"Hello" (0..5), - ), - Interpolation { - prev_whitespace_content: Some( - [Whitespace]" " (5..6), - ), - expression: VariableAccess( - [Ident]"world" (10..15), - ), - post_whitespace_content: None, - }, - ], - } - "#); - } - - #[test] - fn check_simple_if() { - let input = "{{ if foo }} Hiii {{ end }}"; - - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let ast = panic_pretty(input, parse(parsed.tokens())); - - insta::assert_debug_snapshot!(ast, @r#" - TemplateAst { - root: [ - ConditionalChain { - chain: [ - Block { - prev_whitespace_content: None, - expression: IfConditional { - expression: VariableAccess( - [Ident]"foo" (6..9), - ), - }, - post_whitespace_content: Some( - [Whitespace]" " (12..13), - ), - }, - ConditionalContent { - content: [ - StaticContent( - [Content]"Hiii" (13..17), - ), - ], - }, - Block { - prev_whitespace_content: Some( - [Whitespace]" " (17..18), - ), - expression: EndBlock, - post_whitespace_content: None, - }, - ], - }, - ], - } - "#); - } - - #[test] - fn check_invalid_action() { - let input = r#"{{ value }} - {{ value }} - {{ value }} - {{ value }} - {{ value }}"#; - - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let ast = parse(parsed.tokens()).unwrap_err(); - - insta::assert_snapshot!(ast.to_report(input)); - } - - #[test] - fn check_nested_simple_if() { - let input = r#"{{ if foo }} - {{ if bar }} - Hiii - {{ end }} - {{ end }} - - {{= value }} - "#; - - let parsed = crate::lexer::parse(input.into()).unwrap(); - - insta::assert_debug_snapshot!("simple_if_tokens", parsed); - - let ast = panic_pretty(input, parse(parsed.tokens())); - - insta::assert_debug_snapshot!("simple_if_ast", ast); - } - - #[test] - fn check_parsing_block() { - use winnow::RecoverableParser; - - let input = "{{ foo }}"; - - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let result = alt(( - parse_end, - parse_block( - (TokenKind::Ident.void(), fail::<_, (), _>) - .void() - .context(AstError::ctx().msg("No ident allowed")) - .take() - .map(TemplateAstExpr::Invalid), - ), - )) - .recoverable_parse(TokenSlice::new(parsed.tokens())); - - insta::assert_debug_snapshot!(result, @r#" - ( - [ - [LeftDelim]"{{" (0..2), - [Whitespace]" " (2..3), - [Ident]"foo" (3..6), - [Whitespace]" " (6..7), - [RightDelim]"}}" (7..9), - ], - None, - [ - AstError { - message: Some( - "No ident allowed", - ), - help: None, - span: Some( - SourceSpan { - range: 0..6, - }, - ), - is_fatal: false, - }, - ], + insta::assert_debug_snapshot!(output, @r#" + Ok( + ParsedTemplate { + tokens: [ + [Content]"Hello There" (0..11), + ], + }, ) "#); } #[test] - fn check_empty_if_output() { - let input = "{{ if foo }}{{ end }}"; + fn parse_interpolate() { + let input = "Hello {{ there }}"; + let output = parse(input.into()); - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let ast = panic_pretty(input, parse(parsed.tokens())); - - insta::assert_debug_snapshot!(ast, @r#" - TemplateAst { - root: [ - ConditionalChain { - chain: [ - Block { - prev_whitespace_content: None, - expression: IfConditional { - expression: VariableAccess( - [Ident]"foo" (6..9), - ), - }, - post_whitespace_content: None, - }, - ConditionalContent { - content: [], - }, - Block { - prev_whitespace_content: None, - expression: EndBlock, - post_whitespace_content: None, - }, - ], - }, - ], - } + insta::assert_debug_snapshot!(output, @r#" + Ok( + ParsedTemplate { + tokens: [ + [Content]"Hello" (0..5), + [Whitespace]" " (5..6), + [LeftDelim]"{{" (6..8), + [Whitespace]" " (8..9), + [Ident]"there" (9..14), + [Whitespace]" " (14..15), + [RightDelim]"}}" (15..17), + ], + }, + ) "#); } #[test] - fn check_if_else() { - let input = "{{ if foo }} foo {{ else }} bar {{ end }}"; + fn parse_interpolate_bad() { + let input = "Hello {{ the2re }} {{ the@re }}"; + let output = parse(input.into()); - let parsed = crate::lexer::parse(input.into()).unwrap(); + insta::assert_debug_snapshot!(output, @r#" + Err( + ParseFailure { + input: "Hello {{ the2re }} {{ the@re }}", + errors: [ + ParseError { + message: Some( + "Invalid variable identifier", + ), + help: Some( + "valid variable identifiers are alphanumeric", + ), + span: Some( + SourceSpan { + range: 22..28, + }, + ), + is_fatal: true, + }, + ], + }, + ) + "#); - let ast = panic_pretty(input, parse(parsed.tokens())); + let error = output.unwrap_err(); - insta::assert_debug_snapshot!(ast); + insta::assert_snapshot!(error.to_report()); } #[test] - fn check_if_else_if() { - let input = "{{ if foo }} foo {{ else if bar }} bar {{ end }}"; + fn parse_simple_condition() { + let input = "{{ if true }} Hello! {{ else }} Bye {{ end }}"; + let output = parse(input.into()); - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let ast = panic_pretty(input, parse(parsed.tokens())); - - insta::assert_debug_snapshot!(ast); + insta::assert_debug_snapshot!(output, @r#" + Ok( + ParsedTemplate { + tokens: [ + [LeftDelim]"{{" (0..2), + [Whitespace]" " (2..3), + [ConditionalIf]"if" (3..5), + [Whitespace]" " (5..6), + [Literal(Bool(true))]"true" (6..10), + [Whitespace]" " (10..11), + [RightDelim]"}}" (11..13), + [Whitespace]" " (13..14), + [Content]"Hello!" (14..20), + [Whitespace]" " (20..21), + [LeftDelim]"{{" (21..23), + [Whitespace]" " (23..24), + [ConditionalElse]"else" (24..28), + [Whitespace]" " (28..29), + [RightDelim]"}}" (29..31), + [Whitespace]" " (31..32), + [Content]"Bye" (32..35), + [Whitespace]" " (35..36), + [LeftDelim]"{{" (36..38), + [Whitespace]" " (38..39), + [End]"end" (39..42), + [Whitespace]" " (42..43), + [RightDelim]"}}" (43..45), + ], + }, + ) + "#); } #[test] - fn check_trim_whitespace() { - let input = "{{ if foo -}} foo {{- else if bar -}} bar {{- end }}"; + fn parse_trim_whitespace() { + let input = "\n\n{{-= hello -}} \n\n"; + let output = parse(input.into()); - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let ast = panic_pretty(input, parse(parsed.tokens())); - - insta::assert_debug_snapshot!(ast); + insta::assert_debug_snapshot!(output, @r#" + Ok( + ParsedTemplate { + tokens: [ + [Whitespace]"\n\n" (0..2), + [LeftDelim]"{{" (2..4), + [TrimWhitespace]"-" (4..5), + [WantsOutput]"=" (5..6), + [Whitespace]" " (6..7), + [Ident]"hello" (7..12), + [Whitespace]" " (12..13), + [TrimWhitespace]"-" (13..14), + [RightDelim]"}}" (14..16), + [Whitespace]" \n\n" (16..19), + ], + }, + ) + "#); } #[test] - fn check_for_loop() { - let input = "{{ for value in array }} Hi: {{= value }} {{ else }} No Content :C {{ end }}"; + fn parse_for_loop() { + let input = "{{ for value in array }} Hi: {{= value }} {{ end }}"; + let output = parse(input.into()); - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let ast = panic_pretty(input, parse(parsed.tokens())); - - insta::assert_debug_snapshot!(ast); + insta::assert_debug_snapshot!(output, @r#" + Ok( + ParsedTemplate { + tokens: [ + [LeftDelim]"{{" (0..2), + [Whitespace]" " (2..3), + [For]"for" (3..6), + [Whitespace]" " (6..7), + [Ident]"value" (7..12), + [Whitespace]" " (12..13), + [In]"in" (13..15), + [Whitespace]" " (15..16), + [Ident]"array" (16..21), + [Whitespace]" " (21..22), + [RightDelim]"}}" (22..24), + [Whitespace]" " (24..25), + [Content]"Hi:" (25..28), + [Whitespace]" " (28..29), + [LeftDelim]"{{" (29..31), + [WantsOutput]"=" (31..32), + [Whitespace]" " (32..33), + [Ident]"value" (33..38), + [Whitespace]" " (38..39), + [RightDelim]"}}" (39..41), + [Whitespace]" " (41..42), + [LeftDelim]"{{" (42..44), + [Whitespace]" " (44..45), + [End]"end" (45..48), + [Whitespace]" " (48..49), + [RightDelim]"}}" (49..51), + ], + }, + ) + "#); } #[test] - fn check_math_expression() { - let input = "{{= 5 * 3 + 2 / 3 }}"; + fn parse_operations() { + let input = "{{= 5 * 14 + 3 / 2 - 1 }}{{ if foo && bar || baz && 2 != 3 || 4 > 2 || 43 <= 5 }}{{ end }}"; + let output = parse(input.into()); - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let ast = panic_pretty(input, parse(parsed.tokens())); - - insta::assert_debug_snapshot!(ast); - } - - #[test] - fn check_logical_expression() { - let input = "{{= true && false || 3 >= 2 && 5 == 2 }}"; - - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let ast = panic_pretty(input, parse(parsed.tokens())); - - insta::assert_debug_snapshot!(ast); - } - - #[test] - fn check_function_call() { - let input = "{{= foo(2 * 3, bar(2 + baz)) }}"; - - let parsed = crate::lexer::parse(input.into()).unwrap(); - - let ast = panic_pretty(input, parse(parsed.tokens())); - - insta::assert_debug_snapshot!(ast); + insta::assert_debug_snapshot!(output, @r#" + Ok( + ParsedTemplate { + tokens: [ + [LeftDelim]"{{" (0..2), + [WantsOutput]"=" (2..3), + [Whitespace]" " (3..4), + [Literal(Integer(5))]"5" (4..5), + [Whitespace]" " (5..6), + [Operator(Times)]"*" (6..7), + [Whitespace]" " (7..8), + [Literal(Integer(14))]"14" (8..10), + [Whitespace]" " (10..11), + [Operator(Plus)]"+" (11..12), + [Whitespace]" " (12..13), + [Literal(Integer(3))]"3" (13..14), + [Whitespace]" " (14..15), + [Operator(Divide)]"/" (15..16), + [Whitespace]" " (16..17), + [Literal(Integer(2))]"2" (17..18), + [Whitespace]" " (18..19), + [Operator(Minus)]"-" (19..20), + [Whitespace]" " (20..21), + [Literal(Integer(1))]"1" (21..22), + [Whitespace]" " (22..23), + [RightDelim]"}}" (23..25), + [LeftDelim]"{{" (25..27), + [Whitespace]" " (27..28), + [ConditionalIf]"if" (28..30), + [Whitespace]" " (30..31), + [Ident]"foo" (31..34), + [Whitespace]" " (34..35), + [Operator(And)]"&&" (35..37), + [Whitespace]" " (37..38), + [Ident]"bar" (38..41), + [Whitespace]" " (41..42), + [Operator(Or)]"||" (42..44), + [Whitespace]" " (44..45), + [Ident]"baz" (45..48), + [Whitespace]" " (48..49), + [Operator(And)]"&&" (49..51), + [Whitespace]" " (51..52), + [Literal(Integer(2))]"2" (52..53), + [Whitespace]" " (53..54), + [Operator(NotEqual)]"!=" (54..56), + [Whitespace]" " (56..57), + [Literal(Integer(3))]"3" (57..58), + [Whitespace]" " (58..59), + [Operator(Or)]"||" (59..61), + [Whitespace]" " (61..62), + [Literal(Integer(4))]"4" (62..63), + [Whitespace]" " (63..64), + [Operator(Greater)]">" (64..65), + [Whitespace]" " (65..66), + [Literal(Integer(2))]"2" (66..67), + [Whitespace]" " (67..68), + [Operator(Or)]"||" (68..70), + [Whitespace]" " (70..71), + [Literal(Integer(43))]"43" (71..73), + [Whitespace]" " (73..74), + [Operator(LesserOrEqual)]"<=" (74..76), + [Whitespace]" " (76..77), + [Literal(Integer(5))]"5" (77..78), + [Whitespace]" " (78..79), + [RightDelim]"}}" (79..81), + [LeftDelim]"{{" (81..83), + [Whitespace]" " (83..84), + [End]"end" (84..87), + [Whitespace]" " (87..88), + [RightDelim]"}}" (88..90), + ], + }, + ) + "#); } } diff --git a/src/parser/snapshots/nomo__parser__tests__check_function_call.snap b/src/parser/snapshots/nomo__parser__tests__check_function_call.snap deleted file mode 100644 index 40e920c..0000000 --- a/src/parser/snapshots/nomo__parser__tests__check_function_call.snap +++ /dev/null @@ -1,49 +0,0 @@ ---- -source: src/parser/mod.rs -expression: ast ---- -TemplateAst { - root: [ - Interpolation { - prev_whitespace_content: None, - expression: FunctionCall { - name: [Ident]"foo" (4..7), - args: [ - Operation { - op: Times, - lhs: Literal { - source: [Literal(Integer(2))]"2" (8..9), - value: Integer { - value: 2, - }, - }, - rhs: Literal { - source: [Literal(Integer(3))]"3" (12..13), - value: Integer { - value: 3, - }, - }, - }, - FunctionCall { - name: [Ident]"bar" (15..18), - args: [ - Operation { - op: Plus, - lhs: Literal { - source: [Literal(Integer(2))]"2" (19..20), - value: Integer { - value: 2, - }, - }, - rhs: VariableAccess( - [Ident]"baz" (23..26), - ), - }, - ], - }, - ], - }, - post_whitespace_content: None, - }, - ], -} diff --git a/src/lexer/snapshots/nomo__lexer__tests__parse_interpolate_bad-2.snap b/src/parser/snapshots/nomo__parser__tests__parse_interpolate_bad-2.snap similarity index 93% rename from src/lexer/snapshots/nomo__lexer__tests__parse_interpolate_bad-2.snap rename to src/parser/snapshots/nomo__parser__tests__parse_interpolate_bad-2.snap index 9b30aec..9dd6238 100644 --- a/src/lexer/snapshots/nomo__lexer__tests__parse_interpolate_bad-2.snap +++ b/src/parser/snapshots/nomo__parser__tests__parse_interpolate_bad-2.snap @@ -1,5 +1,5 @@ --- -source: src/lexer/mod.rs +source: src/parser/mod.rs expression: error.to_report() --- error: Invalid variable identifier diff --git a/src/value.rs b/src/value.rs index 2d70084..965cb2b 100644 --- a/src/value.rs +++ b/src/value.rs @@ -5,6 +5,8 @@ use std::collections::BTreeMap; use displaydoc::Display; use thiserror::Error; +use crate::Nomo; + #[derive(Clone)] pub enum NomoValue { String { @@ -310,18 +312,6 @@ impl From<&'static str> for NomoValue { } } -impl From for NomoValue { - fn from(val: u64) -> Self { - NomoValue::Integer { value: val } - } -} - -impl From for NomoValue { - fn from(val: i64) -> Self { - NomoValue::SignedInteger { value: val } - } -} - impl From> for NomoValue where V: Into, @@ -367,43 +357,11 @@ where } } +#[cfg(feature = "serde_json")] #[derive(Debug, Error, Display)] -/// Could not transform value to/from [`NomoValue`] +/// Could not transform value to [`NomoValue`] pub struct NomoValueError; -impl TryFrom for String { - type Error = NomoValueError; - - fn try_from(value: NomoValue) -> Result { - match value { - NomoValue::String { value } => Ok(value.to_string()), - _ => Err(NomoValueError), - } - } -} - -impl TryFrom for i64 { - type Error = NomoValueError; - - fn try_from(value: NomoValue) -> Result { - match value { - NomoValue::SignedInteger { value } => Ok(value), - _ => Err(NomoValueError), - } - } -} - -impl TryFrom for u64 { - type Error = NomoValueError; - - fn try_from(value: NomoValue) -> Result { - match value { - NomoValue::Integer { value } => Ok(value), - _ => Err(NomoValueError), - } - } -} - #[cfg(feature = "serde_json")] impl TryFrom for NomoValue { type Error = NomoValueError; diff --git a/tests/checks.rs b/tests/checks.rs index 98480d8..56c736a 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -5,11 +5,11 @@ fn check_files() { for file in files { let input = std::fs::read_to_string(file.unwrap().path()).unwrap(); - let Ok(parsed) = nomo::lexer::parse(input.into()) else { + let Ok(parsed) = nomo::parser::parse(input.into()) else { continue; }; - let Ok(ast) = nomo::parser::parse(parsed.tokens()) else { + let Ok(ast) = nomo::ast::parse(parsed.tokens()) else { continue; }; diff --git a/tests/file_tests.rs b/tests/file_tests.rs index 6163922..db3c666 100644 --- a/tests/file_tests.rs +++ b/tests/file_tests.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::path::Path; use nomo::Context; -use nomo::functions::FunctionMap; test_each_file::test_each_path! { for ["nomo"] in "./tests/cases/" as cases => check_for_input } @@ -39,13 +38,13 @@ fn check_for_input([path]: [&Path; 1]) { context.try_insert(k, v).unwrap(); } - let parsed = nomo::lexer::parse(input.into()).unwrap(); + let parsed = nomo::parser::parse(input.into()).unwrap(); let _guard = settings.bind_to_scope(); insta::assert_debug_snapshot!(format!("{basename}.1-parsed"), parsed); - let ast = match nomo::parser::parse(parsed.tokens()) { + let ast = match nomo::ast::parse(parsed.tokens()) { Ok(ast) => ast, Err(err) => { eprintln!("{}", err.to_report(input)); @@ -59,7 +58,7 @@ fn check_for_input([path]: [&Path; 1]) { insta::assert_debug_snapshot!(format!("{basename}.3-instructions"), emit); - let output = nomo::eval::execute(&FunctionMap::default(), &emit, &context).unwrap(); + let output = nomo::eval::execute(&emit, &context).unwrap(); insta::assert_debug_snapshot!(format!("{basename}.4-output"), output); }