Add function calling
Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
parent
52a63a7066
commit
10bcd77040
5 changed files with 275 additions and 7 deletions
|
|
@ -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<String, EvaluationError> {
|
||||
pub fn execute(
|
||||
available_functions: &FunctionMap,
|
||||
vm: &VMInstructions,
|
||||
global_context: &Context,
|
||||
) -> Result<String, EvaluationError> {
|
||||
let mut output = String::new();
|
||||
|
||||
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());
|
||||
}
|
||||
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;
|
||||
|
|
@ -221,6 +236,8 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result<String,
|
|||
mod tests {
|
||||
use crate::Context;
|
||||
use crate::eval::execute;
|
||||
use crate::functions::FunctionMap;
|
||||
use crate::functions::NomoFunctionError;
|
||||
|
||||
#[test]
|
||||
fn check_simple_variable_interpolation() {
|
||||
|
|
@ -234,7 +251,7 @@ mod tests {
|
|||
|
||||
let mut context = Context::new();
|
||||
context.insert("world", "World");
|
||||
let output = execute(&emit, &context);
|
||||
let output = execute(&FunctionMap::default(), &emit, &context);
|
||||
|
||||
insta::assert_debug_snapshot!(output, @r#"
|
||||
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
175
src/functions.rs
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Template>,
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
48
src/value.rs
48
src/value.rs
|
|
@ -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
|
||||
where
|
||||
V: Into<NomoValue>,
|
||||
|
|
@ -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<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")]
|
||||
impl TryFrom<serde_json::Value> for NomoValue {
|
||||
type Error = NomoValueError;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue