Introduce JumpLabels instead of manually correct jump positions

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2026-03-11 15:50:05 +01:00
parent e64256b65f
commit 7182024342
12 changed files with 485 additions and 381 deletions

View file

@ -1,8 +1,11 @@
use std::collections::HashMap;
use crate::ast::TemplateAstExpr; use crate::ast::TemplateAstExpr;
use crate::input::NomoInput; use crate::input::NomoInput;
pub struct EmitMachine { pub struct EmitMachine {
current_index: usize, current_index: usize,
labels: HashMap<LabelSlot, usize>,
} }
impl EmitMachine { impl EmitMachine {
@ -15,6 +18,22 @@ impl EmitMachine {
}, },
} }
} }
fn reserve_label(&mut self) -> LabelSlot {
LabelSlot {
index: {
let val = self.current_index;
self.current_index += 1;
val
},
}
}
fn assign_label(&mut self, slot: LabelSlot, idx: usize) {
let no_prev = self.labels.insert(slot, idx).is_none();
assert!(no_prev, "A label slot was already assigned")
}
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@ -22,7 +41,12 @@ pub struct VariableSlot {
index: usize, index: usize,
} }
#[derive(Debug)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LabelSlot {
index: usize,
}
#[derive(Debug, Clone)]
pub enum Instruction { pub enum Instruction {
AppendContent { AppendContent {
content: NomoInput, content: NomoInput,
@ -40,24 +64,36 @@ pub enum Instruction {
Abort, Abort,
JumpIfNotTrue { JumpIfNotTrue {
emit_slot: VariableSlot, emit_slot: VariableSlot,
jump: isize, jump: LabelSlot,
}, },
Jump { Jump {
jump: isize, jump: LabelSlot,
}, },
NoOp, NoOp,
} }
pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> Vec<Instruction> { #[derive(Debug, Clone)]
pub struct VMInstructions {
pub labels: HashMap<LabelSlot, usize>,
pub instructions: Vec<Instruction>,
}
pub fn emit_machine(input: crate::ast::TemplateAst<'_>) -> VMInstructions {
let mut eval = vec![]; let mut eval = vec![];
let mut machine = EmitMachine { current_index: 0 }; let mut machine = EmitMachine {
current_index: 0,
labels: HashMap::new(),
};
for ast in input.root() { for ast in input.root() {
emit_ast_expr(&mut machine, &mut eval, ast); emit_ast_expr(&mut machine, &mut eval, ast);
} }
eval VMInstructions {
labels: machine.labels,
instructions: eval,
}
} }
fn emit_ast_expr( fn emit_ast_expr(
@ -95,10 +131,11 @@ fn emit_ast_expr(
TemplateAstExpr::ConditionalChain { chain } => { TemplateAstExpr::ConditionalChain { chain } => {
let mut chain = chain.iter(); let mut chain = chain.iter();
let end_label = machine.reserve_label();
let mut end_indices = vec![]; let mut end_indices = vec![];
let mut previous_post_whitespace_content: &Option<crate::parser::TemplateToken> = &None; let mut previous_post_whitespace_content: &Option<crate::parser::TemplateToken> = &None;
let mut previous_jump: Option<usize> = None; let mut previous_jump: Option<LabelSlot> = None;
loop { loop {
let next = chain.next().unwrap(); let next = chain.next().unwrap();
@ -114,7 +151,7 @@ fn emit_ast_expr(
} }
end_indices.push(eval.len()); end_indices.push(eval.len());
eval.push(Instruction::Jump { jump: isize::MAX }); eval.push(Instruction::Jump { jump: end_label });
} else if let TemplateAstExpr::Block { } else if let TemplateAstExpr::Block {
prev_whitespace_content, prev_whitespace_content,
post_whitespace_content, post_whitespace_content,
@ -139,20 +176,15 @@ fn emit_ast_expr(
let emit_slot = machine.reserve_slot(); let emit_slot = machine.reserve_slot();
emit_expr_load(machine, eval, emit_slot, expression); emit_expr_load(machine, eval, emit_slot, expression);
previous_jump = Some(eval.len()); let jmp_label = machine.reserve_label();
previous_jump = Some(jmp_label);
eval.push(Instruction::JumpIfNotTrue { eval.push(Instruction::JumpIfNotTrue {
emit_slot, emit_slot,
jump: isize::MAX, jump: jmp_label,
}); });
} else if let TemplateAstExpr::ElseConditional { expression } = &**expression { } else if let TemplateAstExpr::ElseConditional { expression } = &**expression {
if let Some(previous_jump) = previous_jump.take() { if let Some(previous_jump) = previous_jump.take() {
let new_jump = eval.len() - previous_jump - 1; machine.assign_label(previous_jump, eval.len());
let Instruction::JumpIfNotTrue { jump, .. } = &mut eval[previous_jump]
else {
panic!("Jump slot had something that is not a jump?!");
};
*jump = new_jump as isize;
} else { } else {
panic!("Got an else without a previous if?"); panic!("Got an else without a previous if?");
} }
@ -161,10 +193,11 @@ fn emit_ast_expr(
let emit_slot = machine.reserve_slot(); let emit_slot = machine.reserve_slot();
emit_expr_load(machine, eval, emit_slot, expression); emit_expr_load(machine, eval, emit_slot, expression);
previous_jump = Some(eval.len()); let jmp_label = machine.reserve_label();
previous_jump = Some(jmp_label);
eval.push(Instruction::JumpIfNotTrue { eval.push(Instruction::JumpIfNotTrue {
emit_slot, emit_slot,
jump: isize::MAX, jump: jmp_label,
}); });
} else { } else {
// We don't have to do anything in the else case // We don't have to do anything in the else case
@ -176,13 +209,9 @@ fn emit_ast_expr(
} }
if let Some(previous_jump) = previous_jump.take() { if let Some(previous_jump) = previous_jump.take() {
let new_jump = eval.len() - previous_jump - 1; machine.assign_label(previous_jump, eval.len());
let Instruction::JumpIfNotTrue { jump, .. } = &mut eval[previous_jump] else {
panic!("Jump slot had something that is not a jump?!");
};
*jump = new_jump as isize;
} }
machine.assign_label(end_label, eval.len());
if let Some(ws) = previous_post_whitespace_content { if let Some(ws) = previous_post_whitespace_content {
eval.push(Instruction::AppendContent { eval.push(Instruction::AppendContent {
@ -191,13 +220,6 @@ fn emit_ast_expr(
} else { } else {
eval.push(Instruction::NoOp); eval.push(Instruction::NoOp);
} }
for index in end_indices {
let jump = eval.len() - index - 1;
eval[index] = Instruction::Jump {
jump: jump as isize,
};
}
} }
TemplateAstExpr::Block { .. } TemplateAstExpr::Block { .. }
@ -258,25 +280,28 @@ mod tests {
let emit = emit_machine(ast); let emit = emit_machine(ast);
insta::assert_debug_snapshot!(emit, @r#" insta::assert_debug_snapshot!(emit, @r#"
[ VMInstructions {
AppendContent { labels: {},
content: "Hello" (0..5), instructions: [
}, AppendContent {
AppendContent { content: "Hello" (0..5),
content: " " (5..6),
},
LoadFromContextToSlot {
name: "world" (10..15),
slot: VariableSlot {
index: 0,
}, },
}, AppendContent {
EmitFromSlot { content: " " (5..6),
slot: VariableSlot {
index: 0,
}, },
}, LoadFromContextToSlot {
] name: "world" (10..15),
slot: VariableSlot {
index: 0,
},
},
EmitFromSlot {
slot: VariableSlot {
index: 0,
},
},
],
}
"#); "#);
} }

View file

@ -2,75 +2,98 @@
source: src/emit/mod.rs source: src/emit/mod.rs
expression: emit expression: emit
--- ---
[ VMInstructions {
LoadFromContextToSlot { labels: {
name: "foo" (6..9), LabelSlot {
slot: VariableSlot { index: 4,
}: 14,
LabelSlot {
index: 0, index: 0,
}: 19,
LabelSlot {
index: 2,
}: 7,
},
instructions: [
LoadFromContextToSlot {
name: "foo" (6..9),
slot: VariableSlot {
index: 1,
},
}, },
}, JumpIfNotTrue {
JumpIfNotTrue { emit_slot: VariableSlot {
emit_slot: VariableSlot { index: 1,
index: 0, },
jump: LabelSlot {
index: 2,
},
}, },
jump: 5, AppendContent {
}, content: " " (12..13),
AppendContent {
content: " " (12..13),
},
AppendContent {
content: "foo" (13..16),
},
AppendContent {
content: " " (16..17),
},
Jump {
jump: 14,
},
AppendContent {
content: " " (12..13),
},
LoadFromContextToSlot {
name: "bar" (28..31),
slot: VariableSlot {
index: 1,
}, },
}, AppendContent {
JumpIfNotTrue { content: "foo" (13..16),
emit_slot: VariableSlot {
index: 1,
}, },
jump: 5, AppendContent {
}, content: " " (16..17),
AppendContent { },
content: " " (34..35), Jump {
}, jump: LabelSlot {
AppendContent { index: 0,
content: "bar" (35..38), },
}, },
AppendContent { AppendContent {
content: " " (38..39), content: " " (12..13),
}, },
Jump { LoadFromContextToSlot {
jump: 7, name: "bar" (28..31),
}, slot: VariableSlot {
AppendContent { index: 3,
content: " " (34..35), },
}, },
AppendContent { JumpIfNotTrue {
content: " " (49..50), emit_slot: VariableSlot {
}, index: 3,
AppendContent { },
content: "foobar" (50..56), jump: LabelSlot {
}, index: 4,
AppendContent { },
content: " " (56..57), },
}, AppendContent {
Jump { content: " " (34..35),
jump: 2, },
}, AppendContent {
AppendContent { content: "bar" (35..38),
content: " " (49..50), },
}, AppendContent {
NoOp, content: " " (38..39),
] },
Jump {
jump: LabelSlot {
index: 0,
},
},
AppendContent {
content: " " (34..35),
},
AppendContent {
content: " " (49..50),
},
AppendContent {
content: "foobar" (50..56),
},
AppendContent {
content: " " (56..57),
},
Jump {
jump: LabelSlot {
index: 0,
},
},
AppendContent {
content: " " (49..50),
},
NoOp,
],
}

View file

@ -5,6 +5,7 @@ use thiserror::Error;
use crate::Context; use crate::Context;
use crate::emit::Instruction; use crate::emit::Instruction;
use crate::emit::VMInstructions;
use crate::input::NomoInput; use crate::input::NomoInput;
#[derive(Debug, Error, Display)] #[derive(Debug, Error, Display)]
@ -17,24 +18,21 @@ pub enum EvaluationError {
** **
** This is an internal error and is a bug that should be reported ** This is an internal error and is a bug that should be reported
*/ */
InstructionPointerOverflow, LabelNotFound,
} }
pub fn execute( pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result<String, EvaluationError> {
instructions: &[Instruction],
global_context: &Context,
) -> Result<String, EvaluationError> {
let mut output = String::new(); let mut output = String::new();
let mut scopes: HashMap<crate::emit::VariableSlot, serde_json::Value> = HashMap::new(); let mut scopes: HashMap<crate::emit::VariableSlot, serde_json::Value> = HashMap::new();
let mut ip = 0; let mut ip = 0;
loop { loop {
if ip >= instructions.len() { if ip >= vm.instructions.len() {
break; break;
} }
let instr = instructions.get(ip).unwrap(); let instr = vm.instructions.get(ip).unwrap();
match instr { match instr {
Instruction::NoOp => (), Instruction::NoOp => (),
@ -58,25 +56,21 @@ pub fn execute(
if dont_jump { if dont_jump {
// We are done // We are done
} else { } else {
let (new_ip, overflow) = ip.overflowing_add_signed(*jump); let Some(new_ip) = vm.labels.get(jump) else {
return Err(EvaluationError::LabelNotFound);
};
if overflow { ip = *new_ip;
return Err(EvaluationError::InstructionPointerOverflow); continue;
} else {
ip = new_ip;
continue;
}
} }
} }
Instruction::Jump { jump } => { Instruction::Jump { jump } => {
let (new_ip, overflow) = ip.overflowing_add_signed(*jump); let Some(new_ip) = vm.labels.get(jump) else {
return Err(EvaluationError::LabelNotFound);
};
if overflow { ip = *new_ip;
return Err(EvaluationError::InstructionPointerOverflow); continue;
} else {
ip = new_ip;
continue;
}
} }
} }

View file

@ -6,6 +6,7 @@ use serde::Serialize;
use thiserror::Error; use thiserror::Error;
use crate::emit::Instruction; use crate::emit::Instruction;
use crate::emit::VMInstructions;
use crate::input::NomoInput; use crate::input::NomoInput;
pub mod ast; pub mod ast;
@ -84,7 +85,7 @@ impl Nomo {
} }
struct Template { struct Template {
instructions: Vec<Instruction>, instructions: VMInstructions,
} }
pub struct Context { pub struct Context {

View file

@ -3,46 +3,60 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/condition.nomo input_file: tests/cases/condition.nomo
--- ---
[ VMInstructions {
LoadFromContextToSlot { labels: {
name: "test" (6..10), LabelSlot {
slot: VariableSlot { index: 2,
}: 7,
LabelSlot {
index: 0, index: 0,
}: 7,
},
instructions: [
LoadFromContextToSlot {
name: "test" (6..10),
slot: VariableSlot {
index: 1,
},
}, },
}, JumpIfNotTrue {
JumpIfNotTrue { emit_slot: VariableSlot {
emit_slot: VariableSlot { index: 1,
index: 0, },
jump: LabelSlot {
index: 2,
},
}, },
jump: 5, AppendContent {
}, content: "\n " (13..18),
AppendContent {
content: "\n " (13..18),
},
AppendContent {
content: "Hello World!" (18..30),
},
AppendContent {
content: "\n" (30..31),
},
Jump {
jump: 2,
},
AppendContent {
content: "\n " (13..18),
},
AppendContent {
content: "\n\n" (40..42),
},
LoadFromContextToSlot {
name: "stuff" (46..51),
slot: VariableSlot {
index: 1,
}, },
}, AppendContent {
EmitFromSlot { content: "Hello World!" (18..30),
slot: VariableSlot {
index: 1,
}, },
}, AppendContent {
] content: "\n" (30..31),
},
Jump {
jump: LabelSlot {
index: 0,
},
},
AppendContent {
content: "\n " (13..18),
},
AppendContent {
content: "\n\n" (40..42),
},
LoadFromContextToSlot {
name: "stuff" (46..51),
slot: VariableSlot {
index: 3,
},
},
EmitFromSlot {
slot: VariableSlot {
index: 3,
},
},
],
}

View file

@ -3,86 +3,89 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/identifiers.nomo input_file: tests/cases/identifiers.nomo
--- ---
[ VMInstructions {
LoadFromContextToSlot { labels: {},
name: "_name" (4..9), instructions: [
slot: VariableSlot { LoadFromContextToSlot {
index: 0, name: "_name" (4..9),
slot: VariableSlot {
index: 0,
},
}, },
}, EmitFromSlot {
EmitFromSlot { slot: VariableSlot {
slot: VariableSlot { index: 0,
index: 0, },
}, },
}, AppendContent {
AppendContent { content: "\n" (12..13),
content: "\n" (12..13),
},
LoadFromContextToSlot {
name: "a_name" (17..23),
slot: VariableSlot {
index: 1,
}, },
}, LoadFromContextToSlot {
EmitFromSlot { name: "a_name" (17..23),
slot: VariableSlot { slot: VariableSlot {
index: 1, index: 1,
},
}, },
}, EmitFromSlot {
AppendContent { slot: VariableSlot {
content: "\n" (26..27), index: 1,
}, },
LoadFromContextToSlot {
name: "1name" (31..36),
slot: VariableSlot {
index: 2,
}, },
}, AppendContent {
EmitFromSlot { content: "\n" (26..27),
slot: VariableSlot {
index: 2,
}, },
}, LoadFromContextToSlot {
AppendContent { name: "1name" (31..36),
content: "\n" (39..40), slot: VariableSlot {
}, index: 2,
LoadFromContextToSlot { },
name: "_name1" (44..50),
slot: VariableSlot {
index: 3,
}, },
}, EmitFromSlot {
EmitFromSlot { slot: VariableSlot {
slot: VariableSlot { index: 2,
index: 3, },
}, },
}, AppendContent {
AppendContent { content: "\n" (39..40),
content: "\n" (53..54),
},
LoadFromContextToSlot {
name: "_namE" (58..63),
slot: VariableSlot {
index: 4,
}, },
}, LoadFromContextToSlot {
EmitFromSlot { name: "_name1" (44..50),
slot: VariableSlot { slot: VariableSlot {
index: 4, index: 3,
},
}, },
}, EmitFromSlot {
AppendContent { slot: VariableSlot {
content: "\n" (66..67), index: 3,
}, },
LoadFromContextToSlot {
name: "name1" (71..76),
slot: VariableSlot {
index: 5,
}, },
}, AppendContent {
EmitFromSlot { content: "\n" (53..54),
slot: VariableSlot {
index: 5,
}, },
}, LoadFromContextToSlot {
] name: "_namE" (58..63),
slot: VariableSlot {
index: 4,
},
},
EmitFromSlot {
slot: VariableSlot {
index: 4,
},
},
AppendContent {
content: "\n" (66..67),
},
LoadFromContextToSlot {
name: "name1" (71..76),
slot: VariableSlot {
index: 5,
},
},
EmitFromSlot {
slot: VariableSlot {
index: 5,
},
},
],
}

View file

@ -3,60 +3,81 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/if_else_if.nomo input_file: tests/cases/if_else_if.nomo
--- ---
[ VMInstructions {
LoadFromContextToSlot { labels: {
name: "test" (6..10), LabelSlot {
slot: VariableSlot { index: 2,
}: 7,
LabelSlot {
index: 4,
}: 14,
LabelSlot {
index: 0, index: 0,
}: 14,
},
instructions: [
LoadFromContextToSlot {
name: "test" (6..10),
slot: VariableSlot {
index: 1,
},
}, },
}, JumpIfNotTrue {
JumpIfNotTrue { emit_slot: VariableSlot {
emit_slot: VariableSlot { index: 1,
index: 0, },
jump: LabelSlot {
index: 2,
},
}, },
jump: 5, AppendContent {
}, content: "\n " (13..18),
AppendContent {
content: "\n " (13..18),
},
AppendContent {
content: "Not Hello World! :C" (18..37),
},
AppendContent {
content: "\n" (37..38),
},
Jump {
jump: 9,
},
AppendContent {
content: "\n " (13..18),
},
LoadFromContextToSlot {
name: "another_test" (49..61),
slot: VariableSlot {
index: 1,
}, },
}, AppendContent {
JumpIfNotTrue { content: "Not Hello World! :C" (18..37),
emit_slot: VariableSlot {
index: 1,
}, },
jump: 5, AppendContent {
}, content: "\n" (37..38),
AppendContent { },
content: "\n " (64..69), Jump {
}, jump: LabelSlot {
AppendContent { index: 0,
content: "Hello World!" (69..81), },
}, },
AppendContent { AppendContent {
content: "\n" (81..82), content: "\n " (13..18),
}, },
Jump { LoadFromContextToSlot {
jump: 2, name: "another_test" (49..61),
}, slot: VariableSlot {
AppendContent { index: 3,
content: "\n " (64..69), },
}, },
NoOp, JumpIfNotTrue {
] emit_slot: VariableSlot {
index: 3,
},
jump: LabelSlot {
index: 4,
},
},
AppendContent {
content: "\n " (64..69),
},
AppendContent {
content: "Hello World!" (69..81),
},
AppendContent {
content: "\n" (81..82),
},
Jump {
jump: LabelSlot {
index: 0,
},
},
AppendContent {
content: "\n " (64..69),
},
NoOp,
],
}

View file

@ -3,22 +3,25 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/interpolation.nomo input_file: tests/cases/interpolation.nomo
--- ---
[ VMInstructions {
AppendContent { labels: {},
content: "Hello! I'm" (0..10), instructions: [
}, AppendContent {
AppendContent { content: "Hello! I'm" (0..10),
content: " " (10..11),
},
LoadFromContextToSlot {
name: "name" (15..19),
slot: VariableSlot {
index: 0,
}, },
}, AppendContent {
EmitFromSlot { content: " " (10..11),
slot: VariableSlot {
index: 0,
}, },
}, LoadFromContextToSlot {
] name: "name" (15..19),
slot: VariableSlot {
index: 0,
},
},
EmitFromSlot {
slot: VariableSlot {
index: 0,
},
},
],
}

View file

@ -3,36 +3,39 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/multiple.nomo input_file: tests/cases/multiple.nomo
--- ---
[ VMInstructions {
AppendContent { labels: {},
content: "Hi there! My name is" (0..20), instructions: [
}, AppendContent {
AppendContent { content: "Hi there! My name is" (0..20),
content: " " (20..21),
},
LoadFromContextToSlot {
name: "name" (25..29),
slot: VariableSlot {
index: 0,
}, },
}, AppendContent {
EmitFromSlot { content: " " (20..21),
slot: VariableSlot {
index: 0,
}, },
}, LoadFromContextToSlot {
AppendContent { name: "name" (25..29),
content: " " (32..33), slot: VariableSlot {
}, index: 0,
LoadFromContextToSlot { },
name: "lastname" (37..45),
slot: VariableSlot {
index: 1,
}, },
}, EmitFromSlot {
EmitFromSlot { slot: VariableSlot {
slot: VariableSlot { index: 0,
index: 1, },
}, },
}, AppendContent {
] content: " " (32..33),
},
LoadFromContextToSlot {
name: "lastname" (37..45),
slot: VariableSlot {
index: 1,
},
},
EmitFromSlot {
slot: VariableSlot {
index: 1,
},
},
],
}

View file

@ -3,8 +3,11 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/simple.nomo input_file: tests/cases/simple.nomo
--- ---
[ VMInstructions {
AppendContent { labels: {},
content: "Hello World!" (0..12), instructions: [
}, AppendContent {
] content: "Hello World!" (0..12),
},
],
}

View file

@ -3,38 +3,52 @@ source: tests/file_tests.rs
expression: emit expression: emit
input_file: tests/cases/trim_whitespace.nomo input_file: tests/cases/trim_whitespace.nomo
--- ---
[ VMInstructions {
LoadFromContextToSlot { labels: {
name: "test" (6..10), LabelSlot {
slot: VariableSlot { index: 2,
}: 7,
LabelSlot {
index: 0, index: 0,
}: 7,
},
instructions: [
LoadFromContextToSlot {
name: "test" (6..10),
slot: VariableSlot {
index: 1,
},
}, },
}, JumpIfNotTrue {
JumpIfNotTrue { emit_slot: VariableSlot {
emit_slot: VariableSlot { index: 1,
index: 0, },
jump: LabelSlot {
index: 2,
},
}, },
jump: 5, AppendContent {
}, content: "Hello" (19..24),
AppendContent {
content: "Hello" (19..24),
},
AppendContent {
content: " " (24..25),
},
LoadFromContextToSlot {
name: "stuff" (29..34),
slot: VariableSlot {
index: 1,
}, },
}, AppendContent {
EmitFromSlot { content: " " (24..25),
slot: VariableSlot {
index: 1,
}, },
}, LoadFromContextToSlot {
Jump { name: "stuff" (29..34),
jump: 1, slot: VariableSlot {
}, index: 3,
NoOp, },
] },
EmitFromSlot {
slot: VariableSlot {
index: 3,
},
},
Jump {
jump: LabelSlot {
index: 0,
},
},
NoOp,
],
}

View file

@ -3,4 +3,4 @@ source: tests/file_tests.rs
expression: output expression: output
input_file: tests/cases/if_else_if.nomo input_file: tests/cases/if_else_if.nomo
--- ---
"\n \n Hello World!\n" "\n Hello World!\n"