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

View file

@ -24,6 +24,12 @@ pub struct TemplateAst<'input> {
root: Vec<TemplateAstExpr<'input>>,
}
impl<'input> TemplateAst<'input> {
pub fn root(&self) -> &[TemplateAstExpr<'input>] {
&self.root
}
}
#[derive(Debug, Clone)]
pub struct AstError {
pub(crate) message: Option<String>,
@ -145,6 +151,7 @@ pub enum TemplateAstExpr<'input> {
StaticContent(TemplateToken<'input>),
Interpolation {
prev_whitespace: Option<TemplateToken<'input>>,
wants_output: Option<TemplateToken<'input>>,
expression: Box<TemplateAstExpr<'input>>,
post_whitespace: Option<TemplateToken<'input>>,
},
@ -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,

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,
},
},
]
"#);
}
}

View file

@ -1,7 +1,70 @@
pub struct EvalStack {
stack: Vec<Evaluation>,
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<String, EvalError> {
let mut output = String::new();
let mut scopes: HashMap<crate::emit::VariableSlot, serde_json::Value> = 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",
)
"#);
}
}

View file

@ -6,6 +6,7 @@ use serde::Serialize;
use thiserror::Error;
pub mod ast;
pub mod emit;
pub mod eval;
pub mod parser;

View file

@ -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<ParsedTemplate<'_>, ParseFailure> {
@ -321,6 +329,7 @@ fn parse_interpolate<'input>(
) -> PResult<'input, Vec<TemplateToken<'input>>> {
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);