Compare commits

..

No commits in common. "474324726a740e1576a0b7b1930448d1d14564af" and "3f549690c12f47e2bfbe2788376f1294a31662a5" have entirely different histories.

26 changed files with 429 additions and 1677 deletions

2
Cargo.lock generated
View file

@ -371,7 +371,6 @@ dependencies = [
"console", "console",
"globset", "globset",
"once_cell", "once_cell",
"serde",
"similar", "similar",
"tempfile", "tempfile",
"walkdir", "walkdir",
@ -440,6 +439,7 @@ dependencies = [
"criterion", "criterion",
"displaydoc", "displaydoc",
"insta", "insta",
"nomo",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",

View file

@ -13,21 +13,20 @@ debug = true
[dependencies] [dependencies]
annotate-snippets = "0.12.13" annotate-snippets = "0.12.13"
displaydoc = "0.2.5" displaydoc = "0.2.5"
serde_json = { version = "1.0.149", optional = true } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
thiserror = "2.0.18" thiserror = "2.0.18"
winnow = { version = "0.7.14", features = ["unstable-recover"] } winnow = { version = "0.7.14", features = ["unstable-recover"] }
[dev-dependencies] [dev-dependencies]
annotate-snippets = { version = "0.12.13", features = ["testing-colors"] } annotate-snippets = { version = "0.12.13", features = ["testing-colors"] }
criterion = "0.8.2" criterion = "0.8.2"
insta = { version = "1.46.3", features = ["glob", "serde"] } insta = { version = "1.46.3", features = ["glob"] }
serde = { version = "1.0.228", features = ["derive"] } nomo = { path = ".", features = ["serialize"] }
serde_json = "1.0.149"
[profile.dev.package] [profile.dev.package]
insta.opt-level = 3 insta.opt-level = 3
similar.opt-level = 3 similar.opt-level = 3
[features] [features]
default = ["serde_json"] serialize = []
serde_json = ["dep:serde_json"]

View file

@ -23,26 +23,5 @@ fn parsing_benchmark(c: &mut Criterion) {
} }
} }
fn parsing_nested(c: &mut Criterion) { criterion_group!(benches, parsing_benchmark);
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); criterion_main!(benches);

2
fuzz/Cargo.lock generated
View file

@ -116,6 +116,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"annotate-snippets", "annotate-snippets",
"displaydoc", "displaydoc",
"serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
"winnow", "winnow",
@ -160,6 +161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [ dependencies = [
"serde_core", "serde_core",
"serde_derive",
] ]
[[package]] [[package]]

View file

@ -217,32 +217,15 @@ pub fn parse(input: &[TemplateToken]) -> Result<TemplateAst<'_>, AstFailure> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum TemplateAstExpr<'input> { pub enum TemplateAstExpr<'input> {
StaticContent(TemplateToken), StaticContent(TemplateToken),
Block {
prev_whitespace_content: Option<TemplateToken>,
expression: Box<TemplateAstExpr<'input>>,
post_whitespace_content: Option<TemplateToken>,
},
Interpolation { Interpolation {
prev_whitespace_content: Option<TemplateToken>, prev_whitespace_content: Option<TemplateToken>,
expression: Box<TemplateAstExpr<'input>>, expression: Box<TemplateAstExpr<'input>>,
post_whitespace_content: Option<TemplateToken>, post_whitespace_content: Option<TemplateToken>,
}, },
VariableAccess(TemplateToken),
ConditionalChain { ConditionalChain {
chain: Vec<TemplateAstExpr<'input>>, chain: Vec<TemplateAstExpr<'input>>,
}, },
ForChain {
for_block: Box<TemplateAstExpr<'input>>,
content: Vec<TemplateAstExpr<'input>>,
else_block: Option<Box<TemplateAstExpr<'input>>>,
else_content: Option<Vec<TemplateAstExpr<'input>>>,
end_block: Box<TemplateAstExpr<'input>>,
},
For {
value_ident: TemplateToken,
value_expression: Box<TemplateAstExpr<'input>>,
},
ForElse,
VariableAccess(TemplateToken),
IfConditional { IfConditional {
expression: Box<TemplateAstExpr<'input>>, expression: Box<TemplateAstExpr<'input>>,
}, },
@ -254,6 +237,11 @@ pub enum TemplateAstExpr<'input> {
}, },
Invalid(&'input [TemplateToken]), Invalid(&'input [TemplateToken]),
EndBlock, EndBlock,
Block {
prev_whitespace_content: Option<TemplateToken>,
expression: Box<TemplateAstExpr<'input>>,
post_whitespace_content: Option<TemplateToken>,
},
} }
fn parse_asts<'input>(input: &mut Input<'input>) -> Result<Vec<TemplateAstExpr<'input>>, AstError> { fn parse_asts<'input>(input: &mut Input<'input>) -> Result<Vec<TemplateAstExpr<'input>>, AstError> {
@ -328,7 +316,6 @@ fn parse_action<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'in
"action", "action",
alt(( alt((
parse_conditional_chain, parse_conditional_chain,
parse_for_chain,
(parse_block( (parse_block(
cut_err(not(repeat_till( cut_err(not(repeat_till(
0.., 0..,
@ -349,67 +336,6 @@ fn parse_action<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'in
.parse_next(input) .parse_next(input)
} }
fn parse_for_chain<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, 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>( fn parse_conditional_chain<'input>(
input: &mut Input<'input>, input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> { ) -> Result<TemplateAstExpr<'input>, AstError> {
@ -521,18 +447,6 @@ fn parse_conditional_else<'input>(
.parse_next(input) .parse_next(input)
} }
fn parse_loop_else<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, 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<TemplateAstExpr<'input>, AstError> { fn parse_end<'input>(input: &mut Input<'input>) -> Result<TemplateAstExpr<'input>, AstError> {
trace( trace(
"end", "end",
@ -901,15 +815,4 @@ mod tests {
insta::assert_debug_snapshot!(ast); 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);
}
} }

View file

@ -1,61 +0,0 @@
---
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,
},
},
],
}

View file

@ -1,11 +1,8 @@
use std::collections::BTreeMap;
use crate::ast::TemplateAstExpr; use crate::ast::TemplateAstExpr;
use crate::input::NomoInput; use crate::input::NomoInput;
pub struct EmitMachine { pub struct EmitMachine {
current_index: usize, current_index: usize,
labels: BTreeMap<LabelSlot, usize>,
} }
impl EmitMachine { impl EmitMachine {
@ -18,35 +15,14 @@ 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, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct VariableSlot { pub struct VariableSlot {
index: usize, index: usize,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Debug)]
pub struct LabelSlot {
index: usize,
}
#[derive(Debug, Clone)]
pub enum Instruction { pub enum Instruction {
AppendContent { AppendContent {
content: NomoInput, content: NomoInput,
@ -64,54 +40,24 @@ pub enum Instruction {
Abort, Abort,
JumpIfNotTrue { JumpIfNotTrue {
emit_slot: VariableSlot, emit_slot: VariableSlot,
jump: LabelSlot, jump: isize,
}, },
Jump { Jump {
jump: LabelSlot, jump: isize,
}, },
NoOp, 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 fn emit_machine(input: crate::ast::TemplateAst<'_>) -> Vec<Instruction> {
pub struct VMInstructions {
pub labels: BTreeMap<LabelSlot, usize>,
pub instructions: Vec<Instruction>,
}
pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> VMInstructions {
let mut eval = vec![]; let mut eval = vec![];
let mut machine = EmitMachine { let mut machine = EmitMachine { current_index: 0 };
current_index: 0,
labels: BTreeMap::new(),
};
for ast in input.root() { for ast in input.root() {
emit_ast_expr(&mut machine, &mut eval, ast); emit_ast_expr(&mut machine, &mut eval, ast);
} }
VMInstructions { eval
labels: machine.labels,
instructions: eval,
}
} }
fn emit_ast_expr( fn emit_ast_expr(
@ -149,11 +95,10 @@ fn emit_ast_expr(
TemplateAstExpr::ConditionalChain { chain } => { TemplateAstExpr::ConditionalChain { chain } => {
let mut chain = chain.iter(); let mut chain = chain.iter();
let end_label = machine.reserve_label();
let mut end_indices = vec![]; let mut end_indices = vec![];
let mut previous_post_whitespace_content: &Option<crate::parser::TemplateToken> = &None; let mut previous_post_whitespace_content: &Option<crate::parser::TemplateToken> = &None;
let mut previous_jump: Option<LabelSlot> = None; let mut previous_jump: Option<usize> = None;
loop { loop {
let next = chain.next().unwrap(); let next = chain.next().unwrap();
@ -169,7 +114,7 @@ fn emit_ast_expr(
} }
end_indices.push(eval.len()); end_indices.push(eval.len());
eval.push(Instruction::Jump { jump: end_label }); eval.push(Instruction::Jump { jump: isize::MAX });
} else if let TemplateAstExpr::Block { } else if let TemplateAstExpr::Block {
prev_whitespace_content, prev_whitespace_content,
post_whitespace_content, post_whitespace_content,
@ -194,15 +139,20 @@ fn emit_ast_expr(
let emit_slot = machine.reserve_slot(); let emit_slot = machine.reserve_slot();
emit_expr_load(machine, eval, emit_slot, expression); emit_expr_load(machine, eval, emit_slot, expression);
let jmp_label = machine.reserve_label(); previous_jump = Some(eval.len());
previous_jump = Some(jmp_label);
eval.push(Instruction::JumpIfNotTrue { eval.push(Instruction::JumpIfNotTrue {
emit_slot, emit_slot,
jump: jmp_label, jump: isize::MAX,
}); });
} else if let TemplateAstExpr::ElseConditional { expression } = &**expression { } else if let TemplateAstExpr::ElseConditional { expression } = &**expression {
if let Some(previous_jump) = previous_jump.take() { if let Some(previous_jump) = previous_jump.take() {
machine.assign_label(previous_jump, eval.len()); 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;
} else { } else {
panic!("Got an else without a previous if?"); panic!("Got an else without a previous if?");
} }
@ -211,11 +161,10 @@ fn emit_ast_expr(
let emit_slot = machine.reserve_slot(); let emit_slot = machine.reserve_slot();
emit_expr_load(machine, eval, emit_slot, expression); emit_expr_load(machine, eval, emit_slot, expression);
let jmp_label = machine.reserve_label(); previous_jump = Some(eval.len());
previous_jump = Some(jmp_label);
eval.push(Instruction::JumpIfNotTrue { eval.push(Instruction::JumpIfNotTrue {
emit_slot, emit_slot,
jump: jmp_label, jump: isize::MAX,
}); });
} else { } else {
// We don't have to do anything in the else case // We don't have to do anything in the else case
@ -227,9 +176,13 @@ fn emit_ast_expr(
} }
if let Some(previous_jump) = previous_jump.take() { if let Some(previous_jump) = previous_jump.take() {
machine.assign_label(previous_jump, eval.len()); 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(end_label, eval.len());
if let Some(ws) = previous_post_whitespace_content { if let Some(ws) = previous_post_whitespace_content {
eval.push(Instruction::AppendContent { eval.push(Instruction::AppendContent {
@ -238,159 +191,12 @@ fn emit_ast_expr(
} else { } else {
eval.push(Instruction::NoOp); 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 { for index in end_indices {
inherit_parent: true, let jump = eval.len() - index - 1;
}); eval[index] = Instruction::Jump {
jump: jump as isize,
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");
} }
} }
@ -399,8 +205,6 @@ fn emit_ast_expr(
| TemplateAstExpr::IfConditional { .. } | TemplateAstExpr::IfConditional { .. }
| TemplateAstExpr::ConditionalContent { .. } | TemplateAstExpr::ConditionalContent { .. }
| TemplateAstExpr::ElseConditional { .. } | TemplateAstExpr::ElseConditional { .. }
| TemplateAstExpr::For { .. }
| TemplateAstExpr::ForElse
| TemplateAstExpr::Invalid { .. } | TemplateAstExpr::Invalid { .. }
| TemplateAstExpr::VariableAccess { .. } => eval.push(Instruction::Abort), | TemplateAstExpr::VariableAccess { .. } => eval.push(Instruction::Abort),
} }
@ -427,10 +231,6 @@ fn emit_expr_load(
TemplateAstExpr::ElseConditional { .. } => todo!(), TemplateAstExpr::ElseConditional { .. } => todo!(),
TemplateAstExpr::EndBlock => todo!(), TemplateAstExpr::EndBlock => todo!(),
TemplateAstExpr::Block { .. } => todo!(), TemplateAstExpr::Block { .. } => todo!(),
TemplateAstExpr::ForChain { .. } => todo!(),
TemplateAstExpr::For { .. } => todo!(),
TemplateAstExpr::ForElse => todo!(),
TemplateAstExpr::IfConditional { .. } => todo!(), TemplateAstExpr::IfConditional { .. } => todo!(),
TemplateAstExpr::ConditionalContent { .. } => todo!(), TemplateAstExpr::ConditionalContent { .. } => todo!(),
} }
@ -451,28 +251,25 @@ mod tests {
let emit = emit_machine(ast); let emit = emit_machine(ast);
insta::assert_debug_snapshot!(emit, @r#" insta::assert_debug_snapshot!(emit, @r#"
VMInstructions { [
labels: {}, AppendContent {
instructions: [ content: "Hello" (0..5),
AppendContent { },
content: "Hello" (0..5), AppendContent {
content: " " (5..6),
},
LoadFromContextToSlot {
name: "world" (10..15),
slot: VariableSlot {
index: 0,
}, },
AppendContent { },
content: " " (5..6), EmitFromSlot {
slot: VariableSlot {
index: 0,
}, },
LoadFromContextToSlot { },
name: "world" (10..15), ]
slot: VariableSlot {
index: 0,
},
},
EmitFromSlot {
slot: VariableSlot {
index: 0,
},
},
],
}
"#); "#);
} }

View file

@ -2,98 +2,75 @@
source: src/emit/mod.rs source: src/emit/mod.rs
expression: emit expression: emit
--- ---
VMInstructions { [
labels: { LoadFromContextToSlot {
LabelSlot { name: "foo" (6..9),
slot: VariableSlot {
index: 0, index: 0,
}: 19, },
LabelSlot {
index: 2,
}: 7,
LabelSlot {
index: 4,
}: 14,
}, },
instructions: [ JumpIfNotTrue {
LoadFromContextToSlot { emit_slot: VariableSlot {
name: "foo" (6..9), index: 0,
slot: VariableSlot {
index: 1,
},
}, },
JumpIfNotTrue { jump: 5,
emit_slot: VariableSlot { },
index: 1, AppendContent {
}, content: " " (12..13),
jump: LabelSlot { },
index: 2, 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 { jump: 5,
content: "foo" (13..16), },
}, AppendContent {
AppendContent { content: " " (34..35),
content: " " (16..17), },
}, AppendContent {
Jump { content: "bar" (35..38),
jump: LabelSlot { },
index: 0, AppendContent {
}, content: " " (38..39),
}, },
AppendContent { Jump {
content: " " (12..13), jump: 7,
}, },
LoadFromContextToSlot { AppendContent {
name: "bar" (28..31), content: " " (34..35),
slot: VariableSlot { },
index: 3, AppendContent {
}, content: " " (49..50),
}, },
JumpIfNotTrue { AppendContent {
emit_slot: VariableSlot { content: "foobar" (50..56),
index: 3, },
}, AppendContent {
jump: LabelSlot { content: " " (56..57),
index: 4, },
}, Jump {
}, jump: 2,
AppendContent { },
content: " " (34..35), AppendContent {
}, content: " " (49..50),
AppendContent { },
content: "bar" (35..38), NoOp,
}, ]
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,
],
}

View file

@ -5,10 +5,7 @@ use thiserror::Error;
use crate::Context; use crate::Context;
use crate::emit::Instruction; use crate::emit::Instruction;
use crate::emit::VMInstructions;
use crate::emit::VariableSlot;
use crate::input::NomoInput; use crate::input::NomoInput;
use crate::value::NomoValue;
#[derive(Debug, Error, Display)] #[derive(Debug, Error, Display)]
pub enum EvaluationError { pub enum EvaluationError {
@ -20,165 +17,67 @@ pub enum EvaluationError {
** **
** This is an internal error and is a bug that should be reported ** This is an internal error and is a bug that should be reported
*/ */
LabelNotFound, InstructionPointerOverflow,
} }
struct Scope { pub fn execute(
stack: Vec<HashMap<String, NomoValue>>, instructions: &[Instruction],
slots: HashMap<VariableSlot, NomoValue>, global_context: &Context,
} ) -> Result<String, EvaluationError> {
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<String, EvaluationError> {
let mut output = String::new(); let mut output = String::new();
let mut scopes = Scope { let mut scopes: HashMap<crate::emit::VariableSlot, serde_json::Value> = HashMap::new();
stack: vec![global_context.values().clone()],
slots: HashMap::new(),
};
let mut ip = 0; let mut ip = 0;
loop { loop {
if ip >= vm.instructions.len() { if ip >= instructions.len() {
break; break;
} }
let instr = vm.instructions.get(ip).unwrap(); let instr = instructions.get(ip).unwrap();
match instr { match instr {
Instruction::NoOp => (), Instruction::NoOp => (),
Instruction::AppendContent { content } => output.push_str(content), Instruction::AppendContent { content } => output.push_str(content),
Instruction::LoadFromContextToSlot { name, slot } => { Instruction::LoadFromContextToSlot { name, slot } => {
let value = scopes let value = global_context
.get_scoped(name) .values
.get(name.as_str())
.ok_or(EvaluationError::UnknownVariable(name.clone()))?; .ok_or(EvaluationError::UnknownVariable(name.clone()))?;
scopes.insert_into_slot(*slot, value.clone()); scopes.insert(*slot, value.clone());
} }
Instruction::EmitFromSlot { slot } => { Instruction::EmitFromSlot { slot } => {
let value = scopes.get(slot).as_str().unwrap(); let value = scopes.get(slot).unwrap().as_str().unwrap();
output.push_str(value); output.push_str(value);
} }
Instruction::PushScope { inherit_parent: _ } => { Instruction::PushScope { inherit_parent: _ } => todo!(),
scopes.push_scope();
}
Instruction::Abort => return Err(EvaluationError::ExplicitAbort), Instruction::Abort => return Err(EvaluationError::ExplicitAbort),
Instruction::JumpIfNotTrue { emit_slot, jump } => { Instruction::JumpIfNotTrue { emit_slot, jump } => {
let dont_jump = scopes.get(emit_slot).as_bool().unwrap(); let dont_jump = scopes.get(emit_slot).unwrap().as_bool().unwrap();
if dont_jump { if dont_jump {
// We are done // We are done
} else { } else {
let Some(new_ip) = vm.labels.get(jump) else { let (new_ip, overflow) = ip.overflowing_add_signed(*jump);
return Err(EvaluationError::LabelNotFound);
};
ip = *new_ip; if overflow {
continue; return Err(EvaluationError::InstructionPointerOverflow);
} else {
ip = new_ip;
continue;
}
} }
} }
Instruction::Jump { jump } => { Instruction::Jump { jump } => {
let Some(new_ip) = vm.labels.get(jump) else { let (new_ip, overflow) = ip.overflowing_add_signed(*jump);
return Err(EvaluationError::LabelNotFound);
};
ip = *new_ip; if overflow {
continue; return Err(EvaluationError::InstructionPointerOverflow);
}
Instruction::CreateIteratorFromSlotToSlot {
iterator_slot,
iterator_source_slot,
} => {
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 { } else {
let Some(new_ip) = vm.labels.get(jump) else { ip = new_ip;
return Err(EvaluationError::LabelNotFound);
};
ip = *new_ip;
continue; continue;
} }
} }
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;
continue;
}
}
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; ip += 1;

View file

@ -1,19 +1,18 @@
use std::collections::BTreeMap;
use std::collections::HashMap; use std::collections::HashMap;
use displaydoc::Display; use displaydoc::Display;
use serde::Serialize;
use thiserror::Error; use thiserror::Error;
use crate::emit::VMInstructions; use crate::emit::Instruction;
use crate::input::NomoInput; use crate::input::NomoInput;
use crate::value::NomoValue;
use crate::value::NomoValueError;
pub mod ast; pub mod ast;
pub mod emit; pub mod emit;
pub mod eval; pub mod eval;
pub mod input; pub mod input;
pub mod parser; pub mod parser;
pub mod value;
#[derive(Debug, Error, Display)] #[derive(Debug, Error, Display)]
pub enum NomoError { pub enum NomoError {
@ -85,11 +84,11 @@ impl Nomo {
} }
struct Template { struct Template {
instructions: VMInstructions, instructions: Vec<Instruction>,
} }
pub struct Context { pub struct Context {
values: HashMap<String, NomoValue>, values: BTreeMap<String, serde_json::Value>,
} }
impl Default for Context { impl Default for Context {
@ -101,26 +100,23 @@ impl Default for Context {
impl Context { impl Context {
pub fn new() -> Context { pub fn new() -> Context {
Context { Context {
values: HashMap::new(), values: BTreeMap::new(),
} }
} }
pub fn try_insert( pub fn try_insert(
&mut self, &mut self,
key: impl Into<String>, key: impl Into<String>,
value: impl TryInto<NomoValue, Error = NomoValueError>, value: impl Serialize,
) -> Result<(), NomoValueError> { ) -> Result<(), serde_json::Error> {
self.values.insert(key.into(), value.try_into()?); self.values.insert(key.into(), serde_json::to_value(value)?);
Ok(()) Ok(())
} }
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<NomoValue>) { pub fn insert(&mut self, key: impl Into<String>, value: impl Serialize) {
self.values.insert(key.into(), value.into()); self.try_insert(key, value)
} .expect("inserted value should serialize without error");
pub fn values(&self) -> &HashMap<String, NomoValue> {
&self.values
} }
} }

View file

@ -209,8 +209,6 @@ pub enum TokenKind {
Invalid, Invalid,
ConditionalIf, ConditionalIf,
ConditionalElse, ConditionalElse,
For,
In,
End, End,
Literal(TokenLiteral), Literal(TokenLiteral),
} }
@ -297,8 +295,6 @@ impl TemplateToken {
invalid => TokenKind::Invalid, invalid => TokenKind::Invalid,
conditional_if => TokenKind::ConditionalIf, conditional_if => TokenKind::ConditionalIf,
conditional_else => TokenKind::ConditionalElse, conditional_else => TokenKind::ConditionalElse,
keyword_for => TokenKind::For,
keyword_in => TokenKind::In,
end => TokenKind::End, end => TokenKind::End,
} }
@ -385,7 +381,14 @@ fn parse_interpolate<'input>(input: &mut Input<'input>) -> PResult<'input, Vec<T
fn parse_block_token<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { fn parse_block_token<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> {
trace( trace(
"parse_block_token", "parse_block_token",
alt((parse_ident, parse_keyword, parse_whitespace)), 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,
)),
) )
.parse_next(input) .parse_next(input)
} }
@ -428,26 +431,6 @@ fn parse_end<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken
trace("parse_end", "end".map(TemplateToken::end)).parse_next(input) 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> { fn parse_whitespace<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> {
trace( trace(
"parse_whitespace", "parse_whitespace",
@ -475,9 +458,14 @@ fn parse_ident<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateTok
} }
fn ident<'input>(input: &mut Input<'input>) -> PResult<'input, NomoInput> { fn ident<'input>(input: &mut Input<'input>) -> PResult<'input, NomoInput> {
peek(not(parse_keyword)) peek(not(alt((
.context(ParseError::ctx().msg("Expected an ident, but found a literal instead")) parse_literal,
.parse_next(input)?; parse_condition_if,
parse_condition_else,
parse_end,
))))
.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) take_while(1.., |c: char| c.is_alphanumeric() || "_".contains(c)).parse_next(input)
} }
@ -639,45 +627,4 @@ 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),
],
},
)
"#);
}
} }

View file

@ -1,251 +0,0 @@
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<NomoValue>,
},
Bool {
value: bool,
},
Object {
value: BTreeMap<String, NomoValue>,
},
Integer {
value: u64,
},
SignedInteger {
value: i64,
},
Float {
value: f64,
},
Iterator {
value: Box<dyn CloneIterator<Item = NomoValue>>,
},
}
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<bool> {
if let Self::Bool { value } = self {
Some(*value)
} else {
None
}
}
pub fn as_object(&self) -> Option<&BTreeMap<String, NomoValue>> {
if let Self::Object { value } = self {
Some(value)
} else {
None
}
}
pub fn as_integer(&self) -> Option<u64> {
if let Self::Integer { value } = self {
Some(*value)
} else {
None
}
}
pub fn as_float(&self) -> Option<f64> {
if let Self::Float { value } = self {
Some(*value)
} else {
None
}
}
pub fn as_iterator(&self) -> Option<&dyn CloneIterator<Item = NomoValue>> {
if let Self::Iterator { value } = self {
Some(value)
} else {
None
}
}
pub fn as_iterator_mut(&mut self) -> Option<&mut dyn CloneIterator<Item = NomoValue>> {
if let Self::Iterator { value } = self {
Some(value)
} else {
None
}
}
}
pub trait CloneIterator: Iterator<Item = NomoValue> {
fn clone_box(&self) -> Box<dyn CloneIterator<Item = NomoValue>>;
}
impl<I> CloneIterator for I
where
I: Iterator<Item = NomoValue> + Clone + 'static,
{
fn clone_box(&self) -> Box<dyn CloneIterator<Item = NomoValue>> {
Box::new(Clone::clone(self))
}
}
impl Clone for Box<dyn CloneIterator> {
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<R> From<&R> for NomoValue
where
R: Into<NomoValue> + Clone,
{
fn from(value: &R) -> Self {
value.clone().into()
}
}
impl From<String> 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<V> From<Vec<V>> for NomoValue
where
V: Into<NomoValue>,
{
fn from(val: Vec<V>) -> Self {
NomoValue::Array {
value: val.into_iter().map(Into::into).collect(),
}
}
}
impl<V> From<std::collections::VecDeque<V>> for NomoValue
where
V: Into<NomoValue>,
{
fn from(val: std::collections::VecDeque<V>) -> Self {
NomoValue::Array {
value: val.into_iter().map(Into::into).collect(),
}
}
}
impl<V> From<&[V]> for NomoValue
where
V: Into<NomoValue> + Clone,
{
fn from(value: &[V]) -> Self {
NomoValue::Array {
value: value.iter().cloned().map(Into::into).collect(),
}
}
}
impl<K, V> From<std::collections::HashMap<K, V>> for NomoValue
where
K: Into<String>,
V: Into<NomoValue>,
{
fn from(val: std::collections::HashMap<K, V>) -> 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<serde_json::Value> for NomoValue {
type Error = NomoValueError;
fn try_from(value: serde_json::Value) -> Result<Self, NomoValueError> {
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::<Result<_, _>>()?,
}),
serde_json::Value::Object(_map) => todo!(),
}
}
}

View file

@ -1,80 +0,0 @@
---
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
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),
[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),
],
}

View file

@ -1,90 +0,0 @@
---
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
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: 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,
post_whitespace_content: None,
},
},
],
}

View file

@ -3,60 +3,46 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/condition.nomo input_file: tests/cases/condition.nomo
--- ---
VMInstructions { [
labels: { LoadFromContextToSlot {
LabelSlot { name: "test" (6..10),
slot: VariableSlot {
index: 0, index: 0,
}: 7, },
LabelSlot {
index: 2,
}: 7,
}, },
instructions: [ JumpIfNotTrue {
LoadFromContextToSlot { emit_slot: VariableSlot {
name: "test" (6..10), index: 0,
slot: VariableSlot {
index: 1,
},
}, },
JumpIfNotTrue { jump: 5,
emit_slot: VariableSlot { },
index: 1, AppendContent {
}, content: "\n " (13..18),
jump: LabelSlot { },
index: 2, 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,
},
},
],
}

View file

@ -3,89 +3,86 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/identifiers.nomo input_file: tests/cases/identifiers.nomo
--- ---
VMInstructions { [
labels: {}, LoadFromContextToSlot {
instructions: [ name: "_name" (4..9),
LoadFromContextToSlot { slot: VariableSlot {
name: "_name" (4..9), index: 0,
slot: VariableSlot {
index: 0,
},
}, },
EmitFromSlot { },
slot: VariableSlot { EmitFromSlot {
index: 0, slot: VariableSlot {
}, index: 0,
}, },
AppendContent { },
content: "\n" (12..13), AppendContent {
content: "\n" (12..13),
},
LoadFromContextToSlot {
name: "a_name" (17..23),
slot: VariableSlot {
index: 1,
}, },
LoadFromContextToSlot { },
name: "a_name" (17..23), EmitFromSlot {
slot: VariableSlot { slot: VariableSlot {
index: 1, index: 1,
},
}, },
EmitFromSlot { },
slot: VariableSlot { AppendContent {
index: 1, content: "\n" (26..27),
}, },
LoadFromContextToSlot {
name: "1name" (31..36),
slot: VariableSlot {
index: 2,
}, },
AppendContent { },
content: "\n" (26..27), EmitFromSlot {
slot: VariableSlot {
index: 2,
}, },
LoadFromContextToSlot { },
name: "1name" (31..36), AppendContent {
slot: VariableSlot { content: "\n" (39..40),
index: 2, },
}, LoadFromContextToSlot {
name: "_name1" (44..50),
slot: VariableSlot {
index: 3,
}, },
EmitFromSlot { },
slot: VariableSlot { EmitFromSlot {
index: 2, slot: VariableSlot {
}, index: 3,
}, },
AppendContent { },
content: "\n" (39..40), AppendContent {
content: "\n" (53..54),
},
LoadFromContextToSlot {
name: "_namE" (58..63),
slot: VariableSlot {
index: 4,
}, },
LoadFromContextToSlot { },
name: "_name1" (44..50), EmitFromSlot {
slot: VariableSlot { slot: VariableSlot {
index: 3, index: 4,
},
}, },
EmitFromSlot { },
slot: VariableSlot { AppendContent {
index: 3, content: "\n" (66..67),
}, },
LoadFromContextToSlot {
name: "name1" (71..76),
slot: VariableSlot {
index: 5,
}, },
AppendContent { },
content: "\n" (53..54), EmitFromSlot {
slot: VariableSlot {
index: 5,
}, },
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,
},
},
],
}

View file

@ -3,81 +3,60 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/if_else_if.nomo input_file: tests/cases/if_else_if.nomo
--- ---
VMInstructions { [
labels: { LoadFromContextToSlot {
LabelSlot { name: "test" (6..10),
slot: VariableSlot {
index: 0, index: 0,
}: 14, },
LabelSlot {
index: 2,
}: 7,
LabelSlot {
index: 4,
}: 14,
}, },
instructions: [ JumpIfNotTrue {
LoadFromContextToSlot { emit_slot: VariableSlot {
name: "test" (6..10), index: 0,
slot: VariableSlot {
index: 1,
},
}, },
JumpIfNotTrue { jump: 5,
emit_slot: VariableSlot { },
index: 1, AppendContent {
}, content: "\n " (13..18),
jump: LabelSlot { },
index: 2, 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 { jump: 5,
content: "Not Hello World! :C" (18..37), },
}, AppendContent {
AppendContent { content: "\n " (64..69),
content: "\n" (37..38), },
}, AppendContent {
Jump { content: "Hello World!" (69..81),
jump: LabelSlot { },
index: 0, AppendContent {
}, content: "\n" (81..82),
}, },
AppendContent { Jump {
content: "\n " (13..18), jump: 2,
}, },
LoadFromContextToSlot { AppendContent {
name: "another_test" (49..61), content: "\n " (64..69),
slot: VariableSlot { },
index: 3, NoOp,
}, ]
},
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,
],
}

View file

@ -3,25 +3,22 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/interpolation.nomo input_file: tests/cases/interpolation.nomo
--- ---
VMInstructions { [
labels: {}, AppendContent {
instructions: [ content: "Hello! I'm" (0..10),
AppendContent { },
content: "Hello! I'm" (0..10), AppendContent {
content: " " (10..11),
},
LoadFromContextToSlot {
name: "name" (15..19),
slot: VariableSlot {
index: 0,
}, },
AppendContent { },
content: " " (10..11), EmitFromSlot {
slot: VariableSlot {
index: 0,
}, },
LoadFromContextToSlot { },
name: "name" (15..19), ]
slot: VariableSlot {
index: 0,
},
},
EmitFromSlot {
slot: VariableSlot {
index: 0,
},
},
],
}

View file

@ -3,39 +3,36 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/multiple.nomo input_file: tests/cases/multiple.nomo
--- ---
VMInstructions { [
labels: {}, AppendContent {
instructions: [ content: "Hi there! My name is" (0..20),
AppendContent { },
content: "Hi there! My name is" (0..20), AppendContent {
content: " " (20..21),
},
LoadFromContextToSlot {
name: "name" (25..29),
slot: VariableSlot {
index: 0,
}, },
AppendContent { },
content: " " (20..21), EmitFromSlot {
slot: VariableSlot {
index: 0,
}, },
LoadFromContextToSlot { },
name: "name" (25..29), AppendContent {
slot: VariableSlot { content: " " (32..33),
index: 0, },
}, LoadFromContextToSlot {
name: "lastname" (37..45),
slot: VariableSlot {
index: 1,
}, },
EmitFromSlot { },
slot: VariableSlot { EmitFromSlot {
index: 0, slot: VariableSlot {
}, index: 1,
}, },
AppendContent { },
content: " " (32..33), ]
},
LoadFromContextToSlot {
name: "lastname" (37..45),
slot: VariableSlot {
index: 1,
},
},
EmitFromSlot {
slot: VariableSlot {
index: 1,
},
},
],
}

View file

@ -3,11 +3,8 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/simple.nomo input_file: tests/cases/simple.nomo
--- ---
VMInstructions { [
labels: {}, AppendContent {
instructions: [ content: "Hello World!" (0..12),
AppendContent { },
content: "Hello World!" (0..12), ]
},
],
}

View file

@ -1,166 +0,0 @@
---
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
input_file: tests/cases/simple_for.nomo
---
VMInstructions {
labels: {
LabelSlot {
index: 0,
}: 10,
LabelSlot {
index: 1,
}: 10,
LabelSlot {
index: 2,
}: 4,
LabelSlot {
index: 7,
}: 22,
LabelSlot {
index: 8,
}: 23,
LabelSlot {
index: 9,
}: 16,
},
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,
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,
],
}

View file

@ -3,52 +3,38 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/trim_whitespace.nomo input_file: tests/cases/trim_whitespace.nomo
--- ---
VMInstructions { [
labels: { LoadFromContextToSlot {
LabelSlot { name: "test" (6..10),
slot: VariableSlot {
index: 0, index: 0,
}: 7, },
LabelSlot {
index: 2,
}: 7,
}, },
instructions: [ JumpIfNotTrue {
LoadFromContextToSlot { emit_slot: VariableSlot {
name: "test" (6..10), index: 0,
slot: VariableSlot {
index: 1,
},
}, },
JumpIfNotTrue { jump: 5,
emit_slot: VariableSlot { },
index: 1, AppendContent {
}, content: "Hello" (19..24),
jump: LabelSlot { },
index: 2, 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,
LoadFromContextToSlot { },
name: "stuff" (29..34), NoOp,
slot: VariableSlot { ]
index: 3,
},
},
EmitFromSlot {
slot: VariableSlot {
index: 3,
},
},
Jump {
jump: LabelSlot {
index: 0,
},
},
NoOp,
],
}

View file

@ -3,4 +3,4 @@ source: tests/file_tests.rs
expression: output expression: output
input_file: tests/cases/if_else_if.nomo input_file: tests/cases/if_else_if.nomo
--- ---
"\n Hello World!\n" "\n \n Hello World!\n"

View file

@ -1,13 +0,0 @@
---
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\nNo Values >:C"

View file

@ -1,13 +0,0 @@
{
"values": [ "one", "two" ],
"no_values": []
}
---
{{ for value in values -}}
{{-= value }}
{{- end }}
{{ for value in no_values -}}
{{-= value }}
{{ else -}}
No Values >:C
{{- end }}

View file

@ -2,12 +2,6 @@ use std::collections::HashMap;
use nomo::Context; use nomo::Context;
#[derive(serde::Serialize)]
struct Info {
input: String,
context: HashMap<String, serde_json::Value>,
}
#[test] #[test]
fn check_cases() { fn check_cases() {
insta::glob!("cases/*.nomo", |path| { insta::glob!("cases/*.nomo", |path| {
@ -15,6 +9,7 @@ fn check_cases() {
settings.set_snapshot_path("cases"); settings.set_snapshot_path("cases");
settings.set_snapshot_suffix(path.file_stem().unwrap().display().to_string()); settings.set_snapshot_suffix(path.file_stem().unwrap().display().to_string());
settings.set_prepend_module_to_snapshot(false); settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
let input = std::fs::read_to_string(path).unwrap(); let input = std::fs::read_to_string(path).unwrap();
@ -26,21 +21,14 @@ fn check_cases() {
HashMap::new() HashMap::new()
}; };
settings.set_info(&Info {
input: input.to_string(),
context: map.clone(),
});
let mut context = Context::new(); let mut context = Context::new();
for (k, v) in map { for (k, v) in map {
context.try_insert(k, v).unwrap(); context.insert(k, v);
} }
let parsed = nomo::parser::parse(input.into()).unwrap(); let parsed = nomo::parser::parse(input.into()).unwrap();
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!("1-parsed", parsed); insta::assert_debug_snapshot!("1-parsed", parsed);
let ast = match nomo::ast::parse(parsed.tokens()) { let ast = match nomo::ast::parse(parsed.tokens()) {