Add for loop

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2026-03-11 18:09:58 +01:00
parent 7182024342
commit 42e0056374
16 changed files with 775 additions and 44 deletions

View file

@ -1,11 +1,11 @@
use std::collections::HashMap;
use std::collections::BTreeMap;
use crate::ast::TemplateAstExpr;
use crate::input::NomoInput;
pub struct EmitMachine {
current_index: usize,
labels: HashMap<LabelSlot, usize>,
labels: BTreeMap<LabelSlot, usize>,
}
impl EmitMachine {
@ -36,12 +36,12 @@ impl EmitMachine {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct VariableSlot {
index: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct LabelSlot {
index: usize,
}
@ -70,11 +70,29 @@ pub enum Instruction {
jump: LabelSlot,
},
NoOp,
CreateIteratorFromSlotToSlot {
iterator_slot: VariableSlot,
iterator_source_slot: VariableSlot,
},
AdvanceIteratorOrJump {
iterator_slot: VariableSlot,
value_slot: VariableSlot,
jump: LabelSlot,
},
GetIteratorEmptyOrJump {
iterator_slot: VariableSlot,
jump: LabelSlot,
},
PopScope,
LoadFromSlotToContext {
value_ident: NomoInput,
value_slot: VariableSlot,
},
}
#[derive(Debug, Clone)]
pub struct VMInstructions {
pub labels: HashMap<LabelSlot, usize>,
pub labels: BTreeMap<LabelSlot, usize>,
pub instructions: Vec<Instruction>,
}
@ -83,7 +101,7 @@ pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> VMInstructions {
let mut machine = EmitMachine {
current_index: 0,
labels: HashMap::new(),
labels: BTreeMap::new(),
};
for ast in input.root() {
@ -221,13 +239,166 @@ fn emit_ast_expr(
eval.push(Instruction::NoOp);
}
}
TemplateAstExpr::ForChain {
for_block,
content,
else_block,
else_content,
end_block,
} => {
let post_for_whitespace_content;
let label_to_else_or_empty_index = machine.reserve_label();
let label_to_end_index = machine.reserve_label();
let label_start_loop = machine.reserve_label();
if let TemplateAstExpr::Block {
prev_whitespace_content,
expression,
post_whitespace_content,
} = &**for_block
&& let TemplateAstExpr::For {
value_ident,
value_expression,
} = &**expression
{
if let Some(ws) = prev_whitespace_content {
eval.push(Instruction::AppendContent {
content: ws.source().clone(),
});
}
post_for_whitespace_content = post_whitespace_content;
eval.push(Instruction::PushScope {
inherit_parent: true,
});
let value_slot = machine.reserve_slot();
let iterator_source_slot = machine.reserve_slot();
let iterator_slot = machine.reserve_slot();
emit_expr_load(machine, eval, iterator_source_slot, value_expression);
eval.push(Instruction::CreateIteratorFromSlotToSlot {
iterator_source_slot,
iterator_slot,
});
eval.push(Instruction::GetIteratorEmptyOrJump {
iterator_slot,
jump: label_to_else_or_empty_index,
});
machine.assign_label(label_start_loop, eval.len());
eval.push(Instruction::AdvanceIteratorOrJump {
iterator_slot,
value_slot,
jump: label_to_end_index,
});
eval.push(Instruction::LoadFromSlotToContext {
value_slot,
value_ident: value_ident.source(),
});
} else {
panic!("For block should be a for block");
};
if let Some(ws) = post_for_whitespace_content {
eval.push(Instruction::AppendContent {
content: ws.source().clone(),
});
}
for content in content {
emit_ast_expr(machine, eval, content);
}
let end_of_content_jump = eval.len();
eval.push(Instruction::Jump {
jump: label_start_loop,
});
let has_else = else_block.is_some();
if let Some(TemplateAstExpr::Block {
prev_whitespace_content,
expression,
post_whitespace_content,
}) = else_block.as_deref()
&& let TemplateAstExpr::ForElse = &**expression
{
if let Some(ws) = prev_whitespace_content {
eval.insert(
end_of_content_jump.saturating_sub(1),
Instruction::AppendContent {
content: ws.source().clone(),
},
);
}
machine.assign_label(label_to_else_or_empty_index, eval.len());
if let Some(ws) = post_whitespace_content {
eval.push(Instruction::AppendContent {
content: ws.source().clone(),
});
}
for content in else_content
.as_ref()
.expect("If there is a for block, there should be for content (even if empty)")
{
emit_ast_expr(machine, eval, content);
}
}
let post_end_whitespace_content;
if let TemplateAstExpr::Block {
prev_whitespace_content,
expression,
post_whitespace_content,
} = &**end_block
&& let TemplateAstExpr::EndBlock = &**expression
{
post_end_whitespace_content = post_whitespace_content;
if let Some(ws) = prev_whitespace_content {
if has_else {
eval.push(Instruction::AppendContent {
content: ws.source().clone(),
});
} else {
eval.insert(
end_of_content_jump.saturating_sub(1),
Instruction::AppendContent {
content: ws.source().clone(),
},
);
}
}
if !has_else {
machine.assign_label(label_to_else_or_empty_index, eval.len());
}
machine.assign_label(label_to_end_index, eval.len());
eval.push(Instruction::PopScope);
if let Some(ws) = post_end_whitespace_content {
eval.push(Instruction::AppendContent {
content: ws.source().clone(),
});
}
} else {
panic!("End block should be an endblock");
}
}
TemplateAstExpr::Block { .. }
| TemplateAstExpr::EndBlock
| TemplateAstExpr::IfConditional { .. }
| TemplateAstExpr::ConditionalContent { .. }
| TemplateAstExpr::ElseConditional { .. }
| TemplateAstExpr::ForChain { .. }
| TemplateAstExpr::For { .. }
| TemplateAstExpr::ForElse
| TemplateAstExpr::Invalid { .. }

View file

@ -4,15 +4,15 @@ expression: emit
---
VMInstructions {
labels: {
LabelSlot {
index: 4,
}: 14,
LabelSlot {
index: 0,
}: 19,
LabelSlot {
index: 2,
}: 7,
LabelSlot {
index: 4,
}: 14,
},
instructions: [
LoadFromContextToSlot {

View file

@ -6,7 +6,9 @@ use thiserror::Error;
use crate::Context;
use crate::emit::Instruction;
use crate::emit::VMInstructions;
use crate::emit::VariableSlot;
use crate::input::NomoInput;
use crate::value::NomoValue;
#[derive(Debug, Error, Display)]
pub enum EvaluationError {
@ -21,10 +23,59 @@ pub enum EvaluationError {
LabelNotFound,
}
struct Scope {
stack: Vec<HashMap<String, NomoValue>>,
slots: HashMap<VariableSlot, NomoValue>,
}
impl Scope {
fn insert_into_slot(&mut self, slot: VariableSlot, value: NomoValue) {
self.slots.insert(slot, value);
}
fn get(&self, slot: &VariableSlot) -> &NomoValue {
self.slots.get(slot).expect("All slot loads must be valid")
}
fn get_mut(&mut self, slot: &VariableSlot) -> &mut NomoValue {
self.slots
.get_mut(slot)
.expect("All slot loads must be valid")
}
fn push_scope(&mut self) {
self.stack.push(Default::default());
}
fn pop_scope(&mut self) {
self.stack.pop();
}
fn insert_into_scope(&mut self, ident: &NomoInput, value: NomoValue) {
self.stack
.last_mut()
.unwrap()
.insert(ident.to_string(), value);
}
fn get_scoped(&self, name: &NomoInput) -> Option<&NomoValue> {
self.stack
.iter()
.rev()
.find_map(|scope| scope.get(name.as_str()))
}
}
#[allow(
clippy::unnecessary_to_owned,
reason = "We cannot do the suggested way as the lifetimes would not match up"
)]
pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result<String, EvaluationError> {
let mut output = String::new();
let mut scopes: HashMap<crate::emit::VariableSlot, serde_json::Value> = HashMap::new();
let mut scopes = Scope {
stack: vec![global_context.values().clone()],
slots: HashMap::new(),
};
let mut ip = 0;
loop {
@ -38,21 +89,22 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result<String,
Instruction::NoOp => (),
Instruction::AppendContent { content } => output.push_str(content),
Instruction::LoadFromContextToSlot { name, slot } => {
let value = global_context
.values
.get(name.as_str())
let value = scopes
.get_scoped(name)
.ok_or(EvaluationError::UnknownVariable(name.clone()))?;
scopes.insert(*slot, value.clone());
scopes.insert_into_slot(*slot, value.clone());
}
Instruction::EmitFromSlot { slot } => {
let value = scopes.get(slot).unwrap().as_str().unwrap();
let value = scopes.get(slot).as_str().unwrap();
output.push_str(value);
}
Instruction::PushScope { inherit_parent: _ } => todo!(),
Instruction::PushScope { inherit_parent: _ } => {
scopes.push_scope();
}
Instruction::Abort => return Err(EvaluationError::ExplicitAbort),
Instruction::JumpIfNotTrue { emit_slot, jump } => {
let dont_jump = scopes.get(emit_slot).unwrap().as_bool().unwrap();
let dont_jump = scopes.get(emit_slot).as_bool().unwrap();
if dont_jump {
// We are done
} else {
@ -72,6 +124,59 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result<String,
ip = *new_ip;
continue;
}
Instruction::CreateIteratorFromSlotToSlot {
iterator_slot,
iterator_source_slot,
} => {
let value = scopes.get(iterator_source_slot).as_array().unwrap();
scopes.insert_into_slot(
*iterator_slot,
NomoValue::Iterator {
value: Box::new(value.to_vec().into_iter()),
},
);
}
Instruction::AdvanceIteratorOrJump {
iterator_slot,
value_slot,
jump,
} => {
let iterator = scopes.get_mut(iterator_slot).as_iterator_mut().unwrap();
if let Some(value) = iterator.next() {
scopes.insert_into_slot(*value_slot, value);
} else {
let Some(new_ip) = vm.labels.get(jump) else {
return Err(EvaluationError::LabelNotFound);
};
ip = *new_ip;
}
}
Instruction::GetIteratorEmptyOrJump {
iterator_slot,
jump,
} => {
let iterator = scopes.get(iterator_slot).as_iterator().unwrap();
let (min, _) = iterator.size_hint();
if min == 0 {
let Some(new_ip) = vm.labels.get(jump) else {
return Err(EvaluationError::LabelNotFound);
};
ip = *new_ip;
}
}
Instruction::PopScope => scopes.pop_scope(),
Instruction::LoadFromSlotToContext {
value_ident,
value_slot,
} => {
let value = scopes.get(value_slot).clone();
scopes.insert_into_scope(value_ident, value);
}
}
ip += 1;

View file

@ -1,19 +1,19 @@
use std::collections::BTreeMap;
use std::collections::HashMap;
use displaydoc::Display;
use serde::Serialize;
use thiserror::Error;
use crate::emit::Instruction;
use crate::emit::VMInstructions;
use crate::input::NomoInput;
use crate::value::NomoValue;
use crate::value::NomoValueError;
pub mod ast;
pub mod emit;
pub mod eval;
pub mod input;
pub mod parser;
pub mod value;
#[derive(Debug, Error, Display)]
pub enum NomoError {
@ -89,7 +89,7 @@ struct Template {
}
pub struct Context {
values: BTreeMap<String, serde_json::Value>,
values: HashMap<String, NomoValue>,
}
impl Default for Context {
@ -101,23 +101,26 @@ impl Default for Context {
impl Context {
pub fn new() -> Context {
Context {
values: BTreeMap::new(),
values: HashMap::new(),
}
}
pub fn try_insert(
&mut self,
key: impl Into<String>,
value: impl Serialize,
) -> Result<(), serde_json::Error> {
self.values.insert(key.into(), serde_json::to_value(value)?);
value: impl TryInto<NomoValue, Error = NomoValueError>,
) -> Result<(), NomoValueError> {
self.values.insert(key.into(), value.try_into()?);
Ok(())
}
pub fn insert(&mut self, key: impl Into<String>, value: impl Serialize) {
self.try_insert(key, value)
.expect("inserted value should serialize without error");
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<NomoValue>) {
self.values.insert(key.into(), value.into());
}
pub fn values(&self) -> &HashMap<String, NomoValue> {
&self.values
}
}

251
src/value.rs Normal file
View file

@ -0,0 +1,251 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
#[cfg(feature = "serde_json")]
use displaydoc::Display;
use thiserror::Error;
#[derive(Clone)]
pub enum NomoValue {
String {
value: Cow<'static, str>,
},
Array {
value: Vec<NomoValue>,
},
Bool {
value: bool,
},
Object {
value: BTreeMap<String, NomoValue>,
},
Integer {
value: u64,
},
SignedInteger {
value: i64,
},
Float {
value: f64,
},
Iterator {
value: Box<dyn CloneIterator<Item = NomoValue>>,
},
}
impl NomoValue {
pub fn as_str(&self) -> Option<&str> {
if let Self::String { value } = self {
Some(value)
} else {
None
}
}
pub fn as_array(&self) -> Option<&[NomoValue]> {
if let Self::Array { value } = self {
Some(value)
} else {
None
}
}
pub fn as_bool(&self) -> Option<bool> {
if let Self::Bool { value } = self {
Some(*value)
} else {
None
}
}
pub fn as_object(&self) -> Option<&BTreeMap<String, NomoValue>> {
if let Self::Object { value } = self {
Some(value)
} else {
None
}
}
pub fn as_integer(&self) -> Option<u64> {
if let Self::Integer { value } = self {
Some(*value)
} else {
None
}
}
pub fn as_float(&self) -> Option<f64> {
if let Self::Float { value } = self {
Some(*value)
} else {
None
}
}
pub fn as_iterator(&self) -> Option<&dyn CloneIterator<Item = NomoValue>> {
if let Self::Iterator { value } = self {
Some(value)
} else {
None
}
}
pub fn as_iterator_mut(&mut self) -> Option<&mut dyn CloneIterator<Item = NomoValue>> {
if let Self::Iterator { value } = self {
Some(value)
} else {
None
}
}
}
pub trait CloneIterator: Iterator<Item = NomoValue> {
fn clone_box(&self) -> Box<dyn CloneIterator<Item = NomoValue>>;
}
impl<I> CloneIterator for I
where
I: Iterator<Item = NomoValue> + Clone + 'static,
{
fn clone_box(&self) -> Box<dyn CloneIterator<Item = NomoValue>> {
Box::new(Clone::clone(self))
}
}
impl Clone for Box<dyn CloneIterator> {
fn clone(&self) -> Self {
self.clone_box()
}
}
impl std::fmt::Debug for NomoValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::String { value } => f.debug_struct("String").field("value", value).finish(),
Self::Array { value } => f.debug_struct("Array").field("value", value).finish(),
Self::Bool { value } => f.debug_struct("Bool").field("value", value).finish(),
Self::Object { value } => f.debug_struct("Object").field("value", value).finish(),
Self::Integer { value } => f.debug_struct("Integer").field("value", value).finish(),
Self::SignedInteger { value } => f
.debug_struct("SignedInteger")
.field("value", value)
.finish(),
Self::Float { value } => f.debug_struct("Float").field("value", value).finish(),
Self::Iterator { value: _ } => f
.debug_struct("Iterator")
.field("value", &"Iterator")
.finish(),
}
}
}
impl<R> From<&R> for NomoValue
where
R: Into<NomoValue> + Clone,
{
fn from(value: &R) -> Self {
value.clone().into()
}
}
impl From<String> for NomoValue {
fn from(val: String) -> Self {
NomoValue::String {
value: Cow::Owned(val),
}
}
}
impl From<&'static str> for NomoValue {
fn from(val: &'static str) -> Self {
NomoValue::String {
value: Cow::Borrowed(val),
}
}
}
impl<V> From<Vec<V>> for NomoValue
where
V: Into<NomoValue>,
{
fn from(val: Vec<V>) -> Self {
NomoValue::Array {
value: val.into_iter().map(Into::into).collect(),
}
}
}
impl<V> From<std::collections::VecDeque<V>> for NomoValue
where
V: Into<NomoValue>,
{
fn from(val: std::collections::VecDeque<V>) -> Self {
NomoValue::Array {
value: val.into_iter().map(Into::into).collect(),
}
}
}
impl<V> From<&[V]> for NomoValue
where
V: Into<NomoValue> + Clone,
{
fn from(value: &[V]) -> Self {
NomoValue::Array {
value: value.iter().cloned().map(Into::into).collect(),
}
}
}
impl<K, V> From<std::collections::HashMap<K, V>> for NomoValue
where
K: Into<String>,
V: Into<NomoValue>,
{
fn from(val: std::collections::HashMap<K, V>) -> Self {
NomoValue::Object {
value: val.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
}
}
}
#[cfg(feature = "serde_json")]
#[derive(Debug, Error, Display)]
/// Could not transform value to [`NomoValue`]
pub struct NomoValueError;
#[cfg(feature = "serde_json")]
impl TryFrom<serde_json::Value> for NomoValue {
type Error = NomoValueError;
fn try_from(value: serde_json::Value) -> Result<Self, NomoValueError> {
match value {
serde_json::Value::Null => todo!(),
serde_json::Value::Bool(value) => Ok(NomoValue::Bool { value }),
serde_json::Value::Number(number) => {
if let Some(value) = number.as_u64() {
return Ok(NomoValue::Integer { value });
}
if let Some(value) = number.as_f64() {
return Ok(NomoValue::Float { value });
}
if let Some(value) = number.as_i64() {
return Ok(NomoValue::SignedInteger { value });
}
Err(NomoValueError)
}
serde_json::Value::String(str) => Ok(NomoValue::String {
value: Cow::Owned(str),
}),
serde_json::Value::Array(values) => Ok(NomoValue::Array {
value: values
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?,
}),
serde_json::Value::Object(_map) => todo!(),
}
}
}