From 05c095ccfe14ffd0a2e8408d5bdd0e17575aed65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 12 Mar 2026 11:31:08 +0100 Subject: [PATCH 1/5] Add using literal loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- src/emit/mod.rs | 15 ++++++- src/eval/mod.rs | 7 +++ tests/cases/1-parsed@literals.snap | 27 ++++++++++++ tests/cases/2-ast@literals.snap | 44 +++++++++++++++++++ tests/cases/3-instructions@literals.snap | 55 ++++++++++++++++++++++++ tests/cases/4-output@literals.snap | 9 ++++ tests/cases/literals.nomo | 6 +++ 7 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 tests/cases/1-parsed@literals.snap create mode 100644 tests/cases/2-ast@literals.snap create mode 100644 tests/cases/3-instructions@literals.snap create mode 100644 tests/cases/4-output@literals.snap create mode 100644 tests/cases/literals.nomo diff --git a/src/emit/mod.rs b/src/emit/mod.rs index afaa4cd..a3c503c 100644 --- a/src/emit/mod.rs +++ b/src/emit/mod.rs @@ -2,6 +2,8 @@ use std::collections::BTreeMap; use crate::ast::TemplateAstExpr; use crate::input::NomoInput; +use crate::parser::TemplateToken; +use crate::value::NomoValue; pub struct EmitMachine { current_index: usize, @@ -88,6 +90,11 @@ pub enum Instruction { value_ident: NomoInput, value_slot: VariableSlot, }, + LoadLiteralToSlot { + source: TemplateToken, + value: NomoValue, + slot: VariableSlot, + }, } #[derive(Debug, Clone)] @@ -421,6 +428,13 @@ fn emit_expr_load( slot: emit_slot, }); } + TemplateAstExpr::Literal { source, value } => { + eval.push(Instruction::LoadLiteralToSlot { + source: source.clone(), + value: value.clone(), + slot: emit_slot, + }); + } TemplateAstExpr::Invalid { .. } => eval.push(Instruction::Abort), TemplateAstExpr::StaticContent { .. } | TemplateAstExpr::Interpolation { .. } => { unreachable!("Invalid AST here") @@ -433,7 +447,6 @@ fn emit_expr_load( TemplateAstExpr::For { .. } => todo!(), TemplateAstExpr::ForElse => todo!(), TemplateAstExpr::Operation { .. } => todo!(), - TemplateAstExpr::Literal { .. } => todo!(), TemplateAstExpr::IfConditional { .. } => todo!(), TemplateAstExpr::ConditionalContent { .. } => todo!(), diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 3059618..5080224 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -179,6 +179,13 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result { + scopes.insert_into_slot(*slot, value.clone()); + } } ip += 1; diff --git a/tests/cases/1-parsed@literals.snap b/tests/cases/1-parsed@literals.snap new file mode 100644 index 0000000..bc0d851 --- /dev/null +++ b/tests/cases/1-parsed@literals.snap @@ -0,0 +1,27 @@ +--- +source: tests/file_tests.rs +expression: parsed +info: + input: "{{ if true }}\n Hello World!\n{{ end }}" + context: {} +input_file: tests/cases/literals.nomo +--- +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]"\n " (13..18), + [Content]"Hello World!" (18..30), + [Whitespace]"\n" (30..31), + [LeftDelim]"{{" (31..33), + [Whitespace]" " (33..34), + [End]"end" (34..37), + [Whitespace]" " (37..38), + [RightDelim]"}}" (38..40), + ], +} diff --git a/tests/cases/2-ast@literals.snap b/tests/cases/2-ast@literals.snap new file mode 100644 index 0000000..85453dc --- /dev/null +++ b/tests/cases/2-ast@literals.snap @@ -0,0 +1,44 @@ +--- +source: tests/file_tests.rs +expression: ast +info: + input: "{{ if true }}\n Hello World!\n{{ end }}" + context: {} +input_file: tests/cases/literals.nomo +--- +TemplateAst { + root: [ + ConditionalChain { + chain: [ + Block { + prev_whitespace_content: None, + expression: IfConditional { + expression: Literal { + source: [Literal(Bool(true))]"true" (6..10), + value: Bool { + value: true, + }, + }, + }, + post_whitespace_content: Some( + [Whitespace]"\n " (13..18), + ), + }, + ConditionalContent { + content: [ + StaticContent( + [Content]"Hello World!" (18..30), + ), + ], + }, + Block { + prev_whitespace_content: Some( + [Whitespace]"\n" (30..31), + ), + expression: EndBlock, + post_whitespace_content: None, + }, + ], + }, + ], +} diff --git a/tests/cases/3-instructions@literals.snap b/tests/cases/3-instructions@literals.snap new file mode 100644 index 0000000..7f68e85 --- /dev/null +++ b/tests/cases/3-instructions@literals.snap @@ -0,0 +1,55 @@ +--- +source: tests/file_tests.rs +expression: emit +info: + input: "{{ if true }}\n Hello World!\n{{ end }}" + context: {} +input_file: tests/cases/literals.nomo +--- +VMInstructions { + labels: { + LabelSlot { + index: 0, + }: 7, + LabelSlot { + index: 2, + }: 7, + }, + instructions: [ + LoadLiteralToSlot { + source: [Literal(Bool(true))]"true" (6..10), + value: Bool { + value: true, + }, + slot: VariableSlot { + index: 1, + }, + }, + JumpIfNotTrue { + emit_slot: VariableSlot { + index: 1, + }, + jump: LabelSlot { + index: 2, + }, + }, + AppendContent { + content: "\n " (13..18), + }, + AppendContent { + content: "Hello World!" (18..30), + }, + AppendContent { + content: "\n" (30..31), + }, + Jump { + jump: LabelSlot { + index: 0, + }, + }, + AppendContent { + content: "\n " (13..18), + }, + NoOp, + ], +} diff --git a/tests/cases/4-output@literals.snap b/tests/cases/4-output@literals.snap new file mode 100644 index 0000000..3a38d32 --- /dev/null +++ b/tests/cases/4-output@literals.snap @@ -0,0 +1,9 @@ +--- +source: tests/file_tests.rs +expression: output +info: + input: "{{ if true }}\n Hello World!\n{{ end }}" + context: {} +input_file: tests/cases/literals.nomo +--- +"\n Hello World!\n" diff --git a/tests/cases/literals.nomo b/tests/cases/literals.nomo new file mode 100644 index 0000000..2318ca1 --- /dev/null +++ b/tests/cases/literals.nomo @@ -0,0 +1,6 @@ +{ +} +--- +{{ if true }} + Hello World! +{{ end }} \ No newline at end of file From d222573a3a32c44354eb696d96cd276de236d5bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 12 Mar 2026 17:29:45 +0100 Subject: [PATCH 2/5] Add evaluating of simple maths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- src/emit/mod.rs | 23 +++- src/eval/mod.rs | 25 +++- src/value.rs | 141 +++++++++++++++++++ tests/cases/1-parsed@maths.snap | 56 ++++++++ tests/cases/2-ast@maths.snap | 100 ++++++++++++++ tests/cases/3-instructions@maths.snap | 187 ++++++++++++++++++++++++++ tests/cases/4-output@maths.snap | 9 ++ tests/cases/maths.nomo | 6 + 8 files changed, 543 insertions(+), 4 deletions(-) create mode 100644 tests/cases/1-parsed@maths.snap create mode 100644 tests/cases/2-ast@maths.snap create mode 100644 tests/cases/3-instructions@maths.snap create mode 100644 tests/cases/4-output@maths.snap create mode 100644 tests/cases/maths.nomo diff --git a/src/emit/mod.rs b/src/emit/mod.rs index a3c503c..aa3e6c0 100644 --- a/src/emit/mod.rs +++ b/src/emit/mod.rs @@ -3,6 +3,7 @@ use std::collections::BTreeMap; use crate::ast::TemplateAstExpr; use crate::input::NomoInput; use crate::parser::TemplateToken; +use crate::parser::TokenOperator; use crate::value::NomoValue; pub struct EmitMachine { @@ -95,6 +96,12 @@ pub enum Instruction { value: NomoValue, slot: VariableSlot, }, + MathOperate { + op: TokenOperator, + left_slot: VariableSlot, + right_slot: VariableSlot, + result_slot: VariableSlot, + }, } #[derive(Debug, Clone)] @@ -416,7 +423,7 @@ fn emit_ast_expr( } fn emit_expr_load( - _machine: &mut EmitMachine, + machine: &mut EmitMachine, eval: &mut Vec, emit_slot: VariableSlot, expression: &TemplateAstExpr<'_>, @@ -435,6 +442,19 @@ fn emit_expr_load( slot: emit_slot, }); } + TemplateAstExpr::Operation { op, lhs, rhs } => { + let left_slot = machine.reserve_slot(); + emit_expr_load(machine, eval, left_slot, lhs); + let right_slot = machine.reserve_slot(); + emit_expr_load(machine, eval, right_slot, rhs); + + eval.push(Instruction::MathOperate { + op: *op, + left_slot, + right_slot, + result_slot: emit_slot, + }); + } TemplateAstExpr::Invalid { .. } => eval.push(Instruction::Abort), TemplateAstExpr::StaticContent { .. } | TemplateAstExpr::Interpolation { .. } => { unreachable!("Invalid AST here") @@ -446,7 +466,6 @@ fn emit_expr_load( TemplateAstExpr::ForChain { .. } => todo!(), TemplateAstExpr::For { .. } => todo!(), TemplateAstExpr::ForElse => todo!(), - TemplateAstExpr::Operation { .. } => todo!(), TemplateAstExpr::IfConditional { .. } => todo!(), TemplateAstExpr::ConditionalContent { .. } => todo!(), diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 5080224..590edd8 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -96,8 +96,9 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result { - let value = scopes.get(slot).as_str().unwrap(); - output.push_str(value); + let value = scopes.get(slot).try_to_string().unwrap(); + + output.push_str(&value); } Instruction::PushScope { inherit_parent: _ } => { scopes.push_scope(); @@ -186,6 +187,26 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result { scopes.insert_into_slot(*slot, value.clone()); } + Instruction::MathOperate { + op, + left_slot, + right_slot, + result_slot, + } => { + let left_value = scopes.get(left_slot); + let right_value = scopes.get(right_slot); + + let result = match op { + 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), + }; + + scopes.insert_into_slot(*result_slot, result.unwrap()); + } } ip += 1; diff --git a/src/value.rs b/src/value.rs index 1dc86e0..7aea85a 100644 --- a/src/value.rs +++ b/src/value.rs @@ -97,6 +97,147 @@ impl NomoValue { None } } + + pub(crate) fn try_add(&self, right_value: &NomoValue) -> Option { + match (self, right_value) { + (NomoValue::Integer { value: lval }, NomoValue::Integer { value: rval }) => { + Some(NomoValue::Integer { + value: *lval + *rval, + }) + } + ( + NomoValue::SignedInteger { value: lval }, + NomoValue::SignedInteger { value: rval }, + ) => Some(NomoValue::SignedInteger { + value: *lval + *rval, + }), + (NomoValue::Integer { value: val }, NomoValue::SignedInteger { value: sval }) + | (NomoValue::SignedInteger { value: sval }, NomoValue::Integer { value: val }) => { + Some(NomoValue::SignedInteger { + value: (i64::try_from(*val).ok()?) + *sval, + }) + } + (NomoValue::String { value: rstr }, NomoValue::String { value: lstr }) => { + Some(NomoValue::String { + value: Cow::Owned(format!("{rstr}{lstr}")), + }) + } + _ => None, + } + } + + pub(crate) fn try_sub(&self, right_value: &NomoValue) -> Option { + macro_rules! op { + ($left:expr, $right:expr) => {{ ($left) - ($right) }}; + } + match (self, right_value) { + (NomoValue::Integer { value: lval }, NomoValue::Integer { value: rval }) => { + Some(NomoValue::Integer { + value: op!(*lval, *rval), + }) + } + ( + NomoValue::SignedInteger { value: lval }, + NomoValue::SignedInteger { value: rval }, + ) => Some(NomoValue::SignedInteger { + value: op!(*lval, *rval), + }), + (NomoValue::Integer { value: val }, NomoValue::SignedInteger { value: sval }) + | (NomoValue::SignedInteger { value: sval }, NomoValue::Integer { value: val }) => { + Some(NomoValue::SignedInteger { + value: op!(i64::try_from(*val).ok()?, *sval), + }) + } + _ => None, + } + } + + pub(crate) fn try_mul(&self, right_value: &NomoValue) -> Option { + macro_rules! op { + ($left:expr, $right:expr) => {{ ($left) * ($right) }}; + } + match (self, right_value) { + (NomoValue::Integer { value: lval }, NomoValue::Integer { value: rval }) => { + Some(NomoValue::Integer { + value: op!(*lval, *rval), + }) + } + ( + NomoValue::SignedInteger { value: lval }, + NomoValue::SignedInteger { value: rval }, + ) => Some(NomoValue::SignedInteger { + value: op!(*lval, *rval), + }), + (NomoValue::Integer { value: val }, NomoValue::SignedInteger { value: sval }) + | (NomoValue::SignedInteger { value: sval }, NomoValue::Integer { value: val }) => { + Some(NomoValue::SignedInteger { + value: op!(i64::try_from(*val).ok()?, *sval), + }) + } + _ => None, + } + } + + pub(crate) fn try_div(&self, right_value: &NomoValue) -> Option { + macro_rules! op { + ($left:expr, $right:expr) => {{ ($left) / ($right) }}; + } + match (self, right_value) { + (NomoValue::Integer { value: lval }, NomoValue::Integer { value: rval }) => { + Some(NomoValue::Integer { + value: op!(*lval, *rval), + }) + } + ( + NomoValue::SignedInteger { value: lval }, + NomoValue::SignedInteger { value: rval }, + ) => Some(NomoValue::SignedInteger { + value: op!(*lval, *rval), + }), + (NomoValue::Integer { value: val }, NomoValue::SignedInteger { value: sval }) + | (NomoValue::SignedInteger { value: sval }, NomoValue::Integer { value: val }) => { + Some(NomoValue::SignedInteger { + value: op!(i64::try_from(*val).ok()?, *sval), + }) + } + _ => None, + } + } + + pub(crate) fn try_and(&self, right_value: &NomoValue) -> Option { + match (self, right_value) { + (NomoValue::Bool { value: lval }, NomoValue::Bool { value: rval }) => { + Some(NomoValue::Bool { + value: *lval && *rval, + }) + } + _ => None, + } + } + + pub(crate) fn try_or(&self, right_value: &NomoValue) -> Option { + match (self, right_value) { + (NomoValue::Bool { value: lval }, NomoValue::Bool { value: rval }) => { + Some(NomoValue::Bool { + value: *lval || *rval, + }) + } + _ => None, + } + } + + pub(crate) fn try_to_string(&self) -> Option { + match self { + NomoValue::String { value } => Some(value.to_string()), + NomoValue::Array { .. } => None, + NomoValue::Bool { value } => Some(value.to_string()), + NomoValue::Object { .. } => None, + NomoValue::Integer { value } => Some(value.to_string()), + NomoValue::SignedInteger { value } => Some(value.to_string()), + NomoValue::Float { value } => Some(value.to_string()), + NomoValue::Iterator { .. } => None, + } + } } pub trait CloneIterator: Iterator { diff --git a/tests/cases/1-parsed@maths.snap b/tests/cases/1-parsed@maths.snap new file mode 100644 index 0000000..837a801 --- /dev/null +++ b/tests/cases/1-parsed@maths.snap @@ -0,0 +1,56 @@ +--- +source: tests/file_tests.rs +expression: parsed +info: + input: "{{= 5 * 3 }}\n{{= 2 * 3 + 4 * 3 }}\n{{= 3 / 3 + 3 }}" + context: {} +input_file: tests/cases/maths.nomo +--- +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(3))]"3" (8..9), + [Whitespace]" " (9..10), + [RightDelim]"}}" (10..12), + [Whitespace]"\n" (12..13), + [LeftDelim]"{{" (13..15), + [WantsOutput]"=" (15..16), + [Whitespace]" " (16..17), + [Literal(Integer(2))]"2" (17..18), + [Whitespace]" " (18..19), + [Operator(Times)]"*" (19..20), + [Whitespace]" " (20..21), + [Literal(Integer(3))]"3" (21..22), + [Whitespace]" " (22..23), + [Operator(Plus)]"+" (23..24), + [Whitespace]" " (24..25), + [Literal(Integer(4))]"4" (25..26), + [Whitespace]" " (26..27), + [Operator(Times)]"*" (27..28), + [Whitespace]" " (28..29), + [Literal(Integer(3))]"3" (29..30), + [Whitespace]" " (30..31), + [RightDelim]"}}" (31..33), + [Whitespace]"\n" (33..34), + [LeftDelim]"{{" (34..36), + [WantsOutput]"=" (36..37), + [Whitespace]" " (37..38), + [Literal(Integer(3))]"3" (38..39), + [Whitespace]" " (39..40), + [Operator(Divide)]"/" (40..41), + [Whitespace]" " (41..42), + [Literal(Integer(3))]"3" (42..43), + [Whitespace]" " (43..44), + [Operator(Plus)]"+" (44..45), + [Whitespace]" " (45..46), + [Literal(Integer(3))]"3" (46..47), + [Whitespace]" " (47..48), + [RightDelim]"}}" (48..50), + ], +} diff --git a/tests/cases/2-ast@maths.snap b/tests/cases/2-ast@maths.snap new file mode 100644 index 0000000..6aa9130 --- /dev/null +++ b/tests/cases/2-ast@maths.snap @@ -0,0 +1,100 @@ +--- +source: tests/file_tests.rs +expression: ast +info: + input: "{{= 5 * 3 }}\n{{= 2 * 3 + 4 * 3 }}\n{{= 3 / 3 + 3 }}" + context: {} +input_file: tests/cases/maths.nomo +--- +TemplateAst { + root: [ + Interpolation { + prev_whitespace_content: None, + expression: 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, + }, + }, + }, + post_whitespace_content: Some( + [Whitespace]"\n" (12..13), + ), + }, + Interpolation { + prev_whitespace_content: None, + expression: Operation { + op: Plus, + lhs: Operation { + op: Times, + lhs: Literal { + source: [Literal(Integer(2))]"2" (17..18), + value: Integer { + value: 2, + }, + }, + rhs: Literal { + source: [Literal(Integer(3))]"3" (21..22), + value: Integer { + value: 3, + }, + }, + }, + rhs: Operation { + op: Times, + lhs: Literal { + source: [Literal(Integer(4))]"4" (25..26), + value: Integer { + value: 4, + }, + }, + rhs: Literal { + source: [Literal(Integer(3))]"3" (29..30), + value: Integer { + value: 3, + }, + }, + }, + }, + post_whitespace_content: Some( + [Whitespace]"\n" (33..34), + ), + }, + Interpolation { + prev_whitespace_content: None, + expression: Operation { + op: Plus, + lhs: Operation { + op: Divide, + lhs: Literal { + source: [Literal(Integer(3))]"3" (38..39), + value: Integer { + value: 3, + }, + }, + rhs: Literal { + source: [Literal(Integer(3))]"3" (42..43), + value: Integer { + value: 3, + }, + }, + }, + rhs: Literal { + source: [Literal(Integer(3))]"3" (46..47), + value: Integer { + value: 3, + }, + }, + }, + post_whitespace_content: None, + }, + ], +} diff --git a/tests/cases/3-instructions@maths.snap b/tests/cases/3-instructions@maths.snap new file mode 100644 index 0000000..d6a4920 --- /dev/null +++ b/tests/cases/3-instructions@maths.snap @@ -0,0 +1,187 @@ +--- +source: tests/file_tests.rs +expression: emit +info: + input: "{{= 5 * 3 }}\n{{= 2 * 3 + 4 * 3 }}\n{{= 3 / 3 + 3 }}" + context: {} +input_file: tests/cases/maths.nomo +--- +VMInstructions { + labels: {}, + instructions: [ + LoadLiteralToSlot { + source: [Literal(Integer(5))]"5" (4..5), + value: Integer { + value: 5, + }, + slot: VariableSlot { + index: 1, + }, + }, + LoadLiteralToSlot { + source: [Literal(Integer(3))]"3" (8..9), + value: Integer { + value: 3, + }, + slot: VariableSlot { + index: 2, + }, + }, + MathOperate { + op: Times, + left_slot: VariableSlot { + index: 1, + }, + right_slot: VariableSlot { + index: 2, + }, + result_slot: VariableSlot { + index: 0, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 0, + }, + }, + AppendContent { + content: "\n" (12..13), + }, + LoadLiteralToSlot { + source: [Literal(Integer(2))]"2" (17..18), + value: Integer { + value: 2, + }, + slot: VariableSlot { + index: 5, + }, + }, + LoadLiteralToSlot { + source: [Literal(Integer(3))]"3" (21..22), + value: Integer { + value: 3, + }, + slot: VariableSlot { + index: 6, + }, + }, + MathOperate { + op: Times, + left_slot: VariableSlot { + index: 5, + }, + right_slot: VariableSlot { + index: 6, + }, + result_slot: VariableSlot { + index: 4, + }, + }, + LoadLiteralToSlot { + source: [Literal(Integer(4))]"4" (25..26), + value: Integer { + value: 4, + }, + slot: VariableSlot { + index: 8, + }, + }, + LoadLiteralToSlot { + source: [Literal(Integer(3))]"3" (29..30), + value: Integer { + value: 3, + }, + slot: VariableSlot { + index: 9, + }, + }, + MathOperate { + op: Times, + left_slot: VariableSlot { + index: 8, + }, + right_slot: VariableSlot { + index: 9, + }, + result_slot: VariableSlot { + index: 7, + }, + }, + MathOperate { + op: Plus, + left_slot: VariableSlot { + index: 4, + }, + right_slot: VariableSlot { + index: 7, + }, + result_slot: VariableSlot { + index: 3, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 3, + }, + }, + AppendContent { + content: "\n" (33..34), + }, + LoadLiteralToSlot { + source: [Literal(Integer(3))]"3" (38..39), + value: Integer { + value: 3, + }, + slot: VariableSlot { + index: 12, + }, + }, + LoadLiteralToSlot { + source: [Literal(Integer(3))]"3" (42..43), + value: Integer { + value: 3, + }, + slot: VariableSlot { + index: 13, + }, + }, + MathOperate { + op: Divide, + left_slot: VariableSlot { + index: 12, + }, + right_slot: VariableSlot { + index: 13, + }, + result_slot: VariableSlot { + index: 11, + }, + }, + LoadLiteralToSlot { + source: [Literal(Integer(3))]"3" (46..47), + value: Integer { + value: 3, + }, + slot: VariableSlot { + index: 14, + }, + }, + MathOperate { + op: Plus, + left_slot: VariableSlot { + index: 11, + }, + right_slot: VariableSlot { + index: 14, + }, + result_slot: VariableSlot { + index: 10, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 10, + }, + }, + ], +} diff --git a/tests/cases/4-output@maths.snap b/tests/cases/4-output@maths.snap new file mode 100644 index 0000000..ab89da7 --- /dev/null +++ b/tests/cases/4-output@maths.snap @@ -0,0 +1,9 @@ +--- +source: tests/file_tests.rs +expression: output +info: + input: "{{= 5 * 3 }}\n{{= 2 * 3 + 4 * 3 }}\n{{= 3 / 3 + 3 }}" + context: {} +input_file: tests/cases/maths.nomo +--- +"15\n18\n4" diff --git a/tests/cases/maths.nomo b/tests/cases/maths.nomo new file mode 100644 index 0000000..4ba36c3 --- /dev/null +++ b/tests/cases/maths.nomo @@ -0,0 +1,6 @@ +{ +} +--- +{{= 5 * 3 }} +{{= 2 * 3 + 4 * 3 }} +{{= 3 / 3 + 3 }} \ No newline at end of file From 437584c84430527a93dc2ecb801b3b6a82a7c674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 12 Mar 2026 17:36:36 +0100 Subject: [PATCH 3/5] Add parsing of more logical combinators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- src/ast/mod.rs | 1 + src/eval/mod.rs | 1 + src/parser/mod.rs | 58 ++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1d483a7..4351042 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -663,6 +663,7 @@ fn parse_expression<'input>( 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) })), + _ => winnow::combinator::todo }), ) .parse_next(input) diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 590edd8..8d32123 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -203,6 +203,7 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result 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()); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a5474c4..af65cf7 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -230,6 +230,12 @@ pub enum TokenOperator { Divide, And, Or, + Equal, + NotEqual, + Greater, + GreaterOrEqual, + Lesser, + LesserOrEqual, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -522,6 +528,22 @@ fn parse_operator<'input>(input: &mut Input<'input>) -> PResult<'input, Template "|".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, }, ) @@ -747,7 +769,7 @@ mod tests { #[test] fn parse_operations() { - let input = "{{= 5 * 14 + 3 / 2 - 1 }}{{ if foo && bar || baz }}{{ end }}"; + 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#" @@ -790,12 +812,36 @@ mod tests { [Whitespace]" " (44..45), [Ident]"baz" (45..48), [Whitespace]" " (48..49), - [RightDelim]"}}" (49..51), - [LeftDelim]"{{" (51..53), + [Operator(And)]"&&" (49..51), + [Whitespace]" " (51..52), + [Literal(Integer(2))]"2" (52..53), [Whitespace]" " (53..54), - [End]"end" (54..57), - [Whitespace]" " (57..58), - [RightDelim]"}}" (58..60), + [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), ], }, ) From 722e61cc85c3032df8c70db0d58f76db1b6388d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 12 Mar 2026 17:53:11 +0100 Subject: [PATCH 4/5] Add operator precedence and refactor expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- src/ast/mod.rs | 45 ++++++++++--- ..._ast__tests__check_logical_expression.snap | 63 +++++++++++++++++++ src/parser/mod.rs | 1 - 3 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 src/ast/snapshots/nomo__ast__tests__check_logical_expression.snap diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4351042..1b25884 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -654,17 +654,33 @@ fn parse_operator<'input>(input: &mut Input<'input>) -> Result( input: &mut Input<'input>, ) -> Result, AstError> { + macro_rules! infix { + ($parser:expr => [ $($side:ident $val:tt => $prec:expr),* $(,)? ]) => { + dispatch! { surrounded(ws, parse_operator); + $( + TokenOperator::$val => Left($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(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) })), - _ => winnow::combinator::todo - }), + 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) } @@ -981,4 +997,15 @@ mod tests { 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/ast/snapshots/nomo__ast__tests__check_logical_expression.snap b/src/ast/snapshots/nomo__ast__tests__check_logical_expression.snap new file mode 100644 index 0000000..e9caca2 --- /dev/null +++ b/src/ast/snapshots/nomo__ast__tests__check_logical_expression.snap @@ -0,0 +1,63 @@ +--- +source: src/ast/mod.rs +expression: ast +--- +TemplateAst { + root: [ + Interpolation { + prev_whitespace_content: None, + expression: Operation { + op: Or, + lhs: Operation { + op: And, + lhs: Literal { + source: [Literal(Bool(true))]"true" (4..8), + value: Bool { + value: true, + }, + }, + rhs: Literal { + source: [Literal(Bool(false))]"false" (12..17), + value: Bool { + value: false, + }, + }, + }, + rhs: Operation { + op: And, + lhs: Operation { + op: GreaterOrEqual, + lhs: Literal { + source: [Literal(Integer(3))]"3" (21..22), + value: Integer { + value: 3, + }, + }, + rhs: Literal { + source: [Literal(Integer(2))]"2" (26..27), + value: Integer { + value: 2, + }, + }, + }, + rhs: Operation { + op: Equal, + lhs: Literal { + source: [Literal(Integer(5))]"5" (31..32), + value: Integer { + value: 5, + }, + }, + rhs: Literal { + source: [Literal(Integer(2))]"2" (36..37), + value: Integer { + value: 2, + }, + }, + }, + }, + }, + post_whitespace_content: None, + }, + ], +} diff --git a/src/parser/mod.rs b/src/parser/mod.rs index af65cf7..37e2757 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -33,7 +33,6 @@ 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; From 605798674f82020263a317f60965d8b963082804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Thu, 12 Mar 2026 18:00:45 +0100 Subject: [PATCH 5/5] Make usage of the $side metavariable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- src/ast/mod.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 1b25884..55190b0 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -655,32 +655,32 @@ fn parse_expression<'input>( input: &mut Input<'input>, ) -> Result, AstError> { macro_rules! infix { - ($parser:expr => [ $($side:ident $val:tt => $prec:expr),* $(,)? ]) => { + ($parser:expr => [ $($side:tt $val:tt => $prec:expr),* $(,)? ]) => { dispatch! { surrounded(ws, parse_operator); $( - TokenOperator::$val => Left($prec, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::$val, lhs: Box::new(lhs), rhs: Box::new(rhs) })) + 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, - ] }, - ), + 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) }