Add first working pipeline of parse -> ast -> instr -> render

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2026-03-06 11:03:48 +01:00
parent f5050e369e
commit 1ea15f0e49
5 changed files with 234 additions and 6 deletions

139
src/emit/mod.rs Normal file
View file

@ -0,0 +1,139 @@
use crate::ast::TemplateAstExpr;
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: String },
LoadFromContextToSlot { name: String, slot: VariableSlot },
EmitFromSlot { slot: VariableSlot },
PushScope { inherit_parent: bool },
Abort,
}
pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> Vec<Instruction> {
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<Instruction>, ast: &TemplateAstExpr<'_>) {
match ast {
TemplateAstExpr::StaticContent(template_token) => {
eval.push(Instruction::AppendContent {
content: template_token.source().to_string(),
});
}
TemplateAstExpr::Interpolation {
prev_whitespace,
wants_output,
expression,
post_whitespace,
} => {
if let Some(ws) = prev_whitespace {
eval.push(Instruction::AppendContent {
content: ws.source().to_string(),
});
}
let emit_slot = machine.reserve_slot();
emit_expr(machine, eval, emit_slot, expression);
if wants_output.is_some() {
eval.push(Instruction::EmitFromSlot { slot: emit_slot });
}
if let Some(ws) = post_whitespace {
eval.push(Instruction::AppendContent {
content: ws.source().to_string(),
});
}
}
TemplateAstExpr::Invalid { .. } | TemplateAstExpr::VariableAccess { .. } => {
eval.push(Instruction::Abort)
}
}
}
fn emit_expr(
machine: &mut EmitMachine,
eval: &mut Vec<Instruction>,
emit_slot: VariableSlot,
expression: &TemplateAstExpr<'_>,
) {
match expression {
TemplateAstExpr::VariableAccess(template_token) => {
eval.push(Instruction::LoadFromContextToSlot {
name: template_token.source().to_string(),
slot: emit_slot,
});
}
TemplateAstExpr::Invalid { .. } => eval.push(Instruction::Abort),
TemplateAstExpr::StaticContent { .. } | TemplateAstExpr::Interpolation { .. } => {
unreachable!("Invalid AST here")
}
}
}
#[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).unwrap();
let ast = crate::ast::parse(parsed.tokens()).unwrap();
let emit = emit_machine(ast);
insta::assert_debug_snapshot!(emit, @r#"
[
AppendContent {
content: "Hello",
},
AppendContent {
content: " ",
},
LoadFromContextToSlot {
name: "world",
slot: VariableSlot {
index: 0,
},
},
EmitFromSlot {
slot: VariableSlot {
index: 0,
},
},
]
"#);
}
}