diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 7617380..1bb7425 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -7,6 +7,7 @@ use crate::Context; use crate::emit::Instruction; use crate::emit::VMInstructions; use crate::emit::VariableSlot; +use crate::functions::FunctionMap; use crate::input::NomoInput; use crate::value::NomoValue; @@ -69,7 +70,11 @@ impl Scope { 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 { +pub fn execute( + available_functions: &FunctionMap, + vm: &VMInstructions, + global_context: &Context, +) -> Result { let mut output = String::new(); let mut scopes = Scope { @@ -208,7 +213,17 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result todo!(), + 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); + } } ip += 1; @@ -221,6 +236,8 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result Result { + Ok(arg.to_uppercase()) + }); + + let output = execute(&function_map, &emit, &context); + + insta::assert_debug_snapshot!(output, @r#" + Ok( + "Hello WORLD", + ) + "#); + } } diff --git a/src/functions.rs b/src/functions.rs new file mode 100644 index 0000000..c15031c --- /dev/null +++ b/src/functions.rs @@ -0,0 +1,175 @@ +use std::any::Any; +use std::collections::HashMap; + +use displaydoc::Display; +use thiserror::Error; + +use crate::NomoValueError; +use crate::value::NomoValue; + +#[derive(Debug, Error, Display)] +pub enum NomoFunctionError { + /// Received {received} arguments, but this function only takes {expected} + WrongArgumentCount { received: usize, expected: usize }, + + /// The argument at this position is of the wrong type + InvalidArgumentType { index: usize }, + + /// A user-provided error + #[error(transparent)] + CustomError { + custom: Box, + }, +} + +pub trait NomoFunction: 'static + Send + Sync { + fn call(&self, args: Vec) -> Result; +} + +#[derive(Default)] +pub struct FunctionMap { + funcs: HashMap, +} + +impl FunctionMap { + pub fn register, T>(&mut self, name: impl Into, func: NF) { + self.funcs + .insert(name.into(), ErasedNomoFunction::erase(func)); + } + + pub fn get(&self, name: impl AsRef) -> Option<&ErasedNomoFunction> { + self.funcs.get(name.as_ref()) + } +} + +pub struct ErasedNomoFunction { + func: Box, + call_fn: fn(&dyn Any, Vec) -> Result, +} + +impl ErasedNomoFunction { + pub fn call(&self, args: Vec) -> Result { + (self.call_fn)(&*self.func, args) + } + + pub fn erase(f: NF) -> ErasedNomoFunction + where + NF: NomoFunction, + { + ErasedNomoFunction { + func: Box::new(f), + call_fn: |nf, args| { + let nf: &NF = nf.downcast_ref().unwrap(); + + nf.call(args) + }, + } + } +} + +#[rustfmt::skip] +macro_rules! for_all_tuples { + ($name:ident) => { + $name!([], T1); + $name!([T1], T2); + $name!([T1, T2], T3); + $name!([T1, T2, T3], T4); + $name!([T1, T2, T3, T4], T5); + $name!([T1, T2, T3, T4, T5], T6); + $name!([T1, T2, T3, T4, T5, T6], T7); + $name!([T1, T2, T3, T4, T5, T6, T7], T8); + $name!([T1, T2, T3, T4, T5, T6, T7, T8], T9); + $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9], T10); + $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10], T11); + $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11], T12); + $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12], T13); + $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13], T14); + $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14], T15); + $name!([T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15], T16); + }; +} + +macro_rules! impl_nomo_function { + ([$($ty:ident),*], $last:ident) => { + impl NomoFunction<($($ty,)* $last,)> for F + where + F: Fn($($ty,)* $last,) -> Result, + F: Send + Sync + 'static, + Res: Into, + Error: Into, + $( $ty: TryFrom, )* + $last: TryFrom, + { + fn call(&self, args: Vec) -> Result { + let arg_count = args.len(); + + let total_count = 1 $(+ impl_nomo_function!(count $ty))*; + + if arg_count != total_count { + return Err(NomoFunctionError::WrongArgumentCount { + expected: total_count, + received: arg_count, + }); + } + + let mut args = args.into_iter(); + + #[allow(unused_mut)] + let mut idx = 0; + + let val = (self)( + $({ + let val = $ty::try_from(args.next().unwrap()).map_err(|_| NomoFunctionError::InvalidArgumentType { index: idx })?; + idx += 1; + val + },)* + $last::try_from(args.next().unwrap()).map_err(|_| NomoFunctionError::InvalidArgumentType { index: idx })?, + ); + + val.map(Into::into).map_err(Into::into) + } + } + }; + + (count $ty:ident) => { + 1 + } +} + +for_all_tuples!(impl_nomo_function); + +impl NomoFunction<()> for F +where + F: Fn() -> Result + Send + Sync + 'static, + Res: Into, + Error: Into, +{ + fn call(&self, args: Vec) -> Result { + let arg_count = args.len(); + + if arg_count != 0 { + return Err(NomoFunctionError::WrongArgumentCount { + received: arg_count, + expected: 0, + }); + } + + let val = (self)(); + + val.map(Into::into).map_err(Into::into) + } +} + +#[cfg(test)] +mod tests { + use crate::functions::ErasedNomoFunction; + use crate::functions::NomoFunctionError; + + #[expect(dead_code, reason = "This is a compile test")] + fn check_various_methods() { + ErasedNomoFunction::erase(|name: String| -> Result { Ok(name) }); + ErasedNomoFunction::erase(|left: u64, right: u64| -> Result { + Ok(left + right) + }); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8d177ce..c9b8222 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ use displaydoc::Display; use thiserror::Error; use crate::emit::VMInstructions; +use crate::functions::FunctionMap; use crate::input::NomoInput; use crate::value::NomoValue; use crate::value::NomoValueError; @@ -11,6 +12,7 @@ use crate::value::NomoValueError; pub mod ast; pub mod emit; pub mod eval; +pub mod functions; pub mod input; pub mod parser; pub mod value; @@ -40,6 +42,7 @@ pub enum NomoError { pub struct Nomo { templates: HashMap, + function_map: FunctionMap, } impl Default for Nomo { @@ -52,6 +55,7 @@ impl Nomo { pub fn new() -> Nomo { Nomo { templates: HashMap::new(), + function_map: FunctionMap::default(), } } @@ -78,7 +82,7 @@ impl Nomo { .get(name) .ok_or_else(|| NomoError::UnknownTemplate(name.to_string()))?; - let res = eval::execute(&template.instructions, ctx)?; + let res = eval::execute(&self.function_map, &template.instructions, ctx)?; Ok(res) } diff --git a/src/value.rs b/src/value.rs index dd57a03..2d70084 100644 --- a/src/value.rs +++ b/src/value.rs @@ -310,6 +310,18 @@ impl From<&'static str> for NomoValue { } } +impl From for NomoValue { + fn from(val: u64) -> Self { + NomoValue::Integer { value: val } + } +} + +impl From for NomoValue { + fn from(val: i64) -> Self { + NomoValue::SignedInteger { value: val } + } +} + impl From> for NomoValue where V: Into, @@ -355,11 +367,43 @@ where } } -#[cfg(feature = "serde_json")] #[derive(Debug, Error, Display)] -/// Could not transform value to [`NomoValue`] +/// Could not transform value to/from [`NomoValue`] pub struct NomoValueError; +impl TryFrom for String { + type Error = NomoValueError; + + fn try_from(value: NomoValue) -> Result { + match value { + NomoValue::String { value } => Ok(value.to_string()), + _ => Err(NomoValueError), + } + } +} + +impl TryFrom for i64 { + type Error = NomoValueError; + + fn try_from(value: NomoValue) -> Result { + match value { + NomoValue::SignedInteger { value } => Ok(value), + _ => Err(NomoValueError), + } + } +} + +impl TryFrom for u64 { + type Error = NomoValueError; + + fn try_from(value: NomoValue) -> Result { + match value { + NomoValue::Integer { value } => Ok(value), + _ => Err(NomoValueError), + } + } +} + #[cfg(feature = "serde_json")] impl TryFrom for NomoValue { type Error = NomoValueError; diff --git a/tests/file_tests.rs b/tests/file_tests.rs index db3c666..28e88e9 100644 --- a/tests/file_tests.rs +++ b/tests/file_tests.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::path::Path; use nomo::Context; +use nomo::functions::FunctionMap; test_each_file::test_each_path! { for ["nomo"] in "./tests/cases/" as cases => check_for_input } @@ -58,7 +59,7 @@ fn check_for_input([path]: [&Path; 1]) { insta::assert_debug_snapshot!(format!("{basename}.3-instructions"), emit); - let output = nomo::eval::execute(&emit, &context).unwrap(); + let output = nomo::eval::execute(&FunctionMap::default(), &emit, &context).unwrap(); insta::assert_debug_snapshot!(format!("{basename}.4-output"), output); }