From 1ea15f0e490bb8c13c7b46257e845dd8a1829f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Fri, 6 Mar 2026 11:03:48 +0100 Subject: [PATCH] Add first working pipeline of parse -> ast -> instr -> render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- src/ast/mod.rs | 19 ++++++- src/emit/mod.rs | 139 ++++++++++++++++++++++++++++++++++++++++++++++ src/eval/mod.rs | 71 +++++++++++++++++++++-- src/lib.rs | 1 + src/parser/mod.rs | 10 ++++ 5 files changed, 234 insertions(+), 6 deletions(-) create mode 100644 src/emit/mod.rs diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 62f86c7..07c492c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -24,6 +24,12 @@ pub struct TemplateAst<'input> { root: Vec>, } +impl<'input> TemplateAst<'input> { + pub fn root(&self) -> &[TemplateAstExpr<'input>] { + &self.root + } +} + #[derive(Debug, Clone)] pub struct AstError { pub(crate) message: Option, @@ -145,6 +151,7 @@ pub enum TemplateAstExpr<'input> { StaticContent(TemplateToken<'input>), Interpolation { prev_whitespace: Option>, + wants_output: Option>, expression: Box>, post_whitespace: Option>, }, @@ -172,10 +179,11 @@ fn parse_interpolation<'input>( ) .with_taken() .map(|(expr, taken)| expr.unwrap_or(TemplateAstExpr::Invalid(taken))); - let (prev_whitespace, _left, (expression, _right, post_whitespace)) = ( + let (prev_whitespace, _left, (wants_output, expression, _right, post_whitespace)) = ( opt(TokenKind::Whitespace), TokenKind::LeftDelim, cut_err(( + opt(TokenKind::WantsOutput), delimited(ignore_ws, expr_parser, ignore_ws).map(Box::new), TokenKind::RightDelim, opt(TokenKind::Whitespace), @@ -185,6 +193,7 @@ fn parse_interpolation<'input>( Ok(TemplateAstExpr::Interpolation { prev_whitespace, + wants_output, expression, post_whitespace, }) @@ -230,7 +239,7 @@ mod tests { #[test] fn check_simple_variable_interpolation() { - let input = "Hello {{ world }}"; + let input = "Hello {{= world }}"; let parsed = crate::parser::parse(input).unwrap(); @@ -252,6 +261,12 @@ mod tests { source: " ", }, ), + wants_output: Some( + TemplateToken { + kind: WantsOutput, + source: "=", + }, + ), expression: VariableAccess( TemplateToken { kind: Ident, diff --git a/src/emit/mod.rs b/src/emit/mod.rs new file mode 100644 index 0000000..32da751 --- /dev/null +++ b/src/emit/mod.rs @@ -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 { + 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().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, + 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, + }, + }, + ] + "#); + } +} diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 655eb9d..c7a598c 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -1,7 +1,70 @@ -pub struct EvalStack { - stack: Vec, +use std::collections::HashMap; + +use displaydoc::Display; +use thiserror::Error; + +use crate::Context; +use crate::emit::Instruction; + +#[derive(Debug, Error, Display)] +enum EvalError { + /// An unknown variable was encountered: .0 + UnknownVariable(String), + /// An explicit abort was requested + ExplicitAbort, } -pub enum Evaluation { - AppendContent { content: String }, +fn execute(instructions: &[Instruction], global_context: &Context) -> Result { + let mut output = String::new(); + + let mut scopes: HashMap = HashMap::new(); + + for instr in instructions { + match instr { + Instruction::AppendContent { content } => output.push_str(content), + Instruction::LoadFromContextToSlot { name, slot } => { + let value = global_context + .values + .get(name) + .ok_or(EvalError::UnknownVariable(name.clone()))?; + + scopes.insert(*slot, value.clone()); + } + Instruction::EmitFromSlot { slot } => { + let value = scopes.get(slot).unwrap().as_str().unwrap(); + output.push_str(value); + } + Instruction::PushScope { inherit_parent: _ } => todo!(), + Instruction::Abort => return Err(EvalError::ExplicitAbort), + } + } + + Ok(output) +} + +#[cfg(test)] +mod tests { + use crate::Context; + use crate::eval::execute; + + #[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 = crate::emit::emit_machine(ast); + + let mut context = Context::new(); + context.insert("world", "World"); + let output = execute(&emit, &context); + + insta::assert_debug_snapshot!(output, @r#" + Ok( + "Hello World", + ) + "#); + } } diff --git a/src/lib.rs b/src/lib.rs index 41b0715..9d1df4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use serde::Serialize; use thiserror::Error; pub mod ast; +pub mod emit; pub mod eval; pub mod parser; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index df24653..367b85e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -285,6 +285,14 @@ impl<'input> TemplateToken<'input> { source, } } + + pub fn kind(&self) -> TokenKind { + self.kind + } + + pub fn source(&self) -> &'input str { + self.source + } } pub fn parse(input: &str) -> Result, ParseFailure> { @@ -321,6 +329,7 @@ fn parse_interpolate<'input>( ) -> PResult<'input, Vec>> { let prev_whitespace = opt(parse_whitespace).parse_next(input)?; let left_delim = "{{".map(TemplateToken::left_delim).parse_next(input)?; + let wants_output = opt("=".map(TemplateToken::wants_output)).parse_next(input)?; let get_tokens = repeat_till(1.., parse_interpolate_token, peek("}}")); let recover = take_until(0.., "}}").void(); @@ -337,6 +346,7 @@ fn parse_interpolate<'input>( let mut tokens = vec![]; tokens.extend(prev_whitespace); tokens.push(left_delim); + tokens.extend(wants_output); tokens.extend(inside_tokens); tokens.push(right_delim); tokens.extend(post_whitespace);