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] 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()) {