Add function calling

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2026-03-15 11:27:25 +01:00
parent 52a63a7066
commit 10bcd77040
5 changed files with 275 additions and 7 deletions

View file

@ -7,6 +7,7 @@ use crate::Context;
use crate::emit::Instruction; use crate::emit::Instruction;
use crate::emit::VMInstructions; use crate::emit::VMInstructions;
use crate::emit::VariableSlot; use crate::emit::VariableSlot;
use crate::functions::FunctionMap;
use crate::input::NomoInput; use crate::input::NomoInput;
use crate::value::NomoValue; use crate::value::NomoValue;
@ -69,7 +70,11 @@ impl Scope {
clippy::unnecessary_to_owned, clippy::unnecessary_to_owned,
reason = "We cannot do the suggested way as the lifetimes would not match up" reason = "We cannot do the suggested way as the lifetimes would not match up"
)] )]
pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result<String, EvaluationError> { pub fn execute(
available_functions: &FunctionMap,
vm: &VMInstructions,
global_context: &Context,
) -> Result<String, EvaluationError> {
let mut output = String::new(); let mut output = String::new();
let mut scopes = Scope { let mut scopes = Scope {
@ -208,7 +213,17 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result<String,
scopes.insert_into_slot(*result_slot, result.unwrap()); scopes.insert_into_slot(*result_slot, result.unwrap());
} }
Instruction::FunctionCall { .. } => 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; ip += 1;
@ -221,6 +236,8 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result<String,
mod tests { mod tests {
use crate::Context; use crate::Context;
use crate::eval::execute; use crate::eval::execute;
use crate::functions::FunctionMap;
use crate::functions::NomoFunctionError;
#[test] #[test]
fn check_simple_variable_interpolation() { fn check_simple_variable_interpolation() {
@ -234,7 +251,7 @@ mod tests {
let mut context = Context::new(); let mut context = Context::new();
context.insert("world", "World"); context.insert("world", "World");
let output = execute(&emit, &context); let output = execute(&FunctionMap::default(), &emit, &context);
insta::assert_debug_snapshot!(output, @r#" insta::assert_debug_snapshot!(output, @r#"
Ok( Ok(
@ -242,4 +259,31 @@ mod tests {
) )
"#); "#);
} }
#[test]
fn check_method_call() {
let input = "Hello {{= foo(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 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",
)
"#);
}
} }

175
src/functions.rs Normal file
View file

@ -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<dyn std::error::Error + Send + Sync>,
},
}
pub trait NomoFunction<T>: 'static + Send + Sync {
fn call(&self, args: Vec<NomoValue>) -> Result<NomoValue, NomoFunctionError>;
}
#[derive(Default)]
pub struct FunctionMap {
funcs: HashMap<String, ErasedNomoFunction>,
}
impl FunctionMap {
pub fn register<NF: NomoFunction<T>, T>(&mut self, name: impl Into<String>, func: NF) {
self.funcs
.insert(name.into(), ErasedNomoFunction::erase(func));
}
pub fn get(&self, name: impl AsRef<str>) -> Option<&ErasedNomoFunction> {
self.funcs.get(name.as_ref())
}
}
pub struct ErasedNomoFunction {
func: Box<dyn Any + Send + Sync>,
call_fn: fn(&dyn Any, Vec<NomoValue>) -> Result<NomoValue, NomoFunctionError>,
}
impl ErasedNomoFunction {
pub fn call(&self, args: Vec<NomoValue>) -> Result<NomoValue, NomoFunctionError> {
(self.call_fn)(&*self.func, args)
}
pub fn erase<NF, T>(f: NF) -> ErasedNomoFunction
where
NF: NomoFunction<T>,
{
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<F, Res, Error, $($ty,)* $last> NomoFunction<($($ty,)* $last,)> for F
where
F: Fn($($ty,)* $last,) -> Result<Res, Error>,
F: Send + Sync + 'static,
Res: Into<NomoValue>,
Error: Into<NomoFunctionError>,
$( $ty: TryFrom<NomoValue, Error = NomoValueError>, )*
$last: TryFrom<NomoValue, Error = NomoValueError>,
{
fn call(&self, args: Vec<NomoValue>) -> Result<NomoValue, NomoFunctionError> {
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<F, Res, Error> NomoFunction<()> for F
where
F: Fn() -> Result<Res, Error> + Send + Sync + 'static,
Res: Into<NomoValue>,
Error: Into<NomoFunctionError>,
{
fn call(&self, args: Vec<NomoValue>) -> Result<NomoValue, NomoFunctionError> {
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<String, NomoFunctionError> { Ok(name) });
ErasedNomoFunction::erase(|left: u64, right: u64| -> Result<u64, NomoFunctionError> {
Ok(left + right)
});
}
}

View file

@ -4,6 +4,7 @@ use displaydoc::Display;
use thiserror::Error; use thiserror::Error;
use crate::emit::VMInstructions; use crate::emit::VMInstructions;
use crate::functions::FunctionMap;
use crate::input::NomoInput; use crate::input::NomoInput;
use crate::value::NomoValue; use crate::value::NomoValue;
use crate::value::NomoValueError; use crate::value::NomoValueError;
@ -11,6 +12,7 @@ use crate::value::NomoValueError;
pub mod ast; pub mod ast;
pub mod emit; pub mod emit;
pub mod eval; pub mod eval;
pub mod functions;
pub mod input; pub mod input;
pub mod parser; pub mod parser;
pub mod value; pub mod value;
@ -40,6 +42,7 @@ pub enum NomoError {
pub struct Nomo { pub struct Nomo {
templates: HashMap<String, Template>, templates: HashMap<String, Template>,
function_map: FunctionMap,
} }
impl Default for Nomo { impl Default for Nomo {
@ -52,6 +55,7 @@ impl Nomo {
pub fn new() -> Nomo { pub fn new() -> Nomo {
Nomo { Nomo {
templates: HashMap::new(), templates: HashMap::new(),
function_map: FunctionMap::default(),
} }
} }
@ -78,7 +82,7 @@ impl Nomo {
.get(name) .get(name)
.ok_or_else(|| NomoError::UnknownTemplate(name.to_string()))?; .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) Ok(res)
} }

View file

@ -310,6 +310,18 @@ impl From<&'static str> for NomoValue {
} }
} }
impl From<u64> for NomoValue {
fn from(val: u64) -> Self {
NomoValue::Integer { value: val }
}
}
impl From<i64> for NomoValue {
fn from(val: i64) -> Self {
NomoValue::SignedInteger { value: val }
}
}
impl<V> From<Vec<V>> for NomoValue impl<V> From<Vec<V>> for NomoValue
where where
V: Into<NomoValue>, V: Into<NomoValue>,
@ -355,11 +367,43 @@ where
} }
} }
#[cfg(feature = "serde_json")]
#[derive(Debug, Error, Display)] #[derive(Debug, Error, Display)]
/// Could not transform value to [`NomoValue`] /// Could not transform value to/from [`NomoValue`]
pub struct NomoValueError; pub struct NomoValueError;
impl TryFrom<NomoValue> for String {
type Error = NomoValueError;
fn try_from(value: NomoValue) -> Result<Self, Self::Error> {
match value {
NomoValue::String { value } => Ok(value.to_string()),
_ => Err(NomoValueError),
}
}
}
impl TryFrom<NomoValue> for i64 {
type Error = NomoValueError;
fn try_from(value: NomoValue) -> Result<Self, Self::Error> {
match value {
NomoValue::SignedInteger { value } => Ok(value),
_ => Err(NomoValueError),
}
}
}
impl TryFrom<NomoValue> for u64 {
type Error = NomoValueError;
fn try_from(value: NomoValue) -> Result<Self, Self::Error> {
match value {
NomoValue::Integer { value } => Ok(value),
_ => Err(NomoValueError),
}
}
}
#[cfg(feature = "serde_json")] #[cfg(feature = "serde_json")]
impl TryFrom<serde_json::Value> for NomoValue { impl TryFrom<serde_json::Value> for NomoValue {
type Error = NomoValueError; type Error = NomoValueError;

View file

@ -2,6 +2,7 @@ use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use nomo::Context; use nomo::Context;
use nomo::functions::FunctionMap;
test_each_file::test_each_path! { for ["nomo"] in "./tests/cases/" as cases => check_for_input } 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); 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); insta::assert_debug_snapshot!(format!("{basename}.4-output"), output);
} }