From 018ba3cd2c7bcaf7ca3080307927f28cd25e293b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 11 Mar 2026 10:34:17 +0100 Subject: [PATCH 1/7] Add for loop parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- src/parser/mod.rs | 85 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a9171f0..de0ba97 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -209,6 +209,8 @@ pub enum TokenKind { Invalid, ConditionalIf, ConditionalElse, + For, + In, End, Literal(TokenLiteral), } @@ -295,6 +297,8 @@ impl TemplateToken { invalid => TokenKind::Invalid, conditional_if => TokenKind::ConditionalIf, conditional_else => TokenKind::ConditionalElse, + keyword_for => TokenKind::For, + keyword_in => TokenKind::In, end => TokenKind::End, } @@ -381,14 +385,7 @@ fn parse_interpolate<'input>(input: &mut Input<'input>) -> PResult<'input, Vec(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { trace( "parse_block_token", - alt(( - parse_ident, - terminated(parse_literal, ident_terminator_check), - terminated(parse_condition_if, ident_terminator_check), - terminated(parse_condition_else, ident_terminator_check), - terminated(parse_end, ident_terminator_check), - parse_whitespace, - )), + alt((parse_ident, parse_keyword, parse_whitespace)), ) .parse_next(input) } @@ -431,6 +428,26 @@ 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", @@ -458,14 +475,9 @@ fn parse_ident<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateTok } fn ident<'input>(input: &mut Input<'input>) -> PResult<'input, NomoInput> { - peek(not(alt(( - parse_literal, - parse_condition_if, - parse_condition_else, - parse_end, - )))) - .context(ParseError::ctx().msg("Expected an ident, but found a literal instead")) - .parse_next(input)?; + peek(not(parse_keyword)) + .context(ParseError::ctx().msg("Expected an ident, but found a literal instead")) + .parse_next(input)?; take_while(1.., |c: char| c.is_alphanumeric() || "_".contains(c)).parse_next(input) } @@ -627,4 +639,45 @@ mod tests { ) "#); } + + #[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), + ], + }, + ) + "#); + } } From a099c74b1bb728fbe7253a461cb6be0217bc3d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 11 Mar 2026 10:36:40 +0100 Subject: [PATCH 2/7] Re-arrange TemplateAstExpr 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 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 04a7e5d..5be6458 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -217,15 +217,20 @@ pub fn parse(input: &[TemplateToken]) -> Result, AstFailure> { #[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, }, - VariableAccess(TemplateToken), ConditionalChain { chain: Vec>, }, + VariableAccess(TemplateToken), IfConditional { expression: Box>, }, @@ -237,11 +242,6 @@ pub enum TemplateAstExpr<'input> { }, Invalid(&'input [TemplateToken]), EndBlock, - Block { - prev_whitespace_content: Option, - expression: Box>, - post_whitespace_content: Option, - }, } fn parse_asts<'input>(input: &mut Input<'input>) -> Result>, AstError> { From e64256b65fffe8c78c76b23198a20bba6a25d7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 11 Mar 2026 14:00:45 +0100 Subject: [PATCH 3/7] Add ast parsing for for loops 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 | 97 +++++++++++++++++++ .../nomo__ast__tests__check_for_loop.snap | 61 ++++++++++++ src/emit/mod.rs | 7 ++ 3 files changed, 165 insertions(+) create mode 100644 src/ast/snapshots/nomo__ast__tests__check_for_loop.snap diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 5be6458..fa78ccc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -230,6 +230,18 @@ pub enum TemplateAstExpr<'input> { 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>, @@ -316,6 +328,7 @@ fn parse_action<'input>(input: &mut Input<'input>) -> Result(input: &mut Input<'input>) -> Result(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_value_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> { @@ -447,6 +521,18 @@ fn parse_conditional_else<'input>( .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", @@ -815,4 +901,15 @@ mod tests { 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); + } } diff --git a/src/ast/snapshots/nomo__ast__tests__check_for_loop.snap b/src/ast/snapshots/nomo__ast__tests__check_for_loop.snap new file mode 100644 index 0000000..737e2c0 --- /dev/null +++ b/src/ast/snapshots/nomo__ast__tests__check_for_loop.snap @@ -0,0 +1,61 @@ +--- +source: src/ast/mod.rs +expression: ast +--- +TemplateAst { + root: [ + ForChain { + for_block: Block { + prev_whitespace_content: None, + expression: For { + value_ident: [Ident]"value" (7..12), + value_expression: VariableAccess( + [Ident]"array" (16..21), + ), + }, + post_whitespace_content: Some( + [Whitespace]" " (24..25), + ), + }, + content: [ + StaticContent( + [Content]"Hi:" (25..28), + ), + Interpolation { + prev_whitespace_content: Some( + [Whitespace]" " (28..29), + ), + expression: VariableAccess( + [Ident]"value" (33..38), + ), + post_whitespace_content: Some( + [Whitespace]" " (41..42), + ), + }, + ], + else_block: Some( + Block { + prev_whitespace_content: None, + expression: ForElse, + post_whitespace_content: Some( + [Whitespace]" " (52..53), + ), + }, + ), + else_content: Some( + [ + StaticContent( + [Content]"No Content :C" (53..66), + ), + ], + ), + end_block: Block { + prev_whitespace_content: Some( + [Whitespace]" " (66..67), + ), + expression: EndBlock, + post_whitespace_content: None, + }, + }, + ], +} diff --git a/src/emit/mod.rs b/src/emit/mod.rs index daf8e0d..04930c6 100644 --- a/src/emit/mod.rs +++ b/src/emit/mod.rs @@ -205,6 +205,9 @@ fn emit_ast_expr( | TemplateAstExpr::IfConditional { .. } | TemplateAstExpr::ConditionalContent { .. } | TemplateAstExpr::ElseConditional { .. } + | TemplateAstExpr::ForChain { .. } + | TemplateAstExpr::For { .. } + | TemplateAstExpr::ForElse | TemplateAstExpr::Invalid { .. } | TemplateAstExpr::VariableAccess { .. } => eval.push(Instruction::Abort), } @@ -231,6 +234,10 @@ fn emit_expr_load( TemplateAstExpr::ElseConditional { .. } => todo!(), TemplateAstExpr::EndBlock => todo!(), TemplateAstExpr::Block { .. } => todo!(), + TemplateAstExpr::ForChain { .. } => todo!(), + TemplateAstExpr::For { .. } => todo!(), + TemplateAstExpr::ForElse => todo!(), + TemplateAstExpr::IfConditional { .. } => todo!(), TemplateAstExpr::ConditionalContent { .. } => todo!(), } From 71820243428c129e063d6ac4ddf3be3cbdf46e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 11 Mar 2026 15:50:05 +0100 Subject: [PATCH 4/7] Introduce JumpLabels instead of manually correct jump positions 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 | 123 ++++++++------ .../nomo__emit__tests__check_if_else_if.snap | 157 ++++++++++-------- src/eval/mod.rs | 36 ++-- src/lib.rs | 3 +- tests/cases/3-instructions@condition.snap | 90 +++++----- tests/cases/3-instructions@identifiers.snap | 145 ++++++++-------- tests/cases/3-instructions@if_else_if.snap | 125 ++++++++------ tests/cases/3-instructions@interpolation.snap | 37 +++-- tests/cases/3-instructions@multiple.snap | 61 +++---- tests/cases/3-instructions@simple.snap | 13 +- .../cases/3-instructions@trim_whitespace.snap | 74 +++++---- tests/cases/4-output@if_else_if.snap | 2 +- 12 files changed, 485 insertions(+), 381 deletions(-) diff --git a/src/emit/mod.rs b/src/emit/mod.rs index 04930c6..5ce4ac7 100644 --- a/src/emit/mod.rs +++ b/src/emit/mod.rs @@ -1,8 +1,11 @@ +use std::collections::HashMap; + use crate::ast::TemplateAstExpr; use crate::input::NomoInput; pub struct EmitMachine { current_index: usize, + labels: HashMap, } impl EmitMachine { @@ -15,6 +18,22 @@ impl EmitMachine { }, } } + + fn reserve_label(&mut self) -> LabelSlot { + LabelSlot { + index: { + let val = self.current_index; + self.current_index += 1; + val + }, + } + } + + fn assign_label(&mut self, slot: LabelSlot, idx: usize) { + let no_prev = self.labels.insert(slot, idx).is_none(); + + assert!(no_prev, "A label slot was already assigned") + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -22,7 +41,12 @@ pub struct VariableSlot { index: usize, } -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct LabelSlot { + index: usize, +} + +#[derive(Debug, Clone)] pub enum Instruction { AppendContent { content: NomoInput, @@ -40,24 +64,36 @@ pub enum Instruction { Abort, JumpIfNotTrue { emit_slot: VariableSlot, - jump: isize, + jump: LabelSlot, }, Jump { - jump: isize, + jump: LabelSlot, }, NoOp, } -pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> Vec { +#[derive(Debug, Clone)] +pub struct VMInstructions { + pub labels: HashMap, + pub instructions: Vec, +} + +pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> VMInstructions { let mut eval = vec![]; - let mut machine = EmitMachine { current_index: 0 }; + let mut machine = EmitMachine { + current_index: 0, + labels: HashMap::new(), + }; for ast in input.root() { emit_ast_expr(&mut machine, &mut eval, ast); } - eval + VMInstructions { + labels: machine.labels, + instructions: eval, + } } fn emit_ast_expr( @@ -95,10 +131,11 @@ fn emit_ast_expr( TemplateAstExpr::ConditionalChain { chain } => { let mut chain = chain.iter(); + let end_label = machine.reserve_label(); let mut end_indices = vec![]; let mut previous_post_whitespace_content: &Option = &None; - let mut previous_jump: Option = None; + let mut previous_jump: Option = None; loop { let next = chain.next().unwrap(); @@ -114,7 +151,7 @@ fn emit_ast_expr( } end_indices.push(eval.len()); - eval.push(Instruction::Jump { jump: isize::MAX }); + eval.push(Instruction::Jump { jump: end_label }); } else if let TemplateAstExpr::Block { prev_whitespace_content, post_whitespace_content, @@ -139,20 +176,15 @@ fn emit_ast_expr( let emit_slot = machine.reserve_slot(); emit_expr_load(machine, eval, emit_slot, expression); - previous_jump = Some(eval.len()); + let jmp_label = machine.reserve_label(); + previous_jump = Some(jmp_label); eval.push(Instruction::JumpIfNotTrue { emit_slot, - jump: isize::MAX, + jump: jmp_label, }); } else if let TemplateAstExpr::ElseConditional { expression } = &**expression { if let Some(previous_jump) = previous_jump.take() { - let new_jump = eval.len() - previous_jump - 1; - let Instruction::JumpIfNotTrue { jump, .. } = &mut eval[previous_jump] - else { - panic!("Jump slot had something that is not a jump?!"); - }; - - *jump = new_jump as isize; + machine.assign_label(previous_jump, eval.len()); } else { panic!("Got an else without a previous if?"); } @@ -161,10 +193,11 @@ fn emit_ast_expr( let emit_slot = machine.reserve_slot(); emit_expr_load(machine, eval, emit_slot, expression); - previous_jump = Some(eval.len()); + let jmp_label = machine.reserve_label(); + previous_jump = Some(jmp_label); eval.push(Instruction::JumpIfNotTrue { emit_slot, - jump: isize::MAX, + jump: jmp_label, }); } else { // We don't have to do anything in the else case @@ -176,13 +209,9 @@ fn emit_ast_expr( } if let Some(previous_jump) = previous_jump.take() { - let new_jump = eval.len() - previous_jump - 1; - let Instruction::JumpIfNotTrue { jump, .. } = &mut eval[previous_jump] else { - panic!("Jump slot had something that is not a jump?!"); - }; - - *jump = new_jump as isize; + machine.assign_label(previous_jump, eval.len()); } + machine.assign_label(end_label, eval.len()); if let Some(ws) = previous_post_whitespace_content { eval.push(Instruction::AppendContent { @@ -191,13 +220,6 @@ fn emit_ast_expr( } else { eval.push(Instruction::NoOp); } - - for index in end_indices { - let jump = eval.len() - index - 1; - eval[index] = Instruction::Jump { - jump: jump as isize, - }; - } } TemplateAstExpr::Block { .. } @@ -258,25 +280,28 @@ mod tests { let emit = emit_machine(ast); insta::assert_debug_snapshot!(emit, @r#" - [ - AppendContent { - content: "Hello" (0..5), - }, - AppendContent { - content: " " (5..6), - }, - LoadFromContextToSlot { - name: "world" (10..15), - slot: VariableSlot { - index: 0, + VMInstructions { + labels: {}, + instructions: [ + AppendContent { + content: "Hello" (0..5), }, - }, - EmitFromSlot { - slot: VariableSlot { - index: 0, + AppendContent { + content: " " (5..6), }, - }, - ] + LoadFromContextToSlot { + name: "world" (10..15), + slot: VariableSlot { + index: 0, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 0, + }, + }, + ], + } "#); } diff --git a/src/emit/snapshots/nomo__emit__tests__check_if_else_if.snap b/src/emit/snapshots/nomo__emit__tests__check_if_else_if.snap index 469aa47..5a6c89f 100644 --- a/src/emit/snapshots/nomo__emit__tests__check_if_else_if.snap +++ b/src/emit/snapshots/nomo__emit__tests__check_if_else_if.snap @@ -2,75 +2,98 @@ source: src/emit/mod.rs expression: emit --- -[ - LoadFromContextToSlot { - name: "foo" (6..9), - slot: VariableSlot { +VMInstructions { + labels: { + LabelSlot { + index: 4, + }: 14, + LabelSlot { index: 0, + }: 19, + LabelSlot { + index: 2, + }: 7, + }, + instructions: [ + LoadFromContextToSlot { + name: "foo" (6..9), + slot: VariableSlot { + index: 1, + }, }, - }, - JumpIfNotTrue { - emit_slot: VariableSlot { - index: 0, + JumpIfNotTrue { + emit_slot: VariableSlot { + index: 1, + }, + jump: LabelSlot { + index: 2, + }, }, - jump: 5, - }, - AppendContent { - content: " " (12..13), - }, - AppendContent { - content: "foo" (13..16), - }, - AppendContent { - content: " " (16..17), - }, - Jump { - jump: 14, - }, - AppendContent { - content: " " (12..13), - }, - LoadFromContextToSlot { - name: "bar" (28..31), - slot: VariableSlot { - index: 1, + AppendContent { + content: " " (12..13), }, - }, - JumpIfNotTrue { - emit_slot: VariableSlot { - index: 1, + AppendContent { + content: "foo" (13..16), }, - jump: 5, - }, - AppendContent { - content: " " (34..35), - }, - AppendContent { - content: "bar" (35..38), - }, - AppendContent { - content: " " (38..39), - }, - Jump { - jump: 7, - }, - AppendContent { - content: " " (34..35), - }, - AppendContent { - content: " " (49..50), - }, - AppendContent { - content: "foobar" (50..56), - }, - AppendContent { - content: " " (56..57), - }, - Jump { - jump: 2, - }, - AppendContent { - content: " " (49..50), - }, - NoOp, -] + AppendContent { + content: " " (16..17), + }, + Jump { + jump: LabelSlot { + index: 0, + }, + }, + AppendContent { + content: " " (12..13), + }, + LoadFromContextToSlot { + name: "bar" (28..31), + slot: VariableSlot { + index: 3, + }, + }, + JumpIfNotTrue { + emit_slot: VariableSlot { + index: 3, + }, + jump: LabelSlot { + index: 4, + }, + }, + AppendContent { + content: " " (34..35), + }, + AppendContent { + content: "bar" (35..38), + }, + AppendContent { + content: " " (38..39), + }, + Jump { + jump: LabelSlot { + index: 0, + }, + }, + AppendContent { + content: " " (34..35), + }, + AppendContent { + content: " " (49..50), + }, + AppendContent { + content: "foobar" (50..56), + }, + AppendContent { + content: " " (56..57), + }, + Jump { + jump: LabelSlot { + index: 0, + }, + }, + AppendContent { + content: " " (49..50), + }, + NoOp, + ], +} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 10250f1..b44ecaa 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -5,6 +5,7 @@ use thiserror::Error; use crate::Context; use crate::emit::Instruction; +use crate::emit::VMInstructions; use crate::input::NomoInput; #[derive(Debug, Error, Display)] @@ -17,24 +18,21 @@ pub enum EvaluationError { ** ** This is an internal error and is a bug that should be reported */ - InstructionPointerOverflow, + LabelNotFound, } -pub fn execute( - instructions: &[Instruction], - global_context: &Context, -) -> Result { +pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result { let mut output = String::new(); let mut scopes: HashMap = HashMap::new(); let mut ip = 0; loop { - if ip >= instructions.len() { + if ip >= vm.instructions.len() { break; } - let instr = instructions.get(ip).unwrap(); + let instr = vm.instructions.get(ip).unwrap(); match instr { Instruction::NoOp => (), @@ -58,25 +56,21 @@ pub fn execute( if dont_jump { // We are done } else { - let (new_ip, overflow) = ip.overflowing_add_signed(*jump); + let Some(new_ip) = vm.labels.get(jump) else { + return Err(EvaluationError::LabelNotFound); + }; - if overflow { - return Err(EvaluationError::InstructionPointerOverflow); - } else { - ip = new_ip; - continue; - } + ip = *new_ip; + continue; } } Instruction::Jump { jump } => { - let (new_ip, overflow) = ip.overflowing_add_signed(*jump); + let Some(new_ip) = vm.labels.get(jump) else { + return Err(EvaluationError::LabelNotFound); + }; - if overflow { - return Err(EvaluationError::InstructionPointerOverflow); - } else { - ip = new_ip; - continue; - } + ip = *new_ip; + continue; } } diff --git a/src/lib.rs b/src/lib.rs index 2679358..53024bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use serde::Serialize; use thiserror::Error; use crate::emit::Instruction; +use crate::emit::VMInstructions; use crate::input::NomoInput; pub mod ast; @@ -84,7 +85,7 @@ impl Nomo { } struct Template { - instructions: Vec, + instructions: VMInstructions, } pub struct Context { diff --git a/tests/cases/3-instructions@condition.snap b/tests/cases/3-instructions@condition.snap index c2901f7..1b96697 100644 --- a/tests/cases/3-instructions@condition.snap +++ b/tests/cases/3-instructions@condition.snap @@ -3,46 +3,60 @@ source: tests/file_tests.rs expression: emit input_file: tests/cases/condition.nomo --- -[ - LoadFromContextToSlot { - name: "test" (6..10), - slot: VariableSlot { +VMInstructions { + labels: { + LabelSlot { + index: 2, + }: 7, + LabelSlot { index: 0, + }: 7, + }, + instructions: [ + LoadFromContextToSlot { + name: "test" (6..10), + slot: VariableSlot { + index: 1, + }, }, - }, - JumpIfNotTrue { - emit_slot: VariableSlot { - index: 0, + JumpIfNotTrue { + emit_slot: VariableSlot { + index: 1, + }, + jump: LabelSlot { + index: 2, + }, }, - jump: 5, - }, - AppendContent { - content: "\n " (13..18), - }, - AppendContent { - content: "Hello World!" (18..30), - }, - AppendContent { - content: "\n" (30..31), - }, - Jump { - jump: 2, - }, - AppendContent { - content: "\n " (13..18), - }, - AppendContent { - content: "\n\n" (40..42), - }, - LoadFromContextToSlot { - name: "stuff" (46..51), - slot: VariableSlot { - index: 1, + AppendContent { + content: "\n " (13..18), }, - }, - EmitFromSlot { - slot: VariableSlot { - index: 1, + AppendContent { + content: "Hello World!" (18..30), }, - }, -] + AppendContent { + content: "\n" (30..31), + }, + Jump { + jump: LabelSlot { + index: 0, + }, + }, + AppendContent { + content: "\n " (13..18), + }, + AppendContent { + content: "\n\n" (40..42), + }, + LoadFromContextToSlot { + name: "stuff" (46..51), + slot: VariableSlot { + index: 3, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 3, + }, + }, + ], +} diff --git a/tests/cases/3-instructions@identifiers.snap b/tests/cases/3-instructions@identifiers.snap index 36bc082..01ec3a8 100644 --- a/tests/cases/3-instructions@identifiers.snap +++ b/tests/cases/3-instructions@identifiers.snap @@ -3,86 +3,89 @@ source: tests/file_tests.rs expression: emit input_file: tests/cases/identifiers.nomo --- -[ - LoadFromContextToSlot { - name: "_name" (4..9), - slot: VariableSlot { - index: 0, +VMInstructions { + labels: {}, + instructions: [ + LoadFromContextToSlot { + name: "_name" (4..9), + slot: VariableSlot { + index: 0, + }, }, - }, - EmitFromSlot { - slot: VariableSlot { - index: 0, + EmitFromSlot { + slot: VariableSlot { + index: 0, + }, }, - }, - AppendContent { - content: "\n" (12..13), - }, - LoadFromContextToSlot { - name: "a_name" (17..23), - slot: VariableSlot { - index: 1, + AppendContent { + content: "\n" (12..13), }, - }, - EmitFromSlot { - slot: VariableSlot { - index: 1, + LoadFromContextToSlot { + name: "a_name" (17..23), + slot: VariableSlot { + index: 1, + }, }, - }, - AppendContent { - content: "\n" (26..27), - }, - LoadFromContextToSlot { - name: "1name" (31..36), - slot: VariableSlot { - index: 2, + EmitFromSlot { + slot: VariableSlot { + index: 1, + }, }, - }, - EmitFromSlot { - slot: VariableSlot { - index: 2, + AppendContent { + content: "\n" (26..27), }, - }, - AppendContent { - content: "\n" (39..40), - }, - LoadFromContextToSlot { - name: "_name1" (44..50), - slot: VariableSlot { - index: 3, + LoadFromContextToSlot { + name: "1name" (31..36), + slot: VariableSlot { + index: 2, + }, }, - }, - EmitFromSlot { - slot: VariableSlot { - index: 3, + EmitFromSlot { + slot: VariableSlot { + index: 2, + }, }, - }, - AppendContent { - content: "\n" (53..54), - }, - LoadFromContextToSlot { - name: "_namE" (58..63), - slot: VariableSlot { - index: 4, + AppendContent { + content: "\n" (39..40), }, - }, - EmitFromSlot { - slot: VariableSlot { - index: 4, + LoadFromContextToSlot { + name: "_name1" (44..50), + slot: VariableSlot { + index: 3, + }, }, - }, - AppendContent { - content: "\n" (66..67), - }, - LoadFromContextToSlot { - name: "name1" (71..76), - slot: VariableSlot { - index: 5, + EmitFromSlot { + slot: VariableSlot { + index: 3, + }, }, - }, - EmitFromSlot { - slot: VariableSlot { - index: 5, + AppendContent { + content: "\n" (53..54), }, - }, -] + LoadFromContextToSlot { + name: "_namE" (58..63), + slot: VariableSlot { + index: 4, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 4, + }, + }, + AppendContent { + content: "\n" (66..67), + }, + LoadFromContextToSlot { + name: "name1" (71..76), + slot: VariableSlot { + index: 5, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 5, + }, + }, + ], +} diff --git a/tests/cases/3-instructions@if_else_if.snap b/tests/cases/3-instructions@if_else_if.snap index c9ab17d..787d521 100644 --- a/tests/cases/3-instructions@if_else_if.snap +++ b/tests/cases/3-instructions@if_else_if.snap @@ -3,60 +3,81 @@ source: tests/file_tests.rs expression: emit input_file: tests/cases/if_else_if.nomo --- -[ - LoadFromContextToSlot { - name: "test" (6..10), - slot: VariableSlot { +VMInstructions { + labels: { + LabelSlot { + index: 2, + }: 7, + LabelSlot { + index: 4, + }: 14, + LabelSlot { index: 0, + }: 14, + }, + instructions: [ + LoadFromContextToSlot { + name: "test" (6..10), + slot: VariableSlot { + index: 1, + }, }, - }, - JumpIfNotTrue { - emit_slot: VariableSlot { - index: 0, + JumpIfNotTrue { + emit_slot: VariableSlot { + index: 1, + }, + jump: LabelSlot { + index: 2, + }, }, - jump: 5, - }, - AppendContent { - content: "\n " (13..18), - }, - AppendContent { - content: "Not Hello World! :C" (18..37), - }, - AppendContent { - content: "\n" (37..38), - }, - Jump { - jump: 9, - }, - AppendContent { - content: "\n " (13..18), - }, - LoadFromContextToSlot { - name: "another_test" (49..61), - slot: VariableSlot { - index: 1, + AppendContent { + content: "\n " (13..18), }, - }, - JumpIfNotTrue { - emit_slot: VariableSlot { - index: 1, + AppendContent { + content: "Not Hello World! :C" (18..37), }, - jump: 5, - }, - AppendContent { - content: "\n " (64..69), - }, - AppendContent { - content: "Hello World!" (69..81), - }, - AppendContent { - content: "\n" (81..82), - }, - Jump { - jump: 2, - }, - AppendContent { - content: "\n " (64..69), - }, - NoOp, -] + AppendContent { + content: "\n" (37..38), + }, + Jump { + jump: LabelSlot { + index: 0, + }, + }, + AppendContent { + content: "\n " (13..18), + }, + LoadFromContextToSlot { + name: "another_test" (49..61), + slot: VariableSlot { + index: 3, + }, + }, + JumpIfNotTrue { + emit_slot: VariableSlot { + index: 3, + }, + jump: LabelSlot { + index: 4, + }, + }, + AppendContent { + content: "\n " (64..69), + }, + AppendContent { + content: "Hello World!" (69..81), + }, + AppendContent { + content: "\n" (81..82), + }, + Jump { + jump: LabelSlot { + index: 0, + }, + }, + AppendContent { + content: "\n " (64..69), + }, + NoOp, + ], +} diff --git a/tests/cases/3-instructions@interpolation.snap b/tests/cases/3-instructions@interpolation.snap index f4873f3..82dc16b 100644 --- a/tests/cases/3-instructions@interpolation.snap +++ b/tests/cases/3-instructions@interpolation.snap @@ -3,22 +3,25 @@ source: tests/file_tests.rs expression: emit input_file: tests/cases/interpolation.nomo --- -[ - AppendContent { - content: "Hello! I'm" (0..10), - }, - AppendContent { - content: " " (10..11), - }, - LoadFromContextToSlot { - name: "name" (15..19), - slot: VariableSlot { - index: 0, +VMInstructions { + labels: {}, + instructions: [ + AppendContent { + content: "Hello! I'm" (0..10), }, - }, - EmitFromSlot { - slot: VariableSlot { - index: 0, + AppendContent { + content: " " (10..11), }, - }, -] + LoadFromContextToSlot { + name: "name" (15..19), + slot: VariableSlot { + index: 0, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 0, + }, + }, + ], +} diff --git a/tests/cases/3-instructions@multiple.snap b/tests/cases/3-instructions@multiple.snap index 99ccf8f..b6f13eb 100644 --- a/tests/cases/3-instructions@multiple.snap +++ b/tests/cases/3-instructions@multiple.snap @@ -3,36 +3,39 @@ source: tests/file_tests.rs expression: emit input_file: tests/cases/multiple.nomo --- -[ - AppendContent { - content: "Hi there! My name is" (0..20), - }, - AppendContent { - content: " " (20..21), - }, - LoadFromContextToSlot { - name: "name" (25..29), - slot: VariableSlot { - index: 0, +VMInstructions { + labels: {}, + instructions: [ + AppendContent { + content: "Hi there! My name is" (0..20), }, - }, - EmitFromSlot { - slot: VariableSlot { - index: 0, + AppendContent { + content: " " (20..21), }, - }, - AppendContent { - content: " " (32..33), - }, - LoadFromContextToSlot { - name: "lastname" (37..45), - slot: VariableSlot { - index: 1, + LoadFromContextToSlot { + name: "name" (25..29), + slot: VariableSlot { + index: 0, + }, }, - }, - EmitFromSlot { - slot: VariableSlot { - index: 1, + EmitFromSlot { + slot: VariableSlot { + index: 0, + }, }, - }, -] + AppendContent { + content: " " (32..33), + }, + LoadFromContextToSlot { + name: "lastname" (37..45), + slot: VariableSlot { + index: 1, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 1, + }, + }, + ], +} diff --git a/tests/cases/3-instructions@simple.snap b/tests/cases/3-instructions@simple.snap index 0f495a6..752a53b 100644 --- a/tests/cases/3-instructions@simple.snap +++ b/tests/cases/3-instructions@simple.snap @@ -3,8 +3,11 @@ source: tests/file_tests.rs expression: emit input_file: tests/cases/simple.nomo --- -[ - AppendContent { - content: "Hello World!" (0..12), - }, -] +VMInstructions { + labels: {}, + instructions: [ + AppendContent { + content: "Hello World!" (0..12), + }, + ], +} diff --git a/tests/cases/3-instructions@trim_whitespace.snap b/tests/cases/3-instructions@trim_whitespace.snap index 754e5e9..7add748 100644 --- a/tests/cases/3-instructions@trim_whitespace.snap +++ b/tests/cases/3-instructions@trim_whitespace.snap @@ -3,38 +3,52 @@ source: tests/file_tests.rs expression: emit input_file: tests/cases/trim_whitespace.nomo --- -[ - LoadFromContextToSlot { - name: "test" (6..10), - slot: VariableSlot { +VMInstructions { + labels: { + LabelSlot { + index: 2, + }: 7, + LabelSlot { index: 0, + }: 7, + }, + instructions: [ + LoadFromContextToSlot { + name: "test" (6..10), + slot: VariableSlot { + index: 1, + }, }, - }, - JumpIfNotTrue { - emit_slot: VariableSlot { - index: 0, + JumpIfNotTrue { + emit_slot: VariableSlot { + index: 1, + }, + jump: LabelSlot { + index: 2, + }, }, - jump: 5, - }, - AppendContent { - content: "Hello" (19..24), - }, - AppendContent { - content: " " (24..25), - }, - LoadFromContextToSlot { - name: "stuff" (29..34), - slot: VariableSlot { - index: 1, + AppendContent { + content: "Hello" (19..24), }, - }, - EmitFromSlot { - slot: VariableSlot { - index: 1, + AppendContent { + content: " " (24..25), }, - }, - Jump { - jump: 1, - }, - NoOp, -] + LoadFromContextToSlot { + name: "stuff" (29..34), + slot: VariableSlot { + index: 3, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 3, + }, + }, + Jump { + jump: LabelSlot { + index: 0, + }, + }, + NoOp, + ], +} diff --git a/tests/cases/4-output@if_else_if.snap b/tests/cases/4-output@if_else_if.snap index 8908bd0..c815937 100644 --- a/tests/cases/4-output@if_else_if.snap +++ b/tests/cases/4-output@if_else_if.snap @@ -3,4 +3,4 @@ source: tests/file_tests.rs expression: output input_file: tests/cases/if_else_if.nomo --- -"\n \n Hello World!\n" +"\n Hello World!\n" From 42e00563747978bfb9063cf9bfda535218d5a591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 11 Mar 2026 18:09:58 +0100 Subject: [PATCH 5/7] Add for loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- Cargo.lock | 2 +- Cargo.toml | 11 +- src/emit/mod.rs | 185 ++++++++++++- .../nomo__emit__tests__check_if_else_if.snap | 6 +- src/eval/mod.rs | 121 ++++++++- src/lib.rs | 25 +- src/value.rs | 251 ++++++++++++++++++ tests/cases/1-parsed@simple_for.snap | 41 +++ tests/cases/2-ast@simple_for.snap | 44 +++ tests/cases/3-instructions@condition.snap | 4 +- tests/cases/3-instructions@if_else_if.snap | 6 +- tests/cases/3-instructions@simple_for.snap | 87 ++++++ .../cases/3-instructions@trim_whitespace.snap | 4 +- tests/cases/4-output@simple_for.snap | 11 + tests/cases/simple_for.nomo | 7 + tests/file_tests.rs | 14 +- 16 files changed, 775 insertions(+), 44 deletions(-) create mode 100644 src/value.rs create mode 100644 tests/cases/1-parsed@simple_for.snap create mode 100644 tests/cases/2-ast@simple_for.snap create mode 100644 tests/cases/3-instructions@simple_for.snap create mode 100644 tests/cases/4-output@simple_for.snap create mode 100644 tests/cases/simple_for.nomo diff --git a/Cargo.lock b/Cargo.lock index e6d1099..729eb06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,6 +371,7 @@ dependencies = [ "console", "globset", "once_cell", + "serde", "similar", "tempfile", "walkdir", @@ -439,7 +440,6 @@ dependencies = [ "criterion", "displaydoc", "insta", - "nomo", "serde", "serde_json", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index c75a84e..8e383d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,20 +13,21 @@ debug = true [dependencies] annotate-snippets = "0.12.13" displaydoc = "0.2.5" -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.149" +serde_json = { version = "1.0.149", optional = true } thiserror = "2.0.18" winnow = { version = "0.7.14", features = ["unstable-recover"] } [dev-dependencies] annotate-snippets = { version = "0.12.13", features = ["testing-colors"] } criterion = "0.8.2" -insta = { version = "1.46.3", features = ["glob"] } -nomo = { path = ".", features = ["serialize"] } +insta = { version = "1.46.3", features = ["glob", "serde"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" [profile.dev.package] insta.opt-level = 3 similar.opt-level = 3 [features] -serialize = [] +default = ["serde_json"] +serde_json = ["dep:serde_json"] diff --git a/src/emit/mod.rs b/src/emit/mod.rs index 5ce4ac7..b34d4cf 100644 --- a/src/emit/mod.rs +++ b/src/emit/mod.rs @@ -1,11 +1,11 @@ -use std::collections::HashMap; +use std::collections::BTreeMap; use crate::ast::TemplateAstExpr; use crate::input::NomoInput; pub struct EmitMachine { current_index: usize, - labels: HashMap, + labels: BTreeMap, } impl EmitMachine { @@ -36,12 +36,12 @@ impl EmitMachine { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct VariableSlot { index: usize, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct LabelSlot { index: usize, } @@ -70,11 +70,29 @@ pub enum Instruction { jump: LabelSlot, }, NoOp, + CreateIteratorFromSlotToSlot { + iterator_slot: VariableSlot, + iterator_source_slot: VariableSlot, + }, + AdvanceIteratorOrJump { + iterator_slot: VariableSlot, + value_slot: VariableSlot, + jump: LabelSlot, + }, + GetIteratorEmptyOrJump { + iterator_slot: VariableSlot, + jump: LabelSlot, + }, + PopScope, + LoadFromSlotToContext { + value_ident: NomoInput, + value_slot: VariableSlot, + }, } #[derive(Debug, Clone)] pub struct VMInstructions { - pub labels: HashMap, + pub labels: BTreeMap, pub instructions: Vec, } @@ -83,7 +101,7 @@ pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> VMInstructions { let mut machine = EmitMachine { current_index: 0, - labels: HashMap::new(), + labels: BTreeMap::new(), }; for ast in input.root() { @@ -221,13 +239,166 @@ fn emit_ast_expr( eval.push(Instruction::NoOp); } } + TemplateAstExpr::ForChain { + for_block, + content, + else_block, + else_content, + end_block, + } => { + let post_for_whitespace_content; + let label_to_else_or_empty_index = machine.reserve_label(); + let label_to_end_index = machine.reserve_label(); + let label_start_loop = machine.reserve_label(); + if let TemplateAstExpr::Block { + prev_whitespace_content, + expression, + post_whitespace_content, + } = &**for_block + && let TemplateAstExpr::For { + value_ident, + value_expression, + } = &**expression + { + if let Some(ws) = prev_whitespace_content { + eval.push(Instruction::AppendContent { + content: ws.source().clone(), + }); + } + post_for_whitespace_content = post_whitespace_content; + + eval.push(Instruction::PushScope { + inherit_parent: true, + }); + + let value_slot = machine.reserve_slot(); + let iterator_source_slot = machine.reserve_slot(); + let iterator_slot = machine.reserve_slot(); + + emit_expr_load(machine, eval, iterator_source_slot, value_expression); + eval.push(Instruction::CreateIteratorFromSlotToSlot { + iterator_source_slot, + iterator_slot, + }); + + eval.push(Instruction::GetIteratorEmptyOrJump { + iterator_slot, + jump: label_to_else_or_empty_index, + }); + + machine.assign_label(label_start_loop, eval.len()); + eval.push(Instruction::AdvanceIteratorOrJump { + iterator_slot, + value_slot, + jump: label_to_end_index, + }); + + eval.push(Instruction::LoadFromSlotToContext { + value_slot, + value_ident: value_ident.source(), + }); + } else { + panic!("For block should be a for block"); + }; + + if let Some(ws) = post_for_whitespace_content { + eval.push(Instruction::AppendContent { + content: ws.source().clone(), + }); + } + + for content in content { + emit_ast_expr(machine, eval, content); + } + + let end_of_content_jump = eval.len(); + eval.push(Instruction::Jump { + jump: label_start_loop, + }); + + let has_else = else_block.is_some(); + + if let Some(TemplateAstExpr::Block { + prev_whitespace_content, + expression, + post_whitespace_content, + }) = else_block.as_deref() + && let TemplateAstExpr::ForElse = &**expression + { + if let Some(ws) = prev_whitespace_content { + eval.insert( + end_of_content_jump.saturating_sub(1), + Instruction::AppendContent { + content: ws.source().clone(), + }, + ); + } + + machine.assign_label(label_to_else_or_empty_index, eval.len()); + + if let Some(ws) = post_whitespace_content { + eval.push(Instruction::AppendContent { + content: ws.source().clone(), + }); + } + + for content in else_content + .as_ref() + .expect("If there is a for block, there should be for content (even if empty)") + { + emit_ast_expr(machine, eval, content); + } + } + + let post_end_whitespace_content; + + if let TemplateAstExpr::Block { + prev_whitespace_content, + expression, + post_whitespace_content, + } = &**end_block + && let TemplateAstExpr::EndBlock = &**expression + { + post_end_whitespace_content = post_whitespace_content; + + if let Some(ws) = prev_whitespace_content { + if has_else { + eval.push(Instruction::AppendContent { + content: ws.source().clone(), + }); + } else { + eval.insert( + end_of_content_jump.saturating_sub(1), + Instruction::AppendContent { + content: ws.source().clone(), + }, + ); + } + } + + if !has_else { + machine.assign_label(label_to_else_or_empty_index, eval.len()); + } + + machine.assign_label(label_to_end_index, eval.len()); + + eval.push(Instruction::PopScope); + + if let Some(ws) = post_end_whitespace_content { + eval.push(Instruction::AppendContent { + content: ws.source().clone(), + }); + } + } else { + panic!("End block should be an endblock"); + } + } TemplateAstExpr::Block { .. } | TemplateAstExpr::EndBlock | TemplateAstExpr::IfConditional { .. } | TemplateAstExpr::ConditionalContent { .. } | TemplateAstExpr::ElseConditional { .. } - | TemplateAstExpr::ForChain { .. } | TemplateAstExpr::For { .. } | TemplateAstExpr::ForElse | TemplateAstExpr::Invalid { .. } diff --git a/src/emit/snapshots/nomo__emit__tests__check_if_else_if.snap b/src/emit/snapshots/nomo__emit__tests__check_if_else_if.snap index 5a6c89f..e178993 100644 --- a/src/emit/snapshots/nomo__emit__tests__check_if_else_if.snap +++ b/src/emit/snapshots/nomo__emit__tests__check_if_else_if.snap @@ -4,15 +4,15 @@ expression: emit --- VMInstructions { labels: { - LabelSlot { - index: 4, - }: 14, LabelSlot { index: 0, }: 19, LabelSlot { index: 2, }: 7, + LabelSlot { + index: 4, + }: 14, }, instructions: [ LoadFromContextToSlot { diff --git a/src/eval/mod.rs b/src/eval/mod.rs index b44ecaa..44600ac 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -6,7 +6,9 @@ use thiserror::Error; use crate::Context; use crate::emit::Instruction; use crate::emit::VMInstructions; +use crate::emit::VariableSlot; use crate::input::NomoInput; +use crate::value::NomoValue; #[derive(Debug, Error, Display)] pub enum EvaluationError { @@ -21,10 +23,59 @@ pub enum EvaluationError { LabelNotFound, } +struct Scope { + stack: Vec>, + slots: HashMap, +} +impl Scope { + fn insert_into_slot(&mut self, slot: VariableSlot, value: NomoValue) { + self.slots.insert(slot, value); + } + + fn get(&self, slot: &VariableSlot) -> &NomoValue { + self.slots.get(slot).expect("All slot loads must be valid") + } + + fn get_mut(&mut self, slot: &VariableSlot) -> &mut NomoValue { + self.slots + .get_mut(slot) + .expect("All slot loads must be valid") + } + + fn push_scope(&mut self) { + self.stack.push(Default::default()); + } + + fn pop_scope(&mut self) { + self.stack.pop(); + } + + fn insert_into_scope(&mut self, ident: &NomoInput, value: NomoValue) { + self.stack + .last_mut() + .unwrap() + .insert(ident.to_string(), value); + } + + fn get_scoped(&self, name: &NomoInput) -> Option<&NomoValue> { + self.stack + .iter() + .rev() + .find_map(|scope| scope.get(name.as_str())) + } +} + +#[allow( + clippy::unnecessary_to_owned, + reason = "We cannot do the suggested way as the lifetimes would not match up" +)] pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result { let mut output = String::new(); - let mut scopes: HashMap = HashMap::new(); + let mut scopes = Scope { + stack: vec![global_context.values().clone()], + slots: HashMap::new(), + }; let mut ip = 0; loop { @@ -38,21 +89,22 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result (), Instruction::AppendContent { content } => output.push_str(content), Instruction::LoadFromContextToSlot { name, slot } => { - let value = global_context - .values - .get(name.as_str()) + let value = scopes + .get_scoped(name) .ok_or(EvaluationError::UnknownVariable(name.clone()))?; - scopes.insert(*slot, value.clone()); + scopes.insert_into_slot(*slot, value.clone()); } Instruction::EmitFromSlot { slot } => { - let value = scopes.get(slot).unwrap().as_str().unwrap(); + let value = scopes.get(slot).as_str().unwrap(); output.push_str(value); } - Instruction::PushScope { inherit_parent: _ } => todo!(), + Instruction::PushScope { inherit_parent: _ } => { + scopes.push_scope(); + } Instruction::Abort => return Err(EvaluationError::ExplicitAbort), Instruction::JumpIfNotTrue { emit_slot, jump } => { - let dont_jump = scopes.get(emit_slot).unwrap().as_bool().unwrap(); + let dont_jump = scopes.get(emit_slot).as_bool().unwrap(); if dont_jump { // We are done } else { @@ -72,6 +124,59 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result { + let value = scopes.get(iterator_source_slot).as_array().unwrap(); + scopes.insert_into_slot( + *iterator_slot, + NomoValue::Iterator { + value: Box::new(value.to_vec().into_iter()), + }, + ); + } + Instruction::AdvanceIteratorOrJump { + iterator_slot, + value_slot, + jump, + } => { + let iterator = scopes.get_mut(iterator_slot).as_iterator_mut().unwrap(); + + if let Some(value) = iterator.next() { + scopes.insert_into_slot(*value_slot, value); + } else { + let Some(new_ip) = vm.labels.get(jump) else { + return Err(EvaluationError::LabelNotFound); + }; + + ip = *new_ip; + } + } + Instruction::GetIteratorEmptyOrJump { + iterator_slot, + jump, + } => { + let iterator = scopes.get(iterator_slot).as_iterator().unwrap(); + let (min, _) = iterator.size_hint(); + + if min == 0 { + let Some(new_ip) = vm.labels.get(jump) else { + return Err(EvaluationError::LabelNotFound); + }; + + ip = *new_ip; + } + } + Instruction::PopScope => scopes.pop_scope(), + Instruction::LoadFromSlotToContext { + value_ident, + value_slot, + } => { + let value = scopes.get(value_slot).clone(); + + scopes.insert_into_scope(value_ident, value); + } } ip += 1; diff --git a/src/lib.rs b/src/lib.rs index 53024bf..8d177ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,19 @@ -use std::collections::BTreeMap; use std::collections::HashMap; use displaydoc::Display; -use serde::Serialize; use thiserror::Error; -use crate::emit::Instruction; use crate::emit::VMInstructions; use crate::input::NomoInput; +use crate::value::NomoValue; +use crate::value::NomoValueError; pub mod ast; pub mod emit; pub mod eval; pub mod input; pub mod parser; +pub mod value; #[derive(Debug, Error, Display)] pub enum NomoError { @@ -89,7 +89,7 @@ struct Template { } pub struct Context { - values: BTreeMap, + values: HashMap, } impl Default for Context { @@ -101,23 +101,26 @@ impl Default for Context { impl Context { pub fn new() -> Context { Context { - values: BTreeMap::new(), + values: HashMap::new(), } } pub fn try_insert( &mut self, key: impl Into, - value: impl Serialize, - ) -> Result<(), serde_json::Error> { - self.values.insert(key.into(), serde_json::to_value(value)?); + value: impl TryInto, + ) -> Result<(), NomoValueError> { + self.values.insert(key.into(), value.try_into()?); Ok(()) } - pub fn insert(&mut self, key: impl Into, value: impl Serialize) { - self.try_insert(key, value) - .expect("inserted value should serialize without error"); + pub fn insert(&mut self, key: impl Into, value: impl Into) { + self.values.insert(key.into(), value.into()); + } + + pub fn values(&self) -> &HashMap { + &self.values } } diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..1dc86e0 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,251 @@ +use std::borrow::Cow; +use std::collections::BTreeMap; + +#[cfg(feature = "serde_json")] +use displaydoc::Display; +use thiserror::Error; + +#[derive(Clone)] +pub enum NomoValue { + String { + value: Cow<'static, str>, + }, + Array { + value: Vec, + }, + Bool { + value: bool, + }, + Object { + value: BTreeMap, + }, + Integer { + value: u64, + }, + SignedInteger { + value: i64, + }, + Float { + value: f64, + }, + Iterator { + value: Box>, + }, +} + +impl NomoValue { + pub fn as_str(&self) -> Option<&str> { + if let Self::String { value } = self { + Some(value) + } else { + None + } + } + + pub fn as_array(&self) -> Option<&[NomoValue]> { + if let Self::Array { value } = self { + Some(value) + } else { + None + } + } + + pub fn as_bool(&self) -> Option { + if let Self::Bool { value } = self { + Some(*value) + } else { + None + } + } + + pub fn as_object(&self) -> Option<&BTreeMap> { + if let Self::Object { value } = self { + Some(value) + } else { + None + } + } + + pub fn as_integer(&self) -> Option { + if let Self::Integer { value } = self { + Some(*value) + } else { + None + } + } + + pub fn as_float(&self) -> Option { + if let Self::Float { value } = self { + Some(*value) + } else { + None + } + } + + pub fn as_iterator(&self) -> Option<&dyn CloneIterator> { + if let Self::Iterator { value } = self { + Some(value) + } else { + None + } + } + + pub fn as_iterator_mut(&mut self) -> Option<&mut dyn CloneIterator> { + if let Self::Iterator { value } = self { + Some(value) + } else { + None + } + } +} + +pub trait CloneIterator: Iterator { + fn clone_box(&self) -> Box>; +} + +impl CloneIterator for I +where + I: Iterator + Clone + 'static, +{ + fn clone_box(&self) -> Box> { + Box::new(Clone::clone(self)) + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.clone_box() + } +} + +impl std::fmt::Debug for NomoValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::String { value } => f.debug_struct("String").field("value", value).finish(), + Self::Array { value } => f.debug_struct("Array").field("value", value).finish(), + Self::Bool { value } => f.debug_struct("Bool").field("value", value).finish(), + Self::Object { value } => f.debug_struct("Object").field("value", value).finish(), + Self::Integer { value } => f.debug_struct("Integer").field("value", value).finish(), + Self::SignedInteger { value } => f + .debug_struct("SignedInteger") + .field("value", value) + .finish(), + Self::Float { value } => f.debug_struct("Float").field("value", value).finish(), + Self::Iterator { value: _ } => f + .debug_struct("Iterator") + .field("value", &"Iterator") + .finish(), + } + } +} + +impl From<&R> for NomoValue +where + R: Into + Clone, +{ + fn from(value: &R) -> Self { + value.clone().into() + } +} + +impl From for NomoValue { + fn from(val: String) -> Self { + NomoValue::String { + value: Cow::Owned(val), + } + } +} + +impl From<&'static str> for NomoValue { + fn from(val: &'static str) -> Self { + NomoValue::String { + value: Cow::Borrowed(val), + } + } +} + +impl From> for NomoValue +where + V: Into, +{ + fn from(val: Vec) -> Self { + NomoValue::Array { + value: val.into_iter().map(Into::into).collect(), + } + } +} + +impl From> for NomoValue +where + V: Into, +{ + fn from(val: std::collections::VecDeque) -> Self { + NomoValue::Array { + value: val.into_iter().map(Into::into).collect(), + } + } +} + +impl From<&[V]> for NomoValue +where + V: Into + Clone, +{ + fn from(value: &[V]) -> Self { + NomoValue::Array { + value: value.iter().cloned().map(Into::into).collect(), + } + } +} + +impl From> for NomoValue +where + K: Into, + V: Into, +{ + fn from(val: std::collections::HashMap) -> Self { + NomoValue::Object { + value: val.into_iter().map(|(k, v)| (k.into(), v.into())).collect(), + } + } +} + +#[cfg(feature = "serde_json")] +#[derive(Debug, Error, Display)] +/// Could not transform value to [`NomoValue`] +pub struct NomoValueError; + +#[cfg(feature = "serde_json")] +impl TryFrom for NomoValue { + type Error = NomoValueError; + fn try_from(value: serde_json::Value) -> Result { + match value { + serde_json::Value::Null => todo!(), + serde_json::Value::Bool(value) => Ok(NomoValue::Bool { value }), + serde_json::Value::Number(number) => { + if let Some(value) = number.as_u64() { + return Ok(NomoValue::Integer { value }); + } + + if let Some(value) = number.as_f64() { + return Ok(NomoValue::Float { value }); + } + + if let Some(value) = number.as_i64() { + return Ok(NomoValue::SignedInteger { value }); + } + + Err(NomoValueError) + } + serde_json::Value::String(str) => Ok(NomoValue::String { + value: Cow::Owned(str), + }), + serde_json::Value::Array(values) => Ok(NomoValue::Array { + value: values + .into_iter() + .map(TryInto::try_into) + .collect::>()?, + }), + serde_json::Value::Object(_map) => todo!(), + } + } +} diff --git a/tests/cases/1-parsed@simple_for.snap b/tests/cases/1-parsed@simple_for.snap new file mode 100644 index 0000000..a903eb4 --- /dev/null +++ b/tests/cases/1-parsed@simple_for.snap @@ -0,0 +1,41 @@ +--- +source: tests/file_tests.rs +expression: parsed +info: + context: + values: + - one + - two +input_file: tests/cases/simple_for.nomo +--- +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]"values" (16..22), + [Whitespace]" " (22..23), + [TrimWhitespace]"-" (23..24), + [RightDelim]"}}" (24..26), + [Whitespace]"\n " (26..31), + [LeftDelim]"{{" (31..33), + [TrimWhitespace]"-" (33..34), + [WantsOutput]"=" (34..35), + [Whitespace]" " (35..36), + [Ident]"value" (36..41), + [Whitespace]" " (41..42), + [RightDelim]"}}" (42..44), + [Whitespace]"\n" (44..45), + [LeftDelim]"{{" (45..47), + [TrimWhitespace]"-" (47..48), + [Whitespace]" " (48..49), + [End]"end" (49..52), + [Whitespace]" " (52..53), + [RightDelim]"}}" (53..55), + ], +} diff --git a/tests/cases/2-ast@simple_for.snap b/tests/cases/2-ast@simple_for.snap new file mode 100644 index 0000000..9b8a4ae --- /dev/null +++ b/tests/cases/2-ast@simple_for.snap @@ -0,0 +1,44 @@ +--- +source: tests/file_tests.rs +expression: ast +info: + context: + values: + - one + - two +input_file: tests/cases/simple_for.nomo +--- +TemplateAst { + root: [ + ForChain { + for_block: Block { + prev_whitespace_content: None, + expression: For { + value_ident: [Ident]"value" (7..12), + value_expression: VariableAccess( + [Ident]"values" (16..22), + ), + }, + post_whitespace_content: None, + }, + content: [ + Interpolation { + prev_whitespace_content: None, + expression: VariableAccess( + [Ident]"value" (36..41), + ), + post_whitespace_content: Some( + [Whitespace]"\n" (44..45), + ), + }, + ], + else_block: None, + else_content: None, + end_block: Block { + prev_whitespace_content: None, + expression: EndBlock, + post_whitespace_content: None, + }, + }, + ], +} diff --git a/tests/cases/3-instructions@condition.snap b/tests/cases/3-instructions@condition.snap index 1b96697..5f79488 100644 --- a/tests/cases/3-instructions@condition.snap +++ b/tests/cases/3-instructions@condition.snap @@ -6,10 +6,10 @@ input_file: tests/cases/condition.nomo VMInstructions { labels: { LabelSlot { - index: 2, + index: 0, }: 7, LabelSlot { - index: 0, + index: 2, }: 7, }, instructions: [ diff --git a/tests/cases/3-instructions@if_else_if.snap b/tests/cases/3-instructions@if_else_if.snap index 787d521..124556e 100644 --- a/tests/cases/3-instructions@if_else_if.snap +++ b/tests/cases/3-instructions@if_else_if.snap @@ -5,15 +5,15 @@ input_file: tests/cases/if_else_if.nomo --- VMInstructions { labels: { + LabelSlot { + index: 0, + }: 14, LabelSlot { index: 2, }: 7, LabelSlot { index: 4, }: 14, - LabelSlot { - index: 0, - }: 14, }, instructions: [ LoadFromContextToSlot { diff --git a/tests/cases/3-instructions@simple_for.snap b/tests/cases/3-instructions@simple_for.snap new file mode 100644 index 0000000..00e554e --- /dev/null +++ b/tests/cases/3-instructions@simple_for.snap @@ -0,0 +1,87 @@ +--- +source: tests/file_tests.rs +expression: emit +info: + context: + values: + - one + - two +input_file: tests/cases/simple_for.nomo +--- +VMInstructions { + labels: { + LabelSlot { + index: 0, + }: 10, + LabelSlot { + index: 1, + }: 10, + LabelSlot { + index: 2, + }: 4, + }, + instructions: [ + PushScope { + inherit_parent: true, + }, + LoadFromContextToSlot { + name: "values" (16..22), + slot: VariableSlot { + index: 4, + }, + }, + CreateIteratorFromSlotToSlot { + iterator_slot: VariableSlot { + index: 5, + }, + iterator_source_slot: VariableSlot { + index: 4, + }, + }, + GetIteratorEmptyOrJump { + iterator_slot: VariableSlot { + index: 5, + }, + jump: LabelSlot { + index: 0, + }, + }, + AdvanceIteratorOrJump { + iterator_slot: VariableSlot { + index: 5, + }, + value_slot: VariableSlot { + index: 3, + }, + jump: LabelSlot { + index: 1, + }, + }, + LoadFromSlotToContext { + value_ident: "value" (7..12), + value_slot: VariableSlot { + index: 3, + }, + }, + LoadFromContextToSlot { + name: "value" (36..41), + slot: VariableSlot { + index: 6, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 6, + }, + }, + AppendContent { + content: "\n" (44..45), + }, + Jump { + jump: LabelSlot { + index: 2, + }, + }, + PopScope, + ], +} diff --git a/tests/cases/3-instructions@trim_whitespace.snap b/tests/cases/3-instructions@trim_whitespace.snap index 7add748..5c1ad06 100644 --- a/tests/cases/3-instructions@trim_whitespace.snap +++ b/tests/cases/3-instructions@trim_whitespace.snap @@ -6,10 +6,10 @@ input_file: tests/cases/trim_whitespace.nomo VMInstructions { labels: { LabelSlot { - index: 2, + index: 0, }: 7, LabelSlot { - index: 0, + index: 2, }: 7, }, instructions: [ diff --git a/tests/cases/4-output@simple_for.snap b/tests/cases/4-output@simple_for.snap new file mode 100644 index 0000000..118ff9e --- /dev/null +++ b/tests/cases/4-output@simple_for.snap @@ -0,0 +1,11 @@ +--- +source: tests/file_tests.rs +expression: output +info: + context: + values: + - one + - two +input_file: tests/cases/simple_for.nomo +--- +"one\ntwo\n" diff --git a/tests/cases/simple_for.nomo b/tests/cases/simple_for.nomo new file mode 100644 index 0000000..a62c36a --- /dev/null +++ b/tests/cases/simple_for.nomo @@ -0,0 +1,7 @@ +{ + "values": [ "one", "two" ] +} +--- +{{ for value in values -}} + {{-= value }} +{{- end }} \ No newline at end of file diff --git a/tests/file_tests.rs b/tests/file_tests.rs index 3acb9ab..aec2f1d 100644 --- a/tests/file_tests.rs +++ b/tests/file_tests.rs @@ -2,6 +2,11 @@ use std::collections::HashMap; use nomo::Context; +#[derive(serde::Serialize)] +struct Info { + context: HashMap, +} + #[test] fn check_cases() { insta::glob!("cases/*.nomo", |path| { @@ -9,7 +14,6 @@ fn check_cases() { settings.set_snapshot_path("cases"); settings.set_snapshot_suffix(path.file_stem().unwrap().display().to_string()); settings.set_prepend_module_to_snapshot(false); - let _guard = settings.bind_to_scope(); let input = std::fs::read_to_string(path).unwrap(); @@ -21,14 +25,20 @@ fn check_cases() { HashMap::new() }; + settings.set_info(&Info { + context: map.clone(), + }); + let mut context = Context::new(); for (k, v) in map { - context.insert(k, v); + context.try_insert(k, v).unwrap(); } let parsed = nomo::parser::parse(input.into()).unwrap(); + let _guard = settings.bind_to_scope(); + insta::assert_debug_snapshot!("1-parsed", parsed); let ast = match nomo::ast::parse(parsed.tokens()) { From dc8281036cad20750b4ae69094f5d61c0035e4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 11 Mar 2026 18:15:14 +0100 Subject: [PATCH 6/7] Add tests for for loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- src/eval/mod.rs | 2 + tests/cases/1-parsed@simple_for.snap | 39 +++++++++++ tests/cases/2-ast@simple_for.snap | 46 +++++++++++++ tests/cases/3-instructions@simple_for.snap | 79 ++++++++++++++++++++++ tests/cases/4-output@simple_for.snap | 4 +- tests/cases/simple_for.nomo | 8 ++- tests/file_tests.rs | 2 + 7 files changed, 178 insertions(+), 2 deletions(-) diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 44600ac..3059618 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -151,6 +151,7 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result Result scopes.pop_scope(), diff --git a/tests/cases/1-parsed@simple_for.snap b/tests/cases/1-parsed@simple_for.snap index a903eb4..4e391b7 100644 --- a/tests/cases/1-parsed@simple_for.snap +++ b/tests/cases/1-parsed@simple_for.snap @@ -2,7 +2,9 @@ source: tests/file_tests.rs expression: parsed info: + input: "{{ for value in values -}}\n {{-= value }}\n{{- end }}\n{{ for value in no_values -}}\n {{-= value }}\n{{ else -}}\nNo Values >:C\n{{- end }}" context: + no_values: [] values: - one - two @@ -37,5 +39,42 @@ ParsedTemplate { [End]"end" (49..52), [Whitespace]" " (52..53), [RightDelim]"}}" (53..55), + [Whitespace]"\n" (55..56), + [LeftDelim]"{{" (56..58), + [Whitespace]" " (58..59), + [For]"for" (59..62), + [Whitespace]" " (62..63), + [Ident]"value" (63..68), + [Whitespace]" " (68..69), + [In]"in" (69..71), + [Whitespace]" " (71..72), + [Ident]"no_values" (72..81), + [Whitespace]" " (81..82), + [TrimWhitespace]"-" (82..83), + [RightDelim]"}}" (83..85), + [Whitespace]"\n " (85..90), + [LeftDelim]"{{" (90..92), + [TrimWhitespace]"-" (92..93), + [WantsOutput]"=" (93..94), + [Whitespace]" " (94..95), + [Ident]"value" (95..100), + [Whitespace]" " (100..101), + [RightDelim]"}}" (101..103), + [Whitespace]"\n" (103..104), + [LeftDelim]"{{" (104..106), + [Whitespace]" " (106..107), + [ConditionalElse]"else" (107..111), + [Whitespace]" " (111..112), + [TrimWhitespace]"-" (112..113), + [RightDelim]"}}" (113..115), + [Whitespace]"\n" (115..116), + [Content]"No Values >:C" (116..129), + [Whitespace]"\n" (129..130), + [LeftDelim]"{{" (130..132), + [TrimWhitespace]"-" (132..133), + [Whitespace]" " (133..134), + [End]"end" (134..137), + [Whitespace]" " (137..138), + [RightDelim]"}}" (138..140), ], } diff --git a/tests/cases/2-ast@simple_for.snap b/tests/cases/2-ast@simple_for.snap index 9b8a4ae..5071ece 100644 --- a/tests/cases/2-ast@simple_for.snap +++ b/tests/cases/2-ast@simple_for.snap @@ -2,7 +2,9 @@ source: tests/file_tests.rs expression: ast info: + input: "{{ for value in values -}}\n {{-= value }}\n{{- end }}\n{{ for value in no_values -}}\n {{-= value }}\n{{ else -}}\nNo Values >:C\n{{- end }}" context: + no_values: [] values: - one - two @@ -34,6 +36,50 @@ TemplateAst { ], else_block: None, else_content: None, + end_block: Block { + prev_whitespace_content: None, + expression: EndBlock, + post_whitespace_content: Some( + [Whitespace]"\n" (55..56), + ), + }, + }, + ForChain { + for_block: Block { + prev_whitespace_content: None, + expression: For { + value_ident: [Ident]"value" (63..68), + value_expression: VariableAccess( + [Ident]"no_values" (72..81), + ), + }, + post_whitespace_content: None, + }, + content: [ + Interpolation { + prev_whitespace_content: None, + expression: VariableAccess( + [Ident]"value" (95..100), + ), + post_whitespace_content: Some( + [Whitespace]"\n" (103..104), + ), + }, + ], + else_block: Some( + Block { + prev_whitespace_content: None, + expression: ForElse, + post_whitespace_content: None, + }, + ), + else_content: Some( + [ + StaticContent( + [Content]"No Values >:C" (116..129), + ), + ], + ), end_block: Block { prev_whitespace_content: None, expression: EndBlock, diff --git a/tests/cases/3-instructions@simple_for.snap b/tests/cases/3-instructions@simple_for.snap index 00e554e..0cac073 100644 --- a/tests/cases/3-instructions@simple_for.snap +++ b/tests/cases/3-instructions@simple_for.snap @@ -2,7 +2,9 @@ source: tests/file_tests.rs expression: emit info: + input: "{{ for value in values -}}\n {{-= value }}\n{{- end }}\n{{ for value in no_values -}}\n {{-= value }}\n{{ else -}}\nNo Values >:C\n{{- end }}" context: + no_values: [] values: - one - two @@ -19,6 +21,15 @@ VMInstructions { LabelSlot { index: 2, }: 4, + LabelSlot { + index: 7, + }: 22, + LabelSlot { + index: 8, + }: 23, + LabelSlot { + index: 9, + }: 16, }, instructions: [ PushScope { @@ -83,5 +94,73 @@ VMInstructions { }, }, PopScope, + AppendContent { + content: "\n" (55..56), + }, + PushScope { + inherit_parent: true, + }, + LoadFromContextToSlot { + name: "no_values" (72..81), + slot: VariableSlot { + index: 11, + }, + }, + CreateIteratorFromSlotToSlot { + iterator_slot: VariableSlot { + index: 12, + }, + iterator_source_slot: VariableSlot { + index: 11, + }, + }, + GetIteratorEmptyOrJump { + iterator_slot: VariableSlot { + index: 12, + }, + jump: LabelSlot { + index: 7, + }, + }, + AdvanceIteratorOrJump { + iterator_slot: VariableSlot { + index: 12, + }, + value_slot: VariableSlot { + index: 10, + }, + jump: LabelSlot { + index: 8, + }, + }, + LoadFromSlotToContext { + value_ident: "value" (63..68), + value_slot: VariableSlot { + index: 10, + }, + }, + LoadFromContextToSlot { + name: "value" (95..100), + slot: VariableSlot { + index: 13, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 13, + }, + }, + AppendContent { + content: "\n" (103..104), + }, + Jump { + jump: LabelSlot { + index: 9, + }, + }, + AppendContent { + content: "No Values >:C" (116..129), + }, + PopScope, ], } diff --git a/tests/cases/4-output@simple_for.snap b/tests/cases/4-output@simple_for.snap index 118ff9e..0c2ca8d 100644 --- a/tests/cases/4-output@simple_for.snap +++ b/tests/cases/4-output@simple_for.snap @@ -2,10 +2,12 @@ source: tests/file_tests.rs expression: output info: + input: "{{ for value in values -}}\n {{-= value }}\n{{- end }}\n{{ for value in no_values -}}\n {{-= value }}\n{{ else -}}\nNo Values >:C\n{{- end }}" context: + no_values: [] values: - one - two input_file: tests/cases/simple_for.nomo --- -"one\ntwo\n" +"one\ntwo\n\nNo Values >:C" diff --git a/tests/cases/simple_for.nomo b/tests/cases/simple_for.nomo index a62c36a..b9fa46f 100644 --- a/tests/cases/simple_for.nomo +++ b/tests/cases/simple_for.nomo @@ -1,7 +1,13 @@ { - "values": [ "one", "two" ] + "values": [ "one", "two" ], + "no_values": [] } --- {{ for value in values -}} {{-= value }} +{{- end }} +{{ for value in no_values -}} + {{-= value }} +{{ else -}} +No Values >:C {{- end }} \ No newline at end of file diff --git a/tests/file_tests.rs b/tests/file_tests.rs index aec2f1d..523758d 100644 --- a/tests/file_tests.rs +++ b/tests/file_tests.rs @@ -4,6 +4,7 @@ use nomo::Context; #[derive(serde::Serialize)] struct Info { + input: String, context: HashMap, } @@ -26,6 +27,7 @@ fn check_cases() { }; settings.set_info(&Info { + input: input.to_string(), context: map.clone(), }); From 474324726a740e1576a0b7b1930448d1d14564af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Wed, 11 Mar 2026 18:39:57 +0100 Subject: [PATCH 7/7] Add fuzzer for deeply nested parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- benches/parsing.rs | 23 ++++++++++++++++++++++- fuzz/Cargo.lock | 2 -- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/benches/parsing.rs b/benches/parsing.rs index f7e3da0..35de7ae 100644 --- a/benches/parsing.rs +++ b/benches/parsing.rs @@ -23,5 +23,26 @@ fn parsing_benchmark(c: &mut Criterion) { } } -criterion_group!(benches, parsing_benchmark); +fn parsing_nested(c: &mut Criterion) { + let mut parsing = c.benchmark_group("Parsing"); + + for size in [1, 2, 8, 12] { + let mut input = String::new(); + + for _ in 0..size { + input = format!( + "{{{{ for foo in bar }}}} {input} {{{{ if foo }}}} Hi! {input} {{{{ end }}}} Yooo {{{{ end }}}}" + ); + } + + let input = NomoInput::from(input); + + parsing.throughput(criterion::Throughput::Bytes(input.len() as u64)); + parsing.bench_with_input(BenchmarkId::from_parameter(size), &input, |b, input| { + b.iter(|| nomo::parser::parse(input.clone()).unwrap()); + }); + } +} + +criterion_group!(benches, parsing_benchmark, parsing_nested); criterion_main!(benches); diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 2b28a53..ee307ed 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -116,7 +116,6 @@ version = "0.1.0" dependencies = [ "annotate-snippets", "displaydoc", - "serde", "serde_json", "thiserror", "winnow", @@ -161,7 +160,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", - "serde_derive", ] [[package]]