diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8e2a1a7..9f18f45 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -227,7 +227,7 @@ pub enum TemplateAstExpr<'input> { chain: Vec>, }, IfConditional { - if_block: Box>, + expression: Box>, }, ConditionalContent { content: Vec>, @@ -316,10 +316,10 @@ fn parse_conditional_chain<'input>( input: &mut Input<'input>, ) -> Result, AstError> { 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![]; - chain.push(TemplateAstExpr::IfConditional { if_block }); + chain.push(if_block); loop { let (content, end_block): (Vec<_>, _) = repeat_till( @@ -344,7 +344,7 @@ fn parse_conditional_chain<'input>( chain.push(end_block); - if dbg!(is_end) { + if is_end { break; } } @@ -359,13 +359,17 @@ fn parse_conditional_if<'input>( ) -> Result, AstError> { trace( "conditional", - parse_block(preceded( - TokenKind::ConditionalIf, - cut_err( - surrounded(ws, parse_value_expression) - .context(AstError::ctx().msg("Expected an expression after 'if'")), - ), - )), + parse_block( + preceded( + TokenKind::ConditionalIf, + cut_err( + 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) } @@ -559,16 +563,16 @@ mod tests { root: [ ConditionalChain { chain: [ - IfConditional { - if_block: Block { - prev_whitespace_content: None, + Block { + prev_whitespace_content: None, + expression: IfConditional { expression: VariableAccess( [Ident]"foo" (6..9), ), - post_whitespace_content: Some( - [Whitespace]" " (12..13), - ), }, + post_whitespace_content: Some( + [Whitespace]" " (12..13), + ), }, ConditionalContent { content: [ @@ -675,11 +679,61 @@ mod tests { } #[test] - fn check_empty_output() { + fn check_empty_if_output() { let input = "{{ if foo }}{{ end }}"; 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); } } diff --git a/src/ast/snapshots/nomo__ast__tests__check_if_else.snap b/src/ast/snapshots/nomo__ast__tests__check_if_else.snap new file mode 100644 index 0000000..0a3b739 --- /dev/null +++ b/src/ast/snapshots/nomo__ast__tests__check_if_else.snap @@ -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, + }, + ], + }, + ], +} diff --git a/src/ast/snapshots/nomo__ast__tests__check_if_else_if.snap b/src/ast/snapshots/nomo__ast__tests__check_if_else_if.snap new file mode 100644 index 0000000..3912e60 --- /dev/null +++ b/src/ast/snapshots/nomo__ast__tests__check_if_else_if.snap @@ -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, + }, + ], + }, + ], +} diff --git a/src/ast/snapshots/nomo__ast__tests__simple_if_ast.snap b/src/ast/snapshots/nomo__ast__tests__simple_if_ast.snap index 59d5426..3701cc2 100644 --- a/src/ast/snapshots/nomo__ast__tests__simple_if_ast.snap +++ b/src/ast/snapshots/nomo__ast__tests__simple_if_ast.snap @@ -6,31 +6,31 @@ TemplateAst { root: [ ConditionalChain { chain: [ - IfConditional { - if_block: Block { - prev_whitespace_content: None, + Block { + prev_whitespace_content: None, + expression: IfConditional { expression: VariableAccess( [Ident]"foo" (6..9), ), - post_whitespace_content: Some( - [Whitespace]"\n " (12..25), - ), }, + post_whitespace_content: Some( + [Whitespace]"\n " (12..25), + ), }, ConditionalContent { content: [ ConditionalChain { chain: [ - IfConditional { - if_block: Block { - prev_whitespace_content: None, + Block { + prev_whitespace_content: None, + expression: IfConditional { expression: VariableAccess( [Ident]"bar" (31..34), ), - post_whitespace_content: Some( - [Whitespace]"\n " (37..54), - ), }, + post_whitespace_content: Some( + [Whitespace]"\n " (37..54), + ), }, ConditionalContent { content: [ diff --git a/src/emit/mod.rs b/src/emit/mod.rs index 6f38a91..95a92e6 100644 --- a/src/emit/mod.rs +++ b/src/emit/mod.rs @@ -42,6 +42,9 @@ pub enum Instruction { emit_slot: VariableSlot, jump: isize, }, + Jump { + jump: isize, + }, } pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> Vec { @@ -90,76 +93,105 @@ fn emit_ast_expr( } TemplateAstExpr::ConditionalChain { chain } => { 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 { - prev_whitespace_content, - expression, - post_whitespace_content, - } = expression.as_ref() - else { - unreachable!("The end of an IfConditional must be a Block"); - }; + let mut end_indices = vec![]; - let Some(TemplateAstExpr::ConditionalContent { content }) = chain.next() else { - unreachable!("The end of an IfConditional must be a Block"); - }; + let mut previous_post_whitespace_content: &Option = &None; + let mut previous_jump: Option = 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 { content: ws.source().clone(), }); } - let emit_slot = machine.reserve_slot(); - emit_expr_load(machine, eval, emit_slot, expression); - - let index = eval.len(); - 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(), - }); + for index in end_indices { + let jump = eval.len() - index - 1; + eval[index] = Instruction::Jump { + jump: jump as isize, + }; } } @@ -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); + } } 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 new file mode 100644 index 0000000..ee52b87 --- /dev/null +++ b/src/emit/snapshots/nomo__emit__tests__check_if_else_if.snap @@ -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), + }, +] diff --git a/src/eval/mod.rs b/src/eval/mod.rs index f94ed8c..0b0b6f9 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -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; diff --git a/tests/cases/1-parsed@if_else_if.snap b/tests/cases/1-parsed@if_else_if.snap new file mode 100644 index 0000000..287fce6 --- /dev/null +++ b/tests/cases/1-parsed@if_else_if.snap @@ -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), + ], +} diff --git a/tests/cases/2-ast@condition.snap b/tests/cases/2-ast@condition.snap index 22d6b93..ae3a62e 100644 --- a/tests/cases/2-ast@condition.snap +++ b/tests/cases/2-ast@condition.snap @@ -7,16 +7,16 @@ TemplateAst { root: [ ConditionalChain { chain: [ - IfConditional { - if_block: Block { - prev_whitespace_content: None, + Block { + prev_whitespace_content: None, + expression: IfConditional { expression: VariableAccess( [Ident]"test" (6..10), ), - post_whitespace_content: Some( - [Whitespace]"\n " (13..18), - ), }, + post_whitespace_content: Some( + [Whitespace]"\n " (13..18), + ), }, ConditionalContent { content: [ diff --git a/tests/cases/2-ast@if_else_if.snap b/tests/cases/2-ast@if_else_if.snap new file mode 100644 index 0000000..3bc850b --- /dev/null +++ b/tests/cases/2-ast@if_else_if.snap @@ -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, + }, + ], + }, + ], +} diff --git a/tests/cases/3-instructions@condition.snap b/tests/cases/3-instructions@condition.snap index f79800d..c2901f7 100644 --- a/tests/cases/3-instructions@condition.snap +++ b/tests/cases/3-instructions@condition.snap @@ -14,7 +14,7 @@ input_file: tests/cases/condition.nomo emit_slot: VariableSlot { index: 0, }, - jump: 3, + jump: 5, }, AppendContent { content: "\n " (13..18), @@ -25,6 +25,12 @@ input_file: tests/cases/condition.nomo AppendContent { content: "\n" (30..31), }, + Jump { + jump: 2, + }, + AppendContent { + content: "\n " (13..18), + }, AppendContent { content: "\n\n" (40..42), }, diff --git a/tests/cases/3-instructions@if_else_if.snap b/tests/cases/3-instructions@if_else_if.snap new file mode 100644 index 0000000..d19a4ed --- /dev/null +++ b/tests/cases/3-instructions@if_else_if.snap @@ -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), + }, +] diff --git a/tests/cases/4-output@if_else_if.snap b/tests/cases/4-output@if_else_if.snap new file mode 100644 index 0000000..efb547b --- /dev/null +++ b/tests/cases/4-output@if_else_if.snap @@ -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 " diff --git a/tests/cases/if_else_if.nomo b/tests/cases/if_else_if.nomo new file mode 100644 index 0000000..befa0fe --- /dev/null +++ b/tests/cases/if_else_if.nomo @@ -0,0 +1,11 @@ +{ + "test": false, + "another_test": true, + "stuff": "more" +} +--- +{{ if test }} + Not Hello World! :C +{{ else if another_test }} + Hello World! +{{ end }} \ No newline at end of file