Virtual machine execution

This commit is contained in:
soup 2024-05-31 15:25:02 -04:00
parent 6e0a7375c0
commit 8064118490
No known key found for this signature in database
3 changed files with 568 additions and 80 deletions

View file

@ -5,3 +5,4 @@
pub mod execute;
pub mod parse;
mod parse_backup;
pub(crate) mod vm;

509
src/internal/vm.rs Normal file
View file

@ -0,0 +1,509 @@
use std::fmt::Write;
use crate::Value;
use self::ops::{Op, OpRef, Ops};
pub(crate) mod ops {
#[cfg(test)]
use crate::go;
use crate::internal::parse::{AstNodeKind, Path};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(super) struct OpRef(pub(super) u32);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum Op {
Noop,
Jump(OpRef),
JumpFalse(OpRef),
Exit,
Emit(String),
ValueDup,
ValuePop,
ValueLookup(String),
ValueIndex,
ValuePrint,
ValueTruthy,
ValueHas,
CounterDup,
CounterPop,
CounterIncrement,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct Ops(Vec<Op>);
impl Ops {
pub(crate) fn new() -> Self {
Self(vec![])
}
fn next_ref(&self) -> OpRef {
let next = OpRef(
self.0
.len()
.try_into()
.expect("Emitted too many instructions"),
);
next
}
fn emit(&mut self, op: Op) -> OpRef {
let next = self.next_ref();
self.0.push(op);
next
}
fn get_mut(&mut self, rf: OpRef) -> &mut Op {
self.0.get_mut(rf.0 as usize).expect("OpRef out of bounds")
}
pub(super) fn get(&self, rf: OpRef) -> &Op {
self.0.get(rf.0 as usize).expect("OpRef out of bounds")
}
}
pub(crate) fn lower_ast(ast: &[AstNodeKind]) -> Ops {
let mut ops = Ops::new();
lower_many(&mut ops, &ast);
ops.emit(Op::Exit);
ops
}
fn lower_many(ops: &mut Ops, nodes: &[AstNodeKind]) {
for node in nodes {
lower(ops, node);
}
}
fn lower(ops: &mut Ops, node: &AstNodeKind) {
match node {
AstNodeKind::Text(p) => {
ops.emit(Op::Emit(p.clone()));
},
AstNodeKind::Print(p) => {
path_stack(ops, &p.path, |ops| {
ops.emit(Op::ValuePrint);
});
},
AstNodeKind::For(f) => path_stack(ops, &f.path, |ops| {
ops.emit(Op::CounterDup);
let dup = ops.emit(Op::ValueDup);
ops.emit(Op::ValueIndex);
let jump_end = ops.emit(Op::Noop);
ops.emit(Op::CounterIncrement);
lower_many(ops, &f.body);
ops.emit(Op::ValuePop);
ops.emit(Op::Jump(dup));
let counter_pop = ops.emit(Op::CounterPop);
ops.emit(Op::ValuePop);
*ops.get_mut(jump_end) = Op::JumpFalse(counter_pop);
}),
AstNodeKind::With(w) => path_stack(ops, &w.path, |ops| {
lower_many(ops, &w.body);
}),
AstNodeKind::If(i) => {
path_stack(ops, &i.path, |ops| {
ops.emit(Op::ValueTruthy);
});
let jump_else = ops.emit(Op::Noop);
lower_many(ops, &i.then);
if let Some(else_) = i.else_.as_ref() {
let jump_end = ops.emit(Op::Noop);
*ops.get_mut(jump_else) = Op::JumpFalse(ops.next_ref());
lower_many(ops, else_);
*ops.get_mut(jump_end) = Op::Jump(ops.next_ref());
} else {
*ops.get_mut(jump_else) = Op::JumpFalse(ops.next_ref());
};
},
AstNodeKind::Has(h) => path_stack(ops, &h.path, |ops| {
ops.emit(Op::ValueHas);
let jump_else = ops.emit(Op::Noop);
lower_many(ops, &h.then);
let next_ref = ops.next_ref();
*ops.get_mut(jump_else) = Op::JumpFalse(next_ref);
if let Some(else_) = h.else_.as_ref() {
let jump_end = ops.emit(Op::Noop);
*ops.get_mut(jump_else) = Op::JumpFalse(ops.next_ref());
lower_many(ops, else_);
*ops.get_mut(jump_end) = Op::Jump(ops.next_ref());
} else {
*ops.get_mut(jump_else) = Op::JumpFalse(ops.next_ref());
};
}),
}
}
fn path_stack(ops: &mut Ops, path: &Path, then: impl FnOnce(&mut Ops)) {
if path.0.is_empty() {
then(ops);
} else {
ops.emit(Op::ValueDup);
for path in &path.0 {
ops.emit(Op::ValueLookup(path.clone()));
}
then(ops);
ops.emit(Op::ValuePop);
}
}
#[cfg(test)]
mod test {
#[macro_export]
macro_rules! go {
($src:expr, $expected:expr) => {
assert_eq!(
crate::internal::vm::ops::lower_ast(
&crate::internal::parse::parse($src)
),
Ops($expected),
)
};
}
}
#[test]
fn test_lower_text() {
go!(
"Hello, world!",
vec![Op::Emit("Hello, world!".to_string()), Op::Exit]
)
}
#[test]
fn test_lower_print() {
go!("{.}", vec![Op::ValuePrint, Op::Exit])
}
#[test]
fn test_lower_print_lookup() {
go!(
"{.test}",
vec![
Op::ValueDup,
Op::ValueLookup("test".to_string()),
Op::ValuePrint,
Op::ValuePop,
Op::Exit,
]
)
}
#[test]
fn test_lower_for_print_lookup() {
go!(
"{for .test}{.}{/for}",
vec![
Op::ValueDup,
Op::ValueLookup("test".to_string()),
Op::CounterDup,
Op::ValueDup,
Op::ValueIndex,
Op::JumpFalse(OpRef(10)),
Op::CounterIncrement,
Op::ValuePrint,
Op::ValuePop,
Op::Jump(OpRef(3)),
Op::CounterPop,
Op::ValuePop,
Op::ValuePop,
Op::Exit,
]
)
}
#[test]
fn test_lower_with() {
go!(
"{with .test}{.}{/with}",
vec![
Op::ValueDup,
Op::ValueLookup("test".to_string()),
Op::ValuePrint,
Op::ValuePop,
Op::Exit,
]
)
}
#[test]
fn test_lower_if() {
go!(
"{if .test}Then{else}Else{/if}",
vec![
Op::ValueDup,
Op::ValueLookup("test".to_string()),
Op::ValueTruthy,
Op::ValuePop,
Op::JumpFalse(OpRef(7)),
Op::Emit("Then".to_string()),
Op::Jump(OpRef(8)),
Op::Emit("Else".to_string()),
Op::Exit,
]
)
}
#[test]
fn test_lower_if_no_else() {
go!(
"{if .test}Then{/if}",
vec![
Op::ValueDup,
Op::ValueLookup("test".to_string()),
Op::ValueTruthy,
Op::ValuePop,
Op::JumpFalse(OpRef(6)),
Op::Emit("Then".to_string()),
Op::Exit,
]
)
}
}
#[derive(Debug)]
pub(crate) struct VmState<'a> {
reg_cond: bool,
reg_pc: u32,
reg_value: &'a dyn Value,
reg_counter: usize,
stack_values: Vec<&'a dyn Value>,
stack_counters: Vec<usize>,
output: String,
ops: &'a Ops,
}
impl<'a> VmState<'a> {
pub(crate) fn new(ops: &'a Ops, value: &'a dyn Value) -> Self {
Self {
reg_cond: false,
reg_pc: 0,
reg_value: value,
reg_counter: 0,
stack_values: vec![value],
stack_counters: vec![0],
output: String::new(),
ops,
}
}
pub(crate) fn execute(mut self) -> String {
self.execute_loop();
self.output
}
fn execute_loop(&mut self) {
loop {
let should_terminate = self.execute_one();
if should_terminate {
break;
}
}
}
fn execute_one(&mut self) -> bool {
let op = self.ops.get(OpRef(self.reg_pc));
let mut op_offset = 1;
match op {
Op::Exit => return true,
Op::Noop => (),
Op::Jump(r) => {
self.reg_pc = r.0;
op_offset = 0;
},
Op::JumpFalse(r) => {
if !self.reg_cond {
self.reg_pc = r.0;
op_offset = 0;
}
},
Op::Emit(s) => {
self.output.push_str(s);
},
Op::ValueDup => {
self.stack_values.push(self.reg_value);
},
Op::ValuePop => {
self.reg_value =
self.stack_values.pop().expect("stack_values is empty");
},
Op::ValueLookup(field) => {
let new_value = self.reg_value.lookup(&field);
match new_value {
Some(v) => {
self.reg_value = v;
},
None => {
let _ = write!(
&mut self.output,
"Field `{field}` missing in current value"
);
},
}
},
Op::ValueIndex => {
let value = self.reg_value.for_(self.reg_counter);
if let Some(value) = value {
self.reg_value = value;
self.reg_cond = true;
} else {
self.reg_cond = false;
}
},
Op::ValuePrint => {
self.reg_value.print(&mut self.output);
},
Op::ValueTruthy => {
self.reg_cond = self.reg_value.if_();
},
Op::ValueHas => {
if let Some(value) = self.reg_value.has() {
self.reg_value = value;
self.reg_cond = true;
} else {
self.reg_cond = false;
}
},
Op::CounterDup => {
self.stack_counters.push(self.reg_counter);
},
Op::CounterPop => {
self.reg_counter =
self.stack_counters.pop().expect("stack_counters is empty");
},
Op::CounterIncrement => {
self.reg_counter += 1;
},
};
self.reg_pc += op_offset;
false
}
}
#[cfg(test)]
mod test {
use super::ops::lower_ast;
use super::VmState;
use crate::internal::parse::parse;
use crate::{values, values_map};
macro_rules! go {
($input:expr, $value:expr, $expected:expr) => {{
let ast = parse($input);
let ops = lower_ast(&ast);
let output = VmState::new(&ops, $value).execute();
assert_eq!(output, $expected);
}};
}
#[test]
fn basic() {
go!("Hello, world!", &(), "Hello, world!");
}
#[test]
fn if_else() {
go!("{if .}Hello{else}Goodbye{/if}", &true, "Hello");
go!("{if .}Hello{else}Goodbye{/if}", &false, "Goodbye");
}
#[test]
fn if_else_lookup() {
go!(
"{if .name}Hello, {.name}!{else}Goodbye{/if}",
values_map! { name: "Bob" },
"Hello, Bob!"
);
go!(
"{if .name}Hello, {.name}{else}Goodbye{/if}",
values_map! { name: "" },
"Goodbye"
);
}
#[test]
fn print() {
go!("{.}", &"Gorilla", "Gorilla");
}
#[test]
fn print_lookup() {
go!("{.name}", values_map! { name: &"Gorilla" }, "Gorilla");
}
#[test]
fn has() {
go!(
"{has .name}{.}{else}No name{/has}",
&values_map! {
name: Some("Bob"),
},
"Bob"
);
go!(
"{has .name}{.}{else}No name{/has}",
&values_map! {
name: None::<&str>,
},
"No name"
)
}
#[test]
fn with() {
go!(
"{with .name}{.}{/with}",
&values_map! {
name: "Bob",
},
"Bob"
)
}
#[test]
fn for_() {
go!(
"{for .names}{.}, {/for}",
&values_map! {
names: values!("Bob", "Larry"),
},
"Bob, Larry, "
)
}
#[test]
fn complex_1() {
go!(
r"{for .}{if .}True {else}False {/if}{/for}",
values!(true, false),
"True False "
)
}
#[test]
fn complex_2() {
go!(
r"{for .bools}{if .}True {else}False {/if}{/for}",
values_map! {
bools: values!(true, false),
},
"True False "
)
}
}

View file

@ -5,9 +5,13 @@ pub mod internal;
use internal::{
execute::execute,
parse::{parse, Ast},
vm::{
ops::{lower_ast, Ops},
VmState,
},
};
pub trait Value {
pub trait Value: core::fmt::Debug {
fn print(&self, out: &mut String);
fn lookup(&self, name: &str) -> Option<&dyn Value>;
fn has(&self) -> Option<&dyn Value>;
@ -202,6 +206,7 @@ where
}
}
#[derive(Debug)]
pub struct ValuesListMap<'a>(pub &'a [(&'a str, &'a dyn Value)]);
impl<'a> Value for ValuesListMap<'a> {
fn print(&self, w: &mut String) {
@ -327,27 +332,72 @@ where
}
}
impl Value for () {
fn print(&self, _: &mut String) {
}
fn lookup(&self, _: &str) -> Option<&dyn Value> {
None
}
fn has(&self) -> Option<&dyn Value> {
None
}
fn if_(&self) -> bool {
true
}
fn for_(&self, _: usize) -> Option<&dyn Value> {
None
}
}
impl Value for bool {
fn print(&self, out: &mut String) {
let _ = write!(out, "{}", self);
}
fn lookup(&self, _name: &str) -> Option<&dyn Value> {
None
}
fn has(&self) -> Option<&dyn Value> {
Some(self)
}
fn if_(&self) -> bool {
*self
}
fn for_(&self, _index: usize) -> Option<&dyn Value> {
None
}
}
pub struct Newt {
ast: Ast,
ops: Ops,
}
impl Newt {
pub fn build(tmpl: &str) -> Self {
let ast = parse(tmpl);
Self { ast: todo!() }
let ops = lower_ast(&ast);
Self { ops }
}
pub fn execute(&self, value: &dyn Value) -> String {
execute(&self.ast, value)
VmState::new(&self.ops, value).execute()
}
}
#[macro_export]
macro_rules! values_list_map {
($($key:literal: $val:expr),* $(,)?) => {{
macro_rules! values_map {
($($key:path: $val:expr),* $(,)?) => {{
&$crate::ValuesListMap(
&[
$(
($key, $val as &dyn Value)
(stringify!($key), &$val as &dyn $crate::Value)
)*,
]
)
@ -359,80 +409,8 @@ macro_rules! values {
($($val:expr),* $(,)?) => {{
&[
$(
&$val as &dyn Value,
&$val as &dyn $crate::Value,
)*
]
}}
}
#[cfg(test)]
mod test {
use crate::{Newt, Value};
fn go(tmpl: &str, value: &dyn Value, expected: &str) {
assert_eq!(Newt::build(tmpl).execute(value), expected)
}
#[test]
fn simple_print() {
go("Hello, {.}!", &"World", "Hello, World!")
}
#[test]
fn print_lookup() {
go(
"Hello, {.name}!",
values_list_map! {
"name": &"Ted",
},
"Hello, Ted!",
);
}
#[test]
fn print_for() {
go(
"{for .}{.}, {/for}",
values!["Bob", "Larry"],
"Bob, Larry, ",
);
}
#[test]
fn print_for_nested() {
go(
"{for .items}{for .names}{.}, {/for}{/for}",
values_list_map! {
"items": values![values_list_map! {
"names": values!["Bob", "Larry", 0],
}]
},
"Bob, Larry, 0, ",
)
}
#[test]
fn print_has() {
go("{has .}{.}{/has}", &Some("Bob"), "Bob");
}
#[test]
fn print_has_none() {
go("{has .}{.}{/has}", &None::<&str>, "");
}
#[test]
fn print_if() {
go("{if .}{.}{/if}", &"Bob", "Bob");
}
#[test]
fn print_if_false() {
go("{if .}{.}{/if}", &"", "");
}
#[test]
fn print_if_false_else() {
go("{if .}{.}{else}Frank{/if}", &"", "Frank");
}
}