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>, slots: HashMap, } 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 { 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 { 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 ", ) "#); } }