Add set statements, to set variables (in a way that isn't block-scoped)

This commit is contained in:
Olivier 'reivilibre' 2025-05-14 21:36:56 +01:00
parent c92ab5aaff
commit d08a088257
9 changed files with 79 additions and 3 deletions

View File

@ -11,6 +11,7 @@ pub struct Template {
pub enum Block {
HtmlElement(HtmlElement),
ComponentElement(ComponentElement),
SetStatement(SetStatement),
IfBlock(IfBlock),
ForBlock(ForBlock),
MatchBlock(MatchBlock),
@ -43,6 +44,13 @@ pub struct ComponentElement {
pub loc: Locator,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct SetStatement {
pub binding: Binding,
pub expression: Expression,
pub loc: Locator,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct IfBlock {
pub condition: Expression,

View File

@ -12,6 +12,7 @@ BlockContent = _{
Text |
RawUnescapedHtml |
DefineExpandSlot |
SetStatement |
ForBlock |
MatchBlock |
DefineFragment
@ -39,6 +40,10 @@ RawUnescapedHtml = {
"raw" ~ ws+ ~ String ~ lineEnd
}
SetStatement = {
"set" ~ ws+ ~ Binding ~ ws+ ~ "=" ~ ws+ ~ Expr ~ lineEnd
}
IfBlock = {
"if" ~ ws+ ~ IfCondition ~ lineEnd ~ NewBlock ~
ElseBlock?

View File

@ -2,8 +2,8 @@
use crate::ast::{
Binding, Block, ComponentElement, DefineExpandSlot, DefineFragment, ElementAttributeFlags,
Expression, ForBlock, HtmlElement, IfBlock, MatchBinding, MatchBlock, StringExpr, StringPiece,
Template,
Expression, ForBlock, HtmlElement, IfBlock, MatchBinding, MatchBlock, SetStatement, StringExpr,
StringPiece, Template,
};
use crate::{intern, IStr, Locator};
use lazy_static::lazy_static;
@ -415,6 +415,22 @@ impl HornbeamParser {
))
}
fn SetStatement(input: Node) -> PCResult<Block> {
let loc = nodeloc(&input);
let (binding, expression) = match_nodes!(input.into_children();
[Binding(binding), Expr(expression)] => {
(binding, expression)
},
);
Ok(Block::SetStatement(SetStatement {
binding,
expression,
loc,
}))
}
fn IfCondition(input: Node) -> PCResult<Expression> {
Self::Expr(input.into_children().single()?)
}
@ -559,6 +575,7 @@ impl HornbeamParser {
Rule::Element => Some(HornbeamParser::Element(input)?),
Rule::Text => Some(HornbeamParser::Text(input)?),
Rule::RawUnescapedHtml => Some(HornbeamParser::RawUnescapedHtml(input)?),
Rule::SetStatement => Some(HornbeamParser::SetStatement(input)?),
Rule::IfBlock => Some(HornbeamParser::IfBlock(input)?),
Rule::ForBlock => Some(HornbeamParser::ForBlock(input)?),
Rule::MatchBlock => Some(HornbeamParser::MatchBlock(input)?),

View File

@ -243,6 +243,20 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
.await
.map_err(|underlying| InterpreterError::OutputError { underlying })?;
}
StepDef::Set {
expression,
binding,
} => {
let expr_evaled = self.evaluate_expression(scope_idx, expression, &step.locator)?;
// Bind the expression
// Unlike the other places where we bind variables, we never unbind this one,
// since `set` is not block-scoped.
// (Ideally we would have slightly more careful scoping rules, but `set` is intentionally being
// added to allow variables to escape their respective blocks...)
let mut binder = Binder::new();
binder.bind(&mut self.scopes[scope_idx].variables, binding, expr_evaled);
}
StepDef::If {
condition,
true_steps,

View File

@ -85,3 +85,15 @@ for $part in $sts.carrot.split("A")
"#
))
}
#[test]
fn snapshot_004() {
assert_snapshot!(simple_render(
r#"
for $part in $sts.carrot.split("A")
set $final_part = $part
"the final part was: $final_part"
"#
))
}

View File

@ -0,0 +1,5 @@
---
source: hornbeam_interpreter/tests/snapshots.rs
expression: "simple_render(r#\"\nfor $part in $sts.carrot.split(\"A\")\n set $final_part = $part\n\n\"the final part was: $final_part\"\n \"#)"
---
the final part was: RROT!

View File

@ -133,7 +133,10 @@ fn pull_out_entrypoints_from_block<'a>(
}
}
}
Block::Text(_) | Block::RawUnescapedHtml(_) | Block::DefineExpandSlot(_) => { /* nop */ }
Block::Text(_)
| Block::RawUnescapedHtml(_)
| Block::DefineExpandSlot(_)
| Block::SetStatement(_) => { /* nop */ }
}
Ok(())
}
@ -304,6 +307,13 @@ fn compile_ast_block_to_steps(
locator: ce.loc.clone(),
})
}
Block::SetStatement(ss) => instructions.push(Step {
def: StepDef::Set {
binding: ss.binding.clone(),
expression: ss.expression.clone(),
},
locator: ss.loc.clone(),
}),
Block::IfBlock(ifb) => {
let mut true_instrs = Vec::new();
let mut false_instrs = Vec::new();

View File

@ -28,6 +28,10 @@ pub enum StepDef {
escape: bool,
expr: Expression,
},
Set {
expression: Expression,
binding: Binding,
},
If {
condition: Expression,
true_steps: Vec<Step>,

View File

@ -54,6 +54,7 @@ fn apply_peephole_pass<F: Fn(&mut Vec<Step>)>(steps: &mut Vec<Step>, pass: &F) {
match &mut step.def {
StepDef::WriteLiteral { .. } => {}
StepDef::WriteEval { .. } => {}
StepDef::Set { .. } => {}
StepDef::If {
true_steps,
false_steps,