use std::collections::HashMap; use displaydoc::Display; use thiserror::Error; use crate::Context; use crate::emit::Instruction; use crate::emit::VMInstructions; use crate::emit::VariableSlot; 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(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 } => { let value = scopes .get_scoped(name) .ok_or(EvaluationError::UnknownVariable(name.clone()))?; scopes.insert_into_slot(*slot, value.clone()); } Instruction::EmitFromSlot { slot } => { let value = scopes.get(slot).try_to_string().unwrap(); 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::parser::TokenOperator::Plus => left_value.try_add(right_value), crate::parser::TokenOperator::Minus => left_value.try_sub(right_value), crate::parser::TokenOperator::Times => left_value.try_mul(right_value), crate::parser::TokenOperator::Divide => left_value.try_div(right_value), crate::parser::TokenOperator::And => left_value.try_and(right_value), crate::parser::TokenOperator::Or => left_value.try_or(right_value), _ => todo!(), }; scopes.insert_into_slot(*result_slot, result.unwrap()); } } ip += 1; } Ok(output) } #[cfg(test)] mod tests { use crate::Context; use crate::eval::execute; #[test] fn check_simple_variable_interpolation() { let input = "Hello {{= world }}"; let parsed = crate::parser::parse(input.into()).unwrap(); let ast = crate::ast::parse(parsed.tokens()).unwrap(); let emit = crate::emit::emit_machine(ast); let mut context = Context::new(); context.insert("world", "World"); let output = execute(&emit, &context); insta::assert_debug_snapshot!(output, @r#" Ok( "Hello World", ) "#); } }