use crate::ast::TemplateAstExpr; use crate::input::NomoInput; pub struct EmitMachine { current_index: usize, } impl EmitMachine { fn reserve_slot(&mut self) -> VariableSlot { VariableSlot { index: { let val = self.current_index; self.current_index += 1; val }, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct VariableSlot { index: usize, } #[derive(Debug)] pub enum Instruction { AppendContent { content: NomoInput, }, LoadFromContextToSlot { name: NomoInput, slot: VariableSlot, }, EmitFromSlot { slot: VariableSlot, }, PushScope { inherit_parent: bool, }, Abort, JumpIfNotTrue { emit_slot: VariableSlot, jump: isize, }, Jump { jump: isize, }, NoOp, } pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> Vec { let mut eval = vec![]; let mut machine = EmitMachine { current_index: 0 }; for ast in input.root() { emit_ast_expr(&mut machine, &mut eval, ast); } eval } fn emit_ast_expr( machine: &mut EmitMachine, eval: &mut Vec, ast: &TemplateAstExpr<'_>, ) { match ast { TemplateAstExpr::StaticContent(template_token) => { eval.push(Instruction::AppendContent { content: template_token.source().clone(), }); } TemplateAstExpr::Interpolation { prev_whitespace_content, expression, post_whitespace_content, } => { if let Some(ws) = prev_whitespace_content { eval.push(Instruction::AppendContent { content: ws.source().clone(), }); } let emit_slot = machine.reserve_slot(); emit_expr_load(machine, eval, emit_slot, expression); eval.push(Instruction::EmitFromSlot { slot: emit_slot }); if let Some(ws) = post_whitespace_content { eval.push(Instruction::AppendContent { content: ws.source().clone(), }); } } TemplateAstExpr::ConditionalChain { chain } => { let mut chain = chain.iter(); let mut end_indices = vec![]; let mut previous_post_whitespace_content: &Option = &None; let mut previous_jump: Option = None; 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 { let idx = end_indices.last().copied(); eval.insert( idx.unwrap_or(eval.len()), Instruction::AppendContent { content: ws.source().clone(), }, ); if let Some(idx) = end_indices.last_mut() { *idx += 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(), }); } else { eval.push(Instruction::NoOp); } for index in end_indices { let jump = eval.len() - index - 1; eval[index] = Instruction::Jump { jump: jump as isize, }; } } TemplateAstExpr::Block { .. } | TemplateAstExpr::EndBlock | TemplateAstExpr::IfConditional { .. } | TemplateAstExpr::ConditionalContent { .. } | TemplateAstExpr::ElseConditional { .. } | TemplateAstExpr::Invalid { .. } | TemplateAstExpr::VariableAccess { .. } => eval.push(Instruction::Abort), } } fn emit_expr_load( _machine: &mut EmitMachine, eval: &mut Vec, emit_slot: VariableSlot, expression: &TemplateAstExpr<'_>, ) { match expression { TemplateAstExpr::VariableAccess(template_token) => { eval.push(Instruction::LoadFromContextToSlot { name: template_token.source().clone(), slot: emit_slot, }); } TemplateAstExpr::Invalid { .. } => eval.push(Instruction::Abort), TemplateAstExpr::StaticContent { .. } | TemplateAstExpr::Interpolation { .. } => { unreachable!("Invalid AST here") } TemplateAstExpr::ConditionalChain { .. } => todo!(), TemplateAstExpr::ElseConditional { .. } => todo!(), TemplateAstExpr::EndBlock => todo!(), TemplateAstExpr::Block { .. } => todo!(), TemplateAstExpr::IfConditional { .. } => todo!(), TemplateAstExpr::ConditionalContent { .. } => todo!(), } } #[cfg(test)] mod tests { use crate::emit::emit_machine; #[test] fn check_simple_variable_interpolation() { let input = "Hello {{= world }}"; 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, @r#" [ AppendContent { content: "Hello" (0..5), }, AppendContent { content: " " (5..6), }, LoadFromContextToSlot { name: "world" (10..15), slot: VariableSlot { index: 0, }, }, EmitFromSlot { slot: VariableSlot { index: 0, }, }, ] "#); } #[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); } }