nomo/src/eval/mod.rs
Marcel Müller 42698bb219 Work on error messages
Signed-off-by: Marcel Müller <neikos@neikos.email>
2026-03-18 09:59:36 +01:00

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 ",
)
"#);
}
}