371 lines
12 KiB
Rust
371 lines
12 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use displaydoc::Display;
|
|
use thiserror::Error;
|
|
|
|
use crate::Context;
|
|
use crate::compiler::Instruction;
|
|
use crate::compiler::VMInstructions;
|
|
use crate::compiler::VariableSlot;
|
|
use crate::functions::FunctionMap;
|
|
use crate::input::NomoInput;
|
|
use crate::value::NomoValue;
|
|
|
|
#[derive(Debug, Error, Display)]
|
|
pub enum EvaluationError {
|
|
/// An unknown variable was encountered: .0
|
|
UnknownVariable(NomoInput),
|
|
/// An explicit abort was requested
|
|
ExplicitAbort,
|
|
/** The instruction pointer overflowed
|
|
**
|
|
** This is an internal error and is a bug that should be reported
|
|
*/
|
|
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(
|
|
available_functions: &FunctionMap,
|
|
vm: &VMInstructions,
|
|
global_context: &Context,
|
|
) -> Result<String, EvaluationError> {
|
|
let mut output = String::new();
|
|
|
|
let mut scopes = Scope {
|
|
stack: vec![global_context.values().clone()],
|
|
slots: HashMap::new(),
|
|
};
|
|
|
|
let mut ip = 0;
|
|
loop {
|
|
if ip >= vm.instructions.len() {
|
|
break;
|
|
}
|
|
|
|
let instr = vm.instructions.get(ip).unwrap();
|
|
|
|
match instr {
|
|
Instruction::NoOp => (),
|
|
Instruction::AppendContent { content } => output.push_str(content),
|
|
Instruction::LoadFromContextToSlot {
|
|
name,
|
|
slot,
|
|
fail_on_not_found,
|
|
} => {
|
|
let value = scopes.get_scoped(name);
|
|
|
|
let value = if let Some(val) = value {
|
|
val
|
|
} else {
|
|
if *fail_on_not_found {
|
|
return Err(EvaluationError::UnknownVariable(name.clone()));
|
|
} else {
|
|
&NomoValue::Undefined
|
|
}
|
|
};
|
|
|
|
scopes.insert_into_slot(*slot, value.clone());
|
|
}
|
|
Instruction::EmitFromSlot { slot } => {
|
|
let value = scopes.get(slot);
|
|
let value = if let Some(value) = value.try_to_string() {
|
|
value
|
|
} else if matches!(value, NomoValue::Undefined) {
|
|
String::new()
|
|
} else {
|
|
panic!("Could not print out value {value:?}");
|
|
};
|
|
|
|
output.push_str(&value);
|
|
}
|
|
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).as_bool().unwrap();
|
|
if dont_jump {
|
|
// We are done
|
|
} else {
|
|
let Some(new_ip) = vm.labels.get(jump) else {
|
|
return Err(EvaluationError::LabelNotFound);
|
|
};
|
|
|
|
ip = *new_ip;
|
|
continue;
|
|
}
|
|
}
|
|
Instruction::Jump { jump } => {
|
|
let Some(new_ip) = vm.labels.get(jump) else {
|
|
return Err(EvaluationError::LabelNotFound);
|
|
};
|
|
|
|
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;
|
|
continue;
|
|
}
|
|
}
|
|
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;
|
|
continue;
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
Instruction::LoadLiteralToSlot {
|
|
source: _,
|
|
value,
|
|
slot,
|
|
} => {
|
|
scopes.insert_into_slot(*slot, value.clone());
|
|
}
|
|
Instruction::MathOperate {
|
|
op,
|
|
left_slot,
|
|
right_slot,
|
|
result_slot,
|
|
} => {
|
|
let left_value = scopes.get(left_slot);
|
|
let right_value = scopes.get(right_slot);
|
|
|
|
let result = match op {
|
|
crate::lexer::TokenOperator::Plus => left_value.try_add(right_value),
|
|
crate::lexer::TokenOperator::Minus => left_value.try_sub(right_value),
|
|
crate::lexer::TokenOperator::Times => left_value.try_mul(right_value),
|
|
crate::lexer::TokenOperator::Divide => left_value.try_div(right_value),
|
|
crate::lexer::TokenOperator::And => left_value.try_and(right_value),
|
|
crate::lexer::TokenOperator::Or => left_value.try_or(right_value),
|
|
_ => todo!(),
|
|
};
|
|
|
|
scopes.insert_into_slot(*result_slot, result.unwrap());
|
|
}
|
|
Instruction::FunctionCall { name, args, slot } => {
|
|
let args = args.iter().map(|slot| scopes.get(slot)).cloned().collect();
|
|
|
|
let value = available_functions
|
|
.get(name.as_str())
|
|
.unwrap()
|
|
.call(args)
|
|
.unwrap();
|
|
|
|
scopes.insert_into_slot(*slot, value);
|
|
}
|
|
Instruction::LoadFromSlotToSlot { from_slot, to_slot } => {
|
|
let value = scopes.get(from_slot);
|
|
scopes.insert_into_slot(*to_slot, value.clone());
|
|
}
|
|
Instruction::JumpIfUndefined { slot, jump } => {
|
|
let is_undefined = matches!(scopes.get(slot), NomoValue::Undefined);
|
|
if is_undefined {
|
|
let Some(new_ip) = vm.labels.get(jump) else {
|
|
return Err(EvaluationError::LabelNotFound);
|
|
};
|
|
|
|
ip = *new_ip;
|
|
continue;
|
|
}
|
|
}
|
|
Instruction::IndexSlotToSlot {
|
|
name,
|
|
from_slot,
|
|
to_slot,
|
|
fail_on_not_found,
|
|
} => {
|
|
let value = scopes.get(from_slot);
|
|
|
|
match value {
|
|
NomoValue::Object { value } => {
|
|
let value = value.get(name.as_str());
|
|
|
|
let value = if let Some(value) = value {
|
|
value.clone()
|
|
} else if *fail_on_not_found {
|
|
panic!("Could not index");
|
|
} else {
|
|
NomoValue::Undefined
|
|
};
|
|
|
|
scopes.insert_into_slot(dbg!(*to_slot), dbg!(value));
|
|
}
|
|
_ => panic!("Invalid indexing"),
|
|
}
|
|
}
|
|
}
|
|
|
|
ip += 1;
|
|
}
|
|
|
|
Ok(output)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::Context;
|
|
use crate::eval::execute;
|
|
use crate::functions::FunctionMap;
|
|
use crate::functions::NomoFunctionError;
|
|
use crate::input::NomoInput;
|
|
|
|
#[test]
|
|
fn check_simple_variable_interpolation() {
|
|
let input = NomoInput::from("Hello {{= world }}");
|
|
|
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
|
|
|
let ast = crate::parser::parse(input, parsed.tokens()).unwrap();
|
|
|
|
let emit = crate::compiler::emit_machine(ast);
|
|
|
|
let mut context = Context::new();
|
|
context.insert("world", "World");
|
|
let output = execute(&FunctionMap::default(), &emit, &context);
|
|
|
|
insta::assert_debug_snapshot!(output, @r#"
|
|
Ok(
|
|
"Hello World",
|
|
)
|
|
"#);
|
|
}
|
|
|
|
#[test]
|
|
fn check_method_call() {
|
|
let input = NomoInput::from("Hello {{= foo(world) }}");
|
|
|
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
|
|
|
let ast = crate::parser::parse(input, parsed.tokens()).unwrap();
|
|
|
|
let emit = crate::compiler::emit_machine(ast);
|
|
|
|
let mut context = Context::new();
|
|
context.insert("world", "World");
|
|
let mut function_map = FunctionMap::default();
|
|
|
|
function_map.register("foo", |arg: String| -> Result<String, NomoFunctionError> {
|
|
Ok(arg.to_uppercase())
|
|
});
|
|
|
|
let output = execute(&function_map, &emit, &context);
|
|
|
|
insta::assert_debug_snapshot!(output, @r#"
|
|
Ok(
|
|
"Hello WORLD",
|
|
)
|
|
"#);
|
|
}
|
|
|
|
#[test]
|
|
fn check_conditional_access() {
|
|
let input = NomoInput::from("Hello {{= unknown? }}");
|
|
|
|
let parsed = crate::lexer::parse(input.clone()).unwrap();
|
|
|
|
let ast = crate::parser::parse(input, parsed.tokens()).unwrap();
|
|
|
|
let emit = crate::compiler::emit_machine(ast);
|
|
|
|
let context = Context::new();
|
|
let function_map = FunctionMap::default();
|
|
|
|
let output = execute(&function_map, &emit, &context);
|
|
|
|
insta::assert_debug_snapshot!(output, @r#"
|
|
Ok(
|
|
"Hello ",
|
|
)
|
|
"#);
|
|
}
|
|
}
|