Compare commits

..

4 commits

Author SHA1 Message Date
6a233e978f Patch in content of previous block if it exists
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-09 13:08:15 +01:00
ae379df9db Also fix the jump if its the last one
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-09 13:04:16 +01:00
ff308649b9 Add if else if chains
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-09 12:51:49 +01:00
ef02e94591 Add tests for if/else if/else/if
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-09 11:40:12 +01:00
14 changed files with 578 additions and 100 deletions

View file

@ -227,7 +227,7 @@ pub enum TemplateAstExpr<'input> {
chain: Vec<TemplateAstExpr<'input>>, chain: Vec<TemplateAstExpr<'input>>,
}, },
IfConditional { IfConditional {
if_block: Box<TemplateAstExpr<'input>>, expression: Box<TemplateAstExpr<'input>>,
}, },
ConditionalContent { ConditionalContent {
content: Vec<TemplateAstExpr<'input>>, content: Vec<TemplateAstExpr<'input>>,
@ -316,10 +316,10 @@ fn parse_conditional_chain<'input>(
input: &mut Input<'input>, input: &mut Input<'input>,
) -> Result<TemplateAstExpr<'input>, AstError> { ) -> Result<TemplateAstExpr<'input>, AstError> {
trace("conditional_chain", |input: &mut Input<'input>| { trace("conditional_chain", |input: &mut Input<'input>| {
let if_block = parse_conditional_if.map(Box::new).parse_next(input)?; let if_block = parse_conditional_if.parse_next(input)?;
let mut chain = vec![]; let mut chain = vec![];
chain.push(TemplateAstExpr::IfConditional { if_block }); chain.push(if_block);
loop { loop {
let (content, end_block): (Vec<_>, _) = repeat_till( let (content, end_block): (Vec<_>, _) = repeat_till(
@ -344,7 +344,7 @@ fn parse_conditional_chain<'input>(
chain.push(end_block); chain.push(end_block);
if dbg!(is_end) { if is_end {
break; break;
} }
} }
@ -359,13 +359,17 @@ fn parse_conditional_if<'input>(
) -> Result<TemplateAstExpr<'input>, AstError> { ) -> Result<TemplateAstExpr<'input>, AstError> {
trace( trace(
"conditional", "conditional",
parse_block(preceded( parse_block(
TokenKind::ConditionalIf, preceded(
cut_err( TokenKind::ConditionalIf,
surrounded(ws, parse_value_expression) cut_err(
.context(AstError::ctx().msg("Expected an expression after 'if'")), surrounded(ws, parse_value_expression)
), .map(Box::new)
)), .context(AstError::ctx().msg("Expected an expression after 'if'")),
),
)
.map(|expression| TemplateAstExpr::IfConditional { expression }),
),
) )
.parse_next(input) .parse_next(input)
} }
@ -559,16 +563,16 @@ mod tests {
root: [ root: [
ConditionalChain { ConditionalChain {
chain: [ chain: [
IfConditional { Block {
if_block: Block { prev_whitespace_content: None,
prev_whitespace_content: None, expression: IfConditional {
expression: VariableAccess( expression: VariableAccess(
[Ident]"foo" (6..9), [Ident]"foo" (6..9),
), ),
post_whitespace_content: Some(
[Whitespace]" " (12..13),
),
}, },
post_whitespace_content: Some(
[Whitespace]" " (12..13),
),
}, },
ConditionalContent { ConditionalContent {
content: [ content: [
@ -675,11 +679,61 @@ mod tests {
} }
#[test] #[test]
fn check_empty_output() { fn check_empty_if_output() {
let input = "{{ if foo }}{{ end }}"; let input = "{{ if foo }}{{ end }}";
let parsed = crate::parser::parse(input.into()).unwrap(); let parsed = crate::parser::parse(input.into()).unwrap();
panic_pretty(input, parse(parsed.tokens())); let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!(ast, @r#"
TemplateAst {
root: [
ConditionalChain {
chain: [
Block {
prev_whitespace_content: None,
expression: IfConditional {
expression: VariableAccess(
[Ident]"foo" (6..9),
),
},
post_whitespace_content: None,
},
ConditionalContent {
content: [],
},
Block {
prev_whitespace_content: None,
expression: EndBlock,
post_whitespace_content: None,
},
],
},
],
}
"#);
}
#[test]
fn check_if_else() {
let input = "{{ if foo }} foo {{ else }} bar {{ end }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!(ast);
}
#[test]
fn check_if_else_if() {
let input = "{{ if foo }} foo {{ else if bar }} bar {{ end }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = panic_pretty(input, parse(parsed.tokens()));
insta::assert_debug_snapshot!(ast);
} }
} }

View file

@ -0,0 +1,55 @@
---
source: src/ast/mod.rs
expression: ast
---
TemplateAst {
root: [
ConditionalChain {
chain: [
Block {
prev_whitespace_content: None,
expression: IfConditional {
expression: VariableAccess(
[Ident]"foo" (6..9),
),
},
post_whitespace_content: Some(
[Whitespace]" " (12..13),
),
},
ConditionalContent {
content: [
StaticContent(
[Content]"foo" (13..16),
),
],
},
Block {
prev_whitespace_content: Some(
[Whitespace]" " (16..17),
),
expression: ElseConditional {
expression: None,
},
post_whitespace_content: Some(
[Whitespace]" " (27..28),
),
},
ConditionalContent {
content: [
StaticContent(
[Content]"bar" (28..31),
),
],
},
Block {
prev_whitespace_content: Some(
[Whitespace]" " (31..32),
),
expression: EndBlock,
post_whitespace_content: None,
},
],
},
],
}

View file

@ -0,0 +1,59 @@
---
source: src/ast/mod.rs
expression: ast
---
TemplateAst {
root: [
ConditionalChain {
chain: [
Block {
prev_whitespace_content: None,
expression: IfConditional {
expression: VariableAccess(
[Ident]"foo" (6..9),
),
},
post_whitespace_content: Some(
[Whitespace]" " (12..13),
),
},
ConditionalContent {
content: [
StaticContent(
[Content]"foo" (13..16),
),
],
},
Block {
prev_whitespace_content: Some(
[Whitespace]" " (16..17),
),
expression: ElseConditional {
expression: Some(
VariableAccess(
[Ident]"bar" (28..31),
),
),
},
post_whitespace_content: Some(
[Whitespace]" " (34..35),
),
},
ConditionalContent {
content: [
StaticContent(
[Content]"bar" (35..38),
),
],
},
Block {
prev_whitespace_content: Some(
[Whitespace]" " (38..39),
),
expression: EndBlock,
post_whitespace_content: None,
},
],
},
],
}

View file

@ -6,31 +6,31 @@ TemplateAst {
root: [ root: [
ConditionalChain { ConditionalChain {
chain: [ chain: [
IfConditional { Block {
if_block: Block { prev_whitespace_content: None,
prev_whitespace_content: None, expression: IfConditional {
expression: VariableAccess( expression: VariableAccess(
[Ident]"foo" (6..9), [Ident]"foo" (6..9),
), ),
post_whitespace_content: Some(
[Whitespace]"\n " (12..25),
),
}, },
post_whitespace_content: Some(
[Whitespace]"\n " (12..25),
),
}, },
ConditionalContent { ConditionalContent {
content: [ content: [
ConditionalChain { ConditionalChain {
chain: [ chain: [
IfConditional { Block {
if_block: Block { prev_whitespace_content: None,
prev_whitespace_content: None, expression: IfConditional {
expression: VariableAccess( expression: VariableAccess(
[Ident]"bar" (31..34), [Ident]"bar" (31..34),
), ),
post_whitespace_content: Some(
[Whitespace]"\n " (37..54),
),
}, },
post_whitespace_content: Some(
[Whitespace]"\n " (37..54),
),
}, },
ConditionalContent { ConditionalContent {
content: [ content: [

View file

@ -42,6 +42,9 @@ pub enum Instruction {
emit_slot: VariableSlot, emit_slot: VariableSlot,
jump: isize, jump: isize,
}, },
Jump {
jump: isize,
},
} }
pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> Vec<Instruction> { pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> Vec<Instruction> {
@ -90,76 +93,105 @@ fn emit_ast_expr(
} }
TemplateAstExpr::ConditionalChain { chain } => { TemplateAstExpr::ConditionalChain { chain } => {
let mut chain = chain.iter(); let mut chain = chain.iter();
let Some(TemplateAstExpr::IfConditional {
if_block: expression,
}) = chain.next()
else {
unreachable!("First element in conditional chain should be an IfConditional");
};
let TemplateAstExpr::Block { let mut end_indices = vec![];
prev_whitespace_content,
expression,
post_whitespace_content,
} = expression.as_ref()
else {
unreachable!("The end of an IfConditional must be a Block");
};
let Some(TemplateAstExpr::ConditionalContent { content }) = chain.next() else { let mut previous_post_whitespace_content: &Option<crate::parser::TemplateToken> = &None;
unreachable!("The end of an IfConditional must be a Block"); let mut previous_jump: Option<usize> = None;
};
if let Some(ws) = prev_whitespace_content { loop {
let next = chain.next().unwrap();
if let Some(ws) = previous_post_whitespace_content {
eval.push(Instruction::AppendContent {
content: ws.source().clone(),
});
}
if let TemplateAstExpr::ConditionalContent { content } = &next {
for ast in content {
emit_ast_expr(machine, eval, ast);
}
end_indices.push(eval.len());
eval.push(Instruction::Jump { jump: isize::MAX });
} else if let TemplateAstExpr::Block {
prev_whitespace_content,
post_whitespace_content,
expression,
} = &next
{
previous_post_whitespace_content = post_whitespace_content;
if let Some(ws) = prev_whitespace_content {
eval.insert(
eval.len() - 2,
Instruction::AppendContent {
content: ws.source().clone(),
},
);
let index_index = end_indices.len() - 1;
end_indices[index_index] += 1;
}
if let TemplateAstExpr::IfConditional { expression } = &**expression {
let emit_slot = machine.reserve_slot();
emit_expr_load(machine, eval, emit_slot, expression);
previous_jump = Some(eval.len());
eval.push(Instruction::JumpIfNotTrue {
emit_slot,
jump: isize::MAX,
});
} 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;
} else {
panic!("Got an else without a previous if?");
}
if let Some(expression) = expression {
let emit_slot = machine.reserve_slot();
emit_expr_load(machine, eval, emit_slot, expression);
previous_jump = Some(eval.len());
eval.push(Instruction::JumpIfNotTrue {
emit_slot,
jump: isize::MAX,
});
} else {
// We don't have to do anything in the else case
}
} else if let TemplateAstExpr::EndBlock = &**expression {
break;
}
}
}
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;
}
if let Some(ws) = previous_post_whitespace_content {
eval.push(Instruction::AppendContent { eval.push(Instruction::AppendContent {
content: ws.source().clone(), content: ws.source().clone(),
}); });
} }
let emit_slot = machine.reserve_slot(); for index in end_indices {
emit_expr_load(machine, eval, emit_slot, expression); let jump = eval.len() - index - 1;
eval[index] = Instruction::Jump {
let index = eval.len(); jump: jump as isize,
eval.push(Instruction::JumpIfNotTrue { };
emit_slot,
jump: isize::MAX,
});
if let Some(ws) = post_whitespace_content {
eval.push(Instruction::AppendContent {
content: ws.source().clone(),
});
}
for ast in content {
emit_ast_expr(machine, eval, ast);
}
let Some(TemplateAstExpr::Block {
prev_whitespace_content,
post_whitespace_content,
..
}) = chain.last()
else {
unreachable!("The end of an IfConditional must be a End Block");
};
if let Some(ws) = prev_whitespace_content {
eval.push(Instruction::AppendContent {
content: ws.source().clone(),
});
}
let jump = eval.len() - index - 1;
eval[index] = Instruction::JumpIfNotTrue {
emit_slot,
jump: jump as isize,
};
if let Some(ws) = post_whitespace_content {
eval.push(Instruction::AppendContent {
content: ws.source().clone(),
});
} }
} }
@ -235,4 +267,17 @@ mod tests {
] ]
"#); "#);
} }
#[test]
fn check_if_else_if() {
let input = "{{ if foo }} foo {{ else if bar }} bar {{ else }} foobar {{ end }}";
let parsed = crate::parser::parse(input.into()).unwrap();
let ast = crate::ast::parse(parsed.tokens()).unwrap();
let emit = emit_machine(ast);
insta::assert_debug_snapshot!(emit);
}
} }

View file

@ -0,0 +1,75 @@
---
source: src/emit/mod.rs
expression: emit
---
[
LoadFromContextToSlot {
name: "foo" (6..9),
slot: VariableSlot {
index: 0,
},
},
JumpIfNotTrue {
emit_slot: VariableSlot {
index: 0,
},
jump: 5,
},
AppendContent {
content: " " (12..13),
},
AppendContent {
content: "foo" (13..16),
},
AppendContent {
content: " " (16..17),
},
Jump {
jump: 13,
},
AppendContent {
content: " " (12..13),
},
LoadFromContextToSlot {
name: "bar" (28..31),
slot: VariableSlot {
index: 1,
},
},
JumpIfNotTrue {
emit_slot: VariableSlot {
index: 1,
},
jump: 5,
},
AppendContent {
content: " " (34..35),
},
AppendContent {
content: "bar" (35..38),
},
AppendContent {
content: " " (38..39),
},
Jump {
jump: 6,
},
AppendContent {
content: " " (34..35),
},
AppendContent {
content: " " (49..50),
},
AppendContent {
content: "foobar" (50..56),
},
AppendContent {
content: " " (56..57),
},
Jump {
jump: 1,
},
AppendContent {
content: " " (49..50),
},
]

View file

@ -67,6 +67,16 @@ pub fn execute(
} }
} }
} }
Instruction::Jump { jump } => {
let (new_ip, overflow) = ip.overflowing_add_signed(*jump);
if overflow {
return Err(EvaluationError::InstructionPointerOverflow);
} else {
ip = new_ip;
continue;
}
}
} }
ip += 1; ip += 1;

View file

@ -0,0 +1,36 @@
---
source: tests/file_tests.rs
expression: parsed
input_file: tests/cases/if_else_if.nomo
---
ParsedTemplate {
tokens: [
[LeftDelim]"{{" (0..2),
[Whitespace]" " (2..3),
[ConditionalIf]"if" (3..5),
[Whitespace]" " (5..6),
[Ident]"test" (6..10),
[Whitespace]" " (10..11),
[RightDelim]"}}" (11..13),
[Whitespace]"\n " (13..18),
[Content]"Not Hello World! :C" (18..37),
[Whitespace]"\n" (37..38),
[LeftDelim]"{{" (38..40),
[Whitespace]" " (40..41),
[ConditionalElse]"else" (41..45),
[Whitespace]" " (45..46),
[ConditionalIf]"if" (46..48),
[Whitespace]" " (48..49),
[Ident]"another_test" (49..61),
[Whitespace]" " (61..62),
[RightDelim]"}}" (62..64),
[Whitespace]"\n " (64..69),
[Content]"Hello World!" (69..81),
[Whitespace]"\n" (81..82),
[LeftDelim]"{{" (82..84),
[Whitespace]" " (84..85),
[End]"end" (85..88),
[Whitespace]" " (88..89),
[RightDelim]"}}" (89..91),
],
}

View file

@ -7,16 +7,16 @@ TemplateAst {
root: [ root: [
ConditionalChain { ConditionalChain {
chain: [ chain: [
IfConditional { Block {
if_block: Block { prev_whitespace_content: None,
prev_whitespace_content: None, expression: IfConditional {
expression: VariableAccess( expression: VariableAccess(
[Ident]"test" (6..10), [Ident]"test" (6..10),
), ),
post_whitespace_content: Some(
[Whitespace]"\n " (13..18),
),
}, },
post_whitespace_content: Some(
[Whitespace]"\n " (13..18),
),
}, },
ConditionalContent { ConditionalContent {
content: [ content: [

View file

@ -0,0 +1,60 @@
---
source: tests/file_tests.rs
expression: ast
input_file: tests/cases/if_else_if.nomo
---
TemplateAst {
root: [
ConditionalChain {
chain: [
Block {
prev_whitespace_content: None,
expression: IfConditional {
expression: VariableAccess(
[Ident]"test" (6..10),
),
},
post_whitespace_content: Some(
[Whitespace]"\n " (13..18),
),
},
ConditionalContent {
content: [
StaticContent(
[Content]"Not Hello World! :C" (18..37),
),
],
},
Block {
prev_whitespace_content: Some(
[Whitespace]"\n" (37..38),
),
expression: ElseConditional {
expression: Some(
VariableAccess(
[Ident]"another_test" (49..61),
),
),
},
post_whitespace_content: Some(
[Whitespace]"\n " (64..69),
),
},
ConditionalContent {
content: [
StaticContent(
[Content]"Hello World!" (69..81),
),
],
},
Block {
prev_whitespace_content: Some(
[Whitespace]"\n" (81..82),
),
expression: EndBlock,
post_whitespace_content: None,
},
],
},
],
}

View file

@ -14,7 +14,7 @@ input_file: tests/cases/condition.nomo
emit_slot: VariableSlot { emit_slot: VariableSlot {
index: 0, index: 0,
}, },
jump: 3, jump: 5,
}, },
AppendContent { AppendContent {
content: "\n " (13..18), content: "\n " (13..18),
@ -25,6 +25,12 @@ input_file: tests/cases/condition.nomo
AppendContent { AppendContent {
content: "\n" (30..31), content: "\n" (30..31),
}, },
Jump {
jump: 2,
},
AppendContent {
content: "\n " (13..18),
},
AppendContent { AppendContent {
content: "\n\n" (40..42), content: "\n\n" (40..42),
}, },

View file

@ -0,0 +1,61 @@
---
source: tests/file_tests.rs
expression: emit
input_file: tests/cases/if_else_if.nomo
---
[
LoadFromContextToSlot {
name: "test" (6..10),
slot: VariableSlot {
index: 0,
},
},
JumpIfNotTrue {
emit_slot: VariableSlot {
index: 0,
},
jump: 5,
},
AppendContent {
content: "\n " (13..18),
},
AppendContent {
content: "Not Hello World! :C" (18..37),
},
AppendContent {
content: "\n" (37..38),
},
Jump {
jump: 8,
},
AppendContent {
content: "\n " (13..18),
},
LoadFromContextToSlot {
name: "another_test" (49..61),
slot: VariableSlot {
index: 1,
},
},
JumpIfNotTrue {
emit_slot: VariableSlot {
index: 1,
},
jump: 5,
},
AppendContent {
content: "\n " (64..69),
},
AppendContent {
content: "Hello World!" (69..81),
},
AppendContent {
content: "\n" (81..82),
},
Jump {
jump: 1,
},
AppendContent {
content: "\n " (64..69),
},
]

View file

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

View file

@ -0,0 +1,11 @@
{
"test": false,
"another_test": true,
"stuff": "more"
}
---
{{ if test }}
Not Hello World! :C
{{ else if another_test }}
Hello World!
{{ end }}