Parse and ast math expressions

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2026-03-12 11:25:55 +01:00
parent 8cc8488de4
commit 8c02dbd672
8 changed files with 196 additions and 51 deletions

View file

@ -1,9 +1,12 @@
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;
@ -24,7 +27,9 @@ 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> {
@ -253,7 +258,16 @@ pub enum TemplateAstExpr<'input> {
expression: Option<Box<TemplateAstExpr<'input>>>,
},
Invalid(&'input [TemplateToken]),
Operation {
op: TokenOperator,
lhs: Box<TemplateAstExpr<'input>>,
rhs: Box<TemplateAstExpr<'input>>,
},
EndBlock,
Literal {
source: TemplateToken,
value: NomoValue,
},
}
fn parse_asts<'input>(input: &mut Input<'input>) -> Result<Vec<TemplateAstExpr<'input>>, AstError> {
@ -275,7 +289,7 @@ fn parse_interpolation<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
let expr_parser = resume_after_cut(
parse_value_expression,
parse_expression,
repeat_till(
0..,
any,
@ -397,7 +411,7 @@ fn parse_for_loop<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'
ws,
TokenKind::In,
ws,
parse_value_expression.map(Box::new),
parse_expression.map(Box::new),
)
.map(|(_, _for, _, value_ident, _, _in, _, value_expression)| {
TemplateAstExpr::For {
@ -489,7 +503,7 @@ fn parse_conditional_if<'input>(
preceded(
TokenKind::ConditionalIf,
cut_err(
surrounded(ws, parse_value_expression)
surrounded(ws, parse_expression)
.map(Box::new)
.context(AstError::ctx().msg("Expected an expression after 'if'")),
),
@ -510,7 +524,7 @@ fn parse_conditional_else<'input>(
surrounded(ws, TokenKind::ConditionalElse),
opt(preceded(
TokenKind::ConditionalIf,
cut_err(surrounded(ws, parse_value_expression)).map(Box::new),
cut_err(surrounded(ws, parse_expression)).map(Box::new),
)),
)
.map(|else_expr| TemplateAstExpr::ElseConditional {
@ -545,12 +559,6 @@ fn parse_end<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input
.parse_next(input)
}
fn parse_value_expression<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
trace("value_expression", alt((parse_variable_access,))).parse_next(input)
}
fn parse_variable_access<'input>(
input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> {
@ -611,6 +619,55 @@ where
)
}
fn parse_operand<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
trace("operand", alt((parse_variable_access, parse_literal))).parse_next(input)
}
fn parse_literal<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, 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<TokenOperator, AstError> {
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<TemplateAstExpr<'input>, AstError> {
trace(
"expression",
expression(surrounded(ws, parse_operand)).infix(dispatch! { surrounded(ws, parse_operator);
TokenOperator::Plus => Left(5, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Plus, lhs: Box::new(lhs), rhs: Box::new(rhs) })),
TokenOperator::Minus => Left(5, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Minus, lhs: Box::new(lhs), rhs: Box::new(rhs) })),
TokenOperator::Times => Left(7, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Times, lhs: Box::new(lhs), rhs: Box::new(rhs) })),
TokenOperator::Divide => Left(7, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Divide, lhs: Box::new(lhs), rhs: Box::new(rhs) })),
TokenOperator::And => Left(7, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::And, lhs: Box::new(lhs), rhs: Box::new(rhs) })),
TokenOperator::Or => Left(5, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Or, lhs: Box::new(lhs), rhs: Box::new(rhs) })),
}),
)
.parse_next(input)
}
fn ws<'input>(input: &mut Input<'input>) -> Result<(), AstError> {
repeat(.., TokenKind::Whitespace).parse_next(input)
}
@ -912,4 +969,15 @@ mod tests {
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);
}
}

View file

@ -0,0 +1,45 @@
---
source: src/ast/mod.rs
expression: ast
---
TemplateAst {
root: [
Interpolation {
prev_whitespace_content: None,
expression: Operation {
op: Plus,
lhs: Operation {
op: Times,
lhs: Literal {
source: [Literal(Integer(5))]"5" (4..5),
value: Integer {
value: 5,
},
},
rhs: Literal {
source: [Literal(Integer(3))]"3" (8..9),
value: Integer {
value: 3,
},
},
},
rhs: Operation {
op: Divide,
lhs: Literal {
source: [Literal(Integer(2))]"2" (12..13),
value: Integer {
value: 2,
},
},
rhs: Literal {
source: [Literal(Integer(3))]"3" (16..17),
value: Integer {
value: 3,
},
},
},
},
post_whitespace_content: None,
},
],
}

View file

@ -402,6 +402,8 @@ fn emit_ast_expr(
| TemplateAstExpr::For { .. }
| TemplateAstExpr::ForElse
| TemplateAstExpr::Invalid { .. }
| TemplateAstExpr::Literal { .. }
| TemplateAstExpr::Operation { .. }
| TemplateAstExpr::VariableAccess { .. } => eval.push(Instruction::Abort),
}
}
@ -430,6 +432,8 @@ fn emit_expr_load(
TemplateAstExpr::ForChain { .. } => todo!(),
TemplateAstExpr::For { .. } => todo!(),
TemplateAstExpr::ForElse => todo!(),
TemplateAstExpr::Operation { .. } => todo!(),
TemplateAstExpr::Literal { .. } => todo!(),
TemplateAstExpr::IfConditional { .. } => todo!(),
TemplateAstExpr::ConditionalContent { .. } => todo!(),

View file

@ -33,6 +33,7 @@ use winnow::stream::Location;
use winnow::stream::Recoverable;
use winnow::stream::Stream;
use winnow::token::any;
use winnow::token::literal;
use winnow::token::one_of;
use winnow::token::rest;
use winnow::token::take_until;
@ -535,7 +536,7 @@ fn ident<'input>(input: &mut Input<'input>) -> PResult<'input, NomoInput> {
.context(ParseError::ctx().msg("Expected an ident, but found a literal instead"))
.parse_next(input)?;
let literal_start = alpha1;
let literal_start = alt((alpha1, "_"));
(
literal_start,

View file

@ -1,6 +1,15 @@
---
source: tests/file_tests.rs
expression: parsed
info:
input: "{{= _name }}\n{{= a_name }}\n{{= name }}\n{{= _name1 }}\n{{= _namE }}\n{{= name1 }}"
context:
_namE: Foo
name1: Foo
name: Foo
_name: Foo
a_name: Foo
_name1: Foo
input_file: tests/cases/identifiers.nomo
---
ParsedTemplate {
@ -22,29 +31,29 @@ ParsedTemplate {
[LeftDelim]"{{" (27..29),
[WantsOutput]"=" (29..30),
[Whitespace]" " (30..31),
[Ident]"1name" (31..36),
[Whitespace]" " (36..37),
[RightDelim]"}}" (37..39),
[Whitespace]"\n" (39..40),
[LeftDelim]"{{" (40..42),
[WantsOutput]"=" (42..43),
[Whitespace]" " (43..44),
[Ident]"_name1" (44..50),
[Whitespace]" " (50..51),
[RightDelim]"}}" (51..53),
[Whitespace]"\n" (53..54),
[LeftDelim]"{{" (54..56),
[WantsOutput]"=" (56..57),
[Whitespace]" " (57..58),
[Ident]"_namE" (58..63),
[Whitespace]" " (63..64),
[RightDelim]"}}" (64..66),
[Whitespace]"\n" (66..67),
[LeftDelim]"{{" (67..69),
[WantsOutput]"=" (69..70),
[Whitespace]" " (70..71),
[Ident]"name1" (71..76),
[Whitespace]" " (76..77),
[RightDelim]"}}" (77..79),
[Ident]"name" (31..35),
[Whitespace]" " (35..36),
[RightDelim]"}}" (36..38),
[Whitespace]"\n" (38..39),
[LeftDelim]"{{" (39..41),
[WantsOutput]"=" (41..42),
[Whitespace]" " (42..43),
[Ident]"_name1" (43..49),
[Whitespace]" " (49..50),
[RightDelim]"}}" (50..52),
[Whitespace]"\n" (52..53),
[LeftDelim]"{{" (53..55),
[WantsOutput]"=" (55..56),
[Whitespace]" " (56..57),
[Ident]"_namE" (57..62),
[Whitespace]" " (62..63),
[RightDelim]"}}" (63..65),
[Whitespace]"\n" (65..66),
[LeftDelim]"{{" (66..68),
[WantsOutput]"=" (68..69),
[Whitespace]" " (69..70),
[Ident]"name1" (70..75),
[Whitespace]" " (75..76),
[RightDelim]"}}" (76..78),
],
}

View file

@ -1,6 +1,15 @@
---
source: tests/file_tests.rs
expression: ast
info:
input: "{{= _name }}\n{{= a_name }}\n{{= name }}\n{{= _name1 }}\n{{= _namE }}\n{{= name1 }}"
context:
_namE: Foo
name1: Foo
name: Foo
_name: Foo
a_name: Foo
_name1: Foo
input_file: tests/cases/identifiers.nomo
---
TemplateAst {
@ -26,34 +35,34 @@ TemplateAst {
Interpolation {
prev_whitespace_content: None,
expression: VariableAccess(
[Ident]"1name" (31..36),
[Ident]"name" (31..35),
),
post_whitespace_content: Some(
[Whitespace]"\n" (39..40),
[Whitespace]"\n" (38..39),
),
},
Interpolation {
prev_whitespace_content: None,
expression: VariableAccess(
[Ident]"_name1" (44..50),
[Ident]"_name1" (43..49),
),
post_whitespace_content: Some(
[Whitespace]"\n" (53..54),
[Whitespace]"\n" (52..53),
),
},
Interpolation {
prev_whitespace_content: None,
expression: VariableAccess(
[Ident]"_namE" (58..63),
[Ident]"_namE" (57..62),
),
post_whitespace_content: Some(
[Whitespace]"\n" (66..67),
[Whitespace]"\n" (65..66),
),
},
Interpolation {
prev_whitespace_content: None,
expression: VariableAccess(
[Ident]"name1" (71..76),
[Ident]"name1" (70..75),
),
post_whitespace_content: None,
},

View file

@ -1,6 +1,15 @@
---
source: tests/file_tests.rs
expression: emit
info:
input: "{{= _name }}\n{{= a_name }}\n{{= name }}\n{{= _name1 }}\n{{= _namE }}\n{{= name1 }}"
context:
_namE: Foo
name1: Foo
name: Foo
_name: Foo
a_name: Foo
_name1: Foo
input_file: tests/cases/identifiers.nomo
---
VMInstructions {
@ -35,7 +44,7 @@ VMInstructions {
content: "\n" (26..27),
},
LoadFromContextToSlot {
name: "1name" (31..36),
name: "name" (31..35),
slot: VariableSlot {
index: 2,
},
@ -46,10 +55,10 @@ VMInstructions {
},
},
AppendContent {
content: "\n" (39..40),
content: "\n" (38..39),
},
LoadFromContextToSlot {
name: "_name1" (44..50),
name: "_name1" (43..49),
slot: VariableSlot {
index: 3,
},
@ -60,10 +69,10 @@ VMInstructions {
},
},
AppendContent {
content: "\n" (53..54),
content: "\n" (52..53),
},
LoadFromContextToSlot {
name: "_namE" (58..63),
name: "_namE" (57..62),
slot: VariableSlot {
index: 4,
},
@ -74,10 +83,10 @@ VMInstructions {
},
},
AppendContent {
content: "\n" (66..67),
content: "\n" (65..66),
},
LoadFromContextToSlot {
name: "name1" (71..76),
name: "name1" (70..75),
slot: VariableSlot {
index: 5,
},

View file

@ -1,7 +1,7 @@
{
"_name": "Foo",
"a_name": "Foo",
"1name": "Foo",
"name": "Foo",
"_name1": "Foo",
"_namE": "Foo",
"name1": "Foo"
@ -9,7 +9,7 @@
---
{{= _name }}
{{= a_name }}
{{= 1name }}
{{= name }}
{{= _name1 }}
{{= _namE }}
{{= name1 }}