Virtual machine execution
This commit is contained in:
parent
6e0a7375c0
commit
8064118490
|
|
@ -5,3 +5,4 @@
|
|||
pub mod execute;
|
||||
pub mod parse;
|
||||
mod parse_backup;
|
||||
pub(crate) mod vm;
|
||||
|
|
|
|||
509
src/internal/vm.rs
Normal file
509
src/internal/vm.rs
Normal 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 "
|
||||
)
|
||||
}
|
||||
}
|
||||
138
src/lib.rs
138
src/lib.rs
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue