diff --git a/RELEASES.md b/RELEASES.md index 68175862..205491a4 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,6 +9,11 @@ Breaking changes * The trait function `ModuleResolver::resolve` no longer takes a `Scope` as argument. +New features +------------ + +* Support for _function pointers_ via `Fn(name)` and `Fn.call(...)` syntax - a poor man's first-class function. + Enhancements ------------ diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 2ba5e9e7..ea307d34 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -71,6 +71,7 @@ The Rhai Scripting Language 14. [Functions](language/functions.md) 1. [Function Overloading](language/overload.md) 2. [Call Method as Function](language/method.md) + 3. [Function Pointers](language/fn-ptr.md) 15. [Print and Debug](language/print-debug.md) 16. [Modules](language/modules/index.md) 1. [Export Variables, Functions and Sub-Modules](language/modules/export.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index bce5bd69..45958e5a 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -35,6 +35,8 @@ Dynamic * Organize code base with dynamically-loadable [modules]. +* [Function pointers]. + Safe ---- diff --git a/doc/src/about/non-design.md b/doc/src/about/non-design.md index 22b70f7d..6ab4f9ed 100644 --- a/doc/src/about/non-design.md +++ b/doc/src/about/non-design.md @@ -11,10 +11,13 @@ It doesn't attempt to be a new language. For example: * No traits... so it is also not Rust. Do your Rusty stuff in Rust. * No structures/records - define your types in Rust instead; Rhai can seamlessly work with _any Rust type_. + There is, however, a built-in [object map] type which is adequate for most uses. * No first-class functions - Code your functions in Rust instead, and register them with Rhai. + There is, however, support for simple [function pointers] allowing runtime dispatch by function name. + * No garbage collection - this should be expected, so... * No closures - do your closure magic in Rust instead; [turn a Rhai scripted function into a Rust closure]({{rootUrl}}/engine/call-fn.md). diff --git a/doc/src/language/dynamic.md b/doc/src/language/dynamic.md index e8c8a21b..9ebe7d6f 100644 --- a/doc/src/language/dynamic.md +++ b/doc/src/language/dynamic.md @@ -27,6 +27,8 @@ if type_of(mystery) == "i64" { print("Hey, I got an array here!"); } else if type_of(mystery) == "map" { print("Hey, I got an object map here!"); +} else if type_of(mystery) == "Fn" { + print("Hey, I got a function pointer here!"); } else if type_of(mystery) == "TestStruct" { print("Hey, I got the TestStruct custom type here!"); } else { diff --git a/doc/src/language/eval.md b/doc/src/language/eval.md index babb9242..a624403f 100644 --- a/doc/src/language/eval.md +++ b/doc/src/language/eval.md @@ -28,7 +28,7 @@ eval("{ let z = y }"); // to keep a variable local, use a statement blo print("z = " + z); // <- error: variable 'z' not found -"print(42)".eval(); // <- nope... method-call style doesn't work +"print(42)".eval(); // <- nope... method-call style doesn't work with 'eval' ``` Script segments passed to `eval` execute inside the current [`Scope`], so they can access and modify _everything_, diff --git a/doc/src/language/fn-ptr.md b/doc/src/language/fn-ptr.md new file mode 100644 index 00000000..fc083183 --- /dev/null +++ b/doc/src/language/fn-ptr.md @@ -0,0 +1,56 @@ +Function Pointers +================= + +It is possible to store a _function pointer_ in a variable just like a normal value. +In fact, internally a function pointer simply stores the _name_ of the function as a string. + +Call a function pointer using the `call` method, which needs to be called in method-call style. + + +Built-in Functions +------------------ + +The following standard methods (mostly defined in the [`BasicFnPackage`]({{rootUrl}}/rust/packages.md) but excluded if +using a [raw `Engine`]) operate on [strings]: + +| Function | Parameter(s) | Description | +| -------------------------- | ------------ | --------------------------------------------------------------------- | +| `name` method and property | _none_ | returns the name of the function encapsulated by the function pointer | + + +Examples +-------- + +```rust +fn foo(x) { 41 + x } + +let func = Fn("foo"); // use the 'Fn' function to create a function pointer + +print(func); // prints 'Fn(foo)' + +let func = fn_name.Fn(); // <- error: 'Fn' cannot be called in method-call style + +func.type_of() == "Fn"; // type_of() as function pointer is 'Fn' + +func.name == "foo"; + +func.call(1) == 42; // call a function pointer with the 'call' method + +foo(1) == 42; // <- the above de-sugars to this + +call(func, 1); //<- error: 'call (Fn, i64)' is not a registered function + +let len = Fn("len"); // 'Fn' also works with registered native Rust functions + +len.call("hello") == 5; + +let add = Fn("+"); // 'Fn' works with built-in operators also + +add.call(40, 2) == 42; + +let fn_name = "hello"; // the function name does not have to exist yet + +let hello = Fn(fn_name + "_world"); + +hello.call(0); // error: function not found - "hello_world (i64)" +``` diff --git a/doc/src/language/values-and-types.md b/doc/src/language/values-and-types.md index b3e55cb4..ddadc21f 100644 --- a/doc/src/language/values-and-types.md +++ b/doc/src/language/values-and-types.md @@ -5,20 +5,21 @@ Values and Types The following primitive types are supported natively: -| Category | Equivalent Rust types | [`type_of()`] | `to_string()` | -| --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------- | --------------------- | -| **Integer number** | `u8`, `i8`, `u16`, `i16`,
`u32`, `i32` (default for [`only_i32`]),
`u64`, `i64` _(default)_ | `"i32"`, `"u64"` etc. | `"42"`, `"123"` etc. | -| **Floating-point number** (disabled with [`no_float`]) | `f32`, `f64` _(default)_ | `"f32"` or `"f64"` | `"123.4567"` etc. | -| **Boolean value** | `bool` | `"bool"` | `"true"` or `"false"` | -| **Unicode character** | `char` | `"char"` | `"A"`, `"x"` etc. | -| **Immutable Unicode string** | `rhai::ImmutableString` (implemented as `Rc` or `Arc`) | `"string"` | `"hello"` etc. | -| **Array** (disabled with [`no_index`]) | `rhai::Array` | `"array"` | `"[ ?, ?, ? ]"` | -| **Object map** (disabled with [`no_object`]) | `rhai::Map` | `"map"` | `#{ "a": 1, "b": 2 }` | -| **Timestamp** (implemented in the [`BasicTimePackage`]({{rootUrl}}/rust/packages.md), disabled with [`no_std`]) | `std::time::Instant` ([`instant::Instant`](https://crates.io/crates/instant) if not [WASM] build) | `"timestamp"` | _not supported_ | -| **Dynamic value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ | _actual value_ | -| **System integer** (current configuration) | `rhai::INT` (`i32` or `i64`) | `"i32"` or `"i64"` | `"42"`, `"123"` etc. | -| **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. | -| **Nothing/void/nil/null/Unit** (or whatever it is called) | `()` | `"()"` | `""` _(empty string)_ | +| Category | Equivalent Rust types | [`type_of()`] | `to_string()` | +| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------- | ----------------------- | +| **Integer number** | `u8`, `i8`, `u16`, `i16`,
`u32`, `i32` (default for [`only_i32`]),
`u64`, `i64` _(default)_ | `"i32"`, `"u64"` etc. | `"42"`, `"123"` etc. | +| **Floating-point number** (disabled with [`no_float`]) | `f32`, `f64` _(default)_ | `"f32"` or `"f64"` | `"123.4567"` etc. | +| **Boolean value** | `bool` | `"bool"` | `"true"` or `"false"` | +| **Unicode character** | `char` | `"char"` | `"A"`, `"x"` etc. | +| **Immutable Unicode [string]** | `rhai::ImmutableString` (implemented as `Rc` or `Arc`) | `"string"` | `"hello"` etc. | +| **[`Array`]** (disabled with [`no_index`]) | `rhai::Array` | `"array"` | `"[ ?, ?, ? ]"` | +| **[Object map]** (disabled with [`no_object`]) | `rhai::Map` | `"map"` | `"#{ "a": 1, "b": 2 }"` | +| **[Timestamp]** (implemented in the [`BasicTimePackage`]({{rootUrl}}/rust/packages.md), disabled with [`no_std`]) | `std::time::Instant` ([`instant::Instant`](https://crates.io/crates/instant) if not [WASM] build) | `"timestamp"` | _not supported_ | +| **[Function pointer]** (disabled with [`no_function`]) | _None_ | `Fn` | `"Fn(foo)"` | +| **[`Dynamic`] value** (i.e. can be anything) | `rhai::Dynamic` | _the actual type_ | _actual value_ | +| **System integer** (current configuration) | `rhai::INT` (`i32` or `i64`) | `"i32"` or `"i64"` | `"42"`, `"123"` etc. | +| **System floating-point** (current configuration, disabled with [`no_float`]) | `rhai::FLOAT` (`f32` or `f64`) | `"f32"` or `"f64"` | `"123.456"` etc. | +| **Nothing/void/nil/null/Unit** (or whatever it is called) | `()` | `"()"` | `""` _(empty string)_ | All types are treated strictly separate by Rhai, meaning that `i32` and `i64` and `u32` are completely different - they even cannot be added together. This is very similar to Rust. diff --git a/doc/src/links.md b/doc/src/links.md index 8be0ed02..a9587253 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -65,6 +65,8 @@ [function]: {{rootUrl}}/language/functions.md [functions]: {{rootUrl}}/language/functions.md +[function pointer]: {{rootUrl}}/language/fn-ptr.md +[function pointers]: {{rootUrl}}/language/fn-ptr.md [`Module`]: {{rootUrl}}/language/modules/index.md [module]: {{rootUrl}}/language/modules/index.md diff --git a/doc/src/rust/packages/builtin.md b/doc/src/rust/packages/builtin.md index da33bfe4..4b4fba81 100644 --- a/doc/src/rust/packages/builtin.md +++ b/doc/src/rust/packages/builtin.md @@ -18,6 +18,7 @@ Built-In Packages | `BasicMathPackage` | Basic math functions (e.g. `sin`, `sqrt`) | No | Yes | | `BasicArrayPackage` | Basic [array] functions (not available under `no_index`) | No | Yes | | `BasicMapPackage` | Basic [object map] functions (not available under `no_object`) | No | Yes | +| `BasicFnPackage` | Basic methods for [function pointers]. | Yes | Yes | | `EvalPackage` | Disable [`eval`] | No | No | | `CorePackage` | Basic essentials | Yes | Yes | | `StandardPackage` | Standard library (default for `Engine::new`) | No | Yes | diff --git a/src/any.rs b/src/any.rs index 47da2e04..0f02f0d5 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1,6 +1,6 @@ //! Helper module which defines the `Any` trait to to allow dynamic value handling. -use crate::fn_native::SendSync; +use crate::fn_native::{FnPtr, SendSync}; use crate::module::Module; use crate::parser::{ImmutableString, INT}; use crate::r#unsafe::{unsafe_cast_box, unsafe_try_cast}; @@ -139,6 +139,8 @@ pub enum Union { #[cfg(not(feature = "no_object"))] Map(Box), Module(Box), + #[cfg(not(feature = "no_function"))] + FnPtr(FnPtr), Variant(Box>), } @@ -176,6 +178,8 @@ impl Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(_) => TypeId::of::(), Union::Module(_) => TypeId::of::(), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(_) => TypeId::of::(), Union::Variant(value) => (***value).type_id(), } } @@ -195,6 +199,8 @@ impl Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(_) => "map", Union::Module(_) => "sub-scope", + #[cfg(not(feature = "no_function"))] + Union::FnPtr(_) => "Fn", #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] @@ -219,6 +225,8 @@ impl fmt::Display for Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), Union::Module(value) => fmt::Debug::fmt(value, f), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] @@ -243,6 +251,8 @@ impl fmt::Debug for Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), Union::Module(value) => fmt::Debug::fmt(value, f), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] @@ -267,6 +277,8 @@ impl Clone for Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(ref value) => Self(Union::Map(value.clone())), Union::Module(ref value) => Self(Union::Module(value.clone())), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(ref value) => Self(Union::FnPtr(value.clone())), Union::Variant(ref value) => (***value).clone_into_dynamic(), } } @@ -396,6 +408,8 @@ impl Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), Union::Module(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(value) => unsafe_try_cast(value), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).ok(), } } @@ -439,6 +453,8 @@ impl Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(value) => *unsafe_cast_box::<_, T>(value).unwrap(), Union::Module(value) => *unsafe_cast_box::<_, T>(value).unwrap(), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(value) => unsafe_try_cast(value).unwrap(), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).unwrap(), } } @@ -454,8 +470,7 @@ impl Dynamic { match &self.0 { Union::Unit(value) => ::downcast_ref::(value), Union::Bool(value) => ::downcast_ref::(value), - Union::Str(value) => (value as &dyn Any) - .downcast_ref::() + Union::Str(value) => ::downcast_ref::(value) .or_else(|| ::downcast_ref::(value.as_ref())), Union::Char(value) => ::downcast_ref::(value), Union::Int(value) => ::downcast_ref::(value), @@ -466,6 +481,8 @@ impl Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(value) => ::downcast_ref::(value.as_ref()), Union::Module(value) => ::downcast_ref::(value.as_ref()), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(value) => ::downcast_ref::(value), Union::Variant(value) => value.as_ref().as_ref().as_any().downcast_ref::(), } } @@ -491,6 +508,8 @@ impl Dynamic { #[cfg(not(feature = "no_object"))] Union::Map(value) => ::downcast_mut::(value.as_mut()), Union::Module(value) => ::downcast_mut::(value.as_mut()), + #[cfg(not(feature = "no_function"))] + Union::FnPtr(value) => ::downcast_mut::(value), Union::Variant(value) => value.as_mut().as_mut_any().downcast_mut::(), } } @@ -556,6 +575,15 @@ impl Dynamic { _ => Err(self.type_name()), } } + + /// Cast the `Dynamic` as a `FnPtr` and return the function name. + /// Returns the name of the actual type if the cast fails. + pub(crate) fn as_fn_name(&self) -> Result<&str, &'static str> { + match &self.0 { + Union::FnPtr(f) => Ok(f.fn_name()), + _ => Err(self.type_name()), + } + } } impl From<()> for Dynamic { @@ -616,6 +644,12 @@ impl, T: Variant + Clone> From> for Dynam ))) } } +#[cfg(not(feature = "no_function"))] +impl From for Dynamic { + fn from(value: FnPtr) -> Self { + Self(Union::FnPtr(value)) + } +} /// Private type which ensures that `rhai::Any` and `rhai::AnyExt` can only /// be implemented by this crate. diff --git a/src/engine.rs b/src/engine.rs index 92704b24..fa91e450 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -3,7 +3,8 @@ use crate::any::{Dynamic, Union, Variant}; use crate::calc_fn_hash; use crate::error::ParseErrorType; -use crate::fn_native::{CallableFunction, Callback, FnCallArgs}; +use crate::fn_native::{CallableFunction, Callback, FnCallArgs, FnPtr}; +use crate::fn_register::RegisterResultFn; use crate::module::{resolvers, Module, ModuleResolver}; use crate::optimize::OptimizationLevel; use crate::packages::{Package, PackageLibrary, PackagesCollection, StandardPackage}; @@ -11,7 +12,7 @@ use crate::parser::{Expr, FnAccess, ImmutableString, ReturnType, ScriptFnDef, St use crate::r#unsafe::{unsafe_cast_var_name_to_lifetime, unsafe_mut_cast_to_lifetime}; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; -use crate::token::Position; +use crate::token::{Position, Token}; use crate::utils::StaticVec; #[cfg(not(feature = "no_float"))] @@ -71,11 +72,13 @@ pub const KEYWORD_PRINT: &str = "print"; pub const KEYWORD_DEBUG: &str = "debug"; pub const KEYWORD_TYPE_OF: &str = "type_of"; pub const KEYWORD_EVAL: &str = "eval"; +pub const KEYWORD_FN_PTR_CALL: &str = "call"; pub const FN_TO_STRING: &str = "to_string"; pub const FN_GET: &str = "get$"; pub const FN_SET: &str = "set$"; pub const FN_IDX_GET: &str = "$index$get$"; pub const FN_IDX_SET: &str = "$index$set$"; +pub const FN_FN_PTR: &str = "Fn"; /// A type specifying the method of chaining. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] @@ -954,6 +957,14 @@ impl Engine { false, )), + // Fn + FN_FN_PTR if args.len() == 1 && !self.has_override(lib, hashes) => { + Err(Box::new(EvalAltResult::ErrorRuntime( + "'Fn' should not be called in method style. Try Fn(...);".into(), + Position::none(), + ))) + } + // eval - reaching this point it must be a method-style call KEYWORD_EVAL if args.len() == 1 && !self.has_override(lib, hashes) => { Err(Box::new(EvalAltResult::ErrorRuntime( @@ -1124,18 +1135,31 @@ impl Engine { // Get a reference to the mutation target Dynamic let (result, updated) = { let obj = target.as_mut(); - let mut arg_values: StaticVec<_> = once(obj) - .chain( - idx_val - .downcast_mut::>() - .unwrap() - .iter_mut(), - ) - .collect(); + let idx = idx_val.downcast_mut::>().unwrap(); + + // Check if it is a FnPtr call + let (fn_name, mut arg_values, hash_fn, is_native) = + if name == KEYWORD_FN_PTR_CALL && obj.is::() { + // Redirect function name + let name = obj.as_fn_name().unwrap(); + // Recalculate hash + let hash = calc_fn_hash(empty(), name, idx.len(), empty()); + // Arguments are passed as-is + (name, idx.iter_mut().collect::>(), hash, false) + } else { + // Attached object pointer in front of the arguments + ( + name.as_ref(), + once(obj).chain(idx.iter_mut()).collect::>(), + *hash, + *native, + ) + }; + let args = arg_values.as_mut(); self.exec_fn_call( - state, lib, name, *native, *hash, args, is_ref, def_val, 0, + state, lib, fn_name, is_native, hash_fn, args, is_ref, def_val, 0, ) .map_err(|err| EvalAltResult::new_position(err, *pos))? }; @@ -1697,6 +1721,27 @@ impl Engine { let ((name, native, pos), _, hash, args_expr, def_val) = x.as_ref(); let def_val = def_val.as_ref(); + // Handle Fn + if name == FN_FN_PTR && args_expr.len() == 1 { + let hash_fn = + calc_fn_hash(empty(), name, 1, once(TypeId::of::())); + + if !self.has_override(lib, (hash_fn, *hash)) { + // Fn - only in function call style + let expr = args_expr.get(0); + let arg_value = self.eval_expr(scope, state, lib, expr, level)?; + return arg_value + .take_immutable_string() + .map(|s| FnPtr::from(s).into()) + .map_err(|type_name| { + Box::new(EvalAltResult::ErrorMismatchOutputType( + type_name.into(), + Position::none(), + )) + }); + } + } + // Handle eval if name == KEYWORD_EVAL && args_expr.len() == 1 { let hash_fn = diff --git a/src/fn_native.rs b/src/fn_native.rs index b2b3aa67..f3ab7053 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -2,6 +2,7 @@ use crate::any::Dynamic; use crate::engine::Engine; use crate::parser::ScriptFnDef; use crate::result::EvalAltResult; +use crate::utils::ImmutableString; use crate::stdlib::{boxed::Box, fmt, rc::Rc, sync::Arc}; @@ -53,8 +54,37 @@ pub fn shared_take(value: Shared) -> T { pub type FnCallArgs<'a> = [&'a mut Dynamic]; +/// A general function pointer. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] +pub struct FnPtr(ImmutableString); + +impl FnPtr { + /// Get the name of the function. + pub fn fn_name(&self) -> &str { + self.get_fn_name().as_ref() + } + /// Get the name of the function. + pub(crate) fn get_fn_name(&self) -> &ImmutableString { + &self.0 + } +} + +impl fmt::Display for FnPtr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Fn({})", self.0) + } +} + +impl> From for FnPtr { + fn from(value: S) -> Self { + Self(value.into()) + } +} + +/// A general function trail object. #[cfg(not(feature = "sync"))] pub type FnAny = dyn Fn(&Engine, &mut FnCallArgs) -> Result>; +/// A general function trail object. #[cfg(feature = "sync")] pub type FnAny = dyn Fn(&Engine, &mut FnCallArgs) -> Result> + Send + Sync; diff --git a/src/packages/fn_basic.rs b/src/packages/fn_basic.rs new file mode 100644 index 00000000..01807c9e --- /dev/null +++ b/src/packages/fn_basic.rs @@ -0,0 +1,8 @@ +use crate::def_package; +use crate::fn_native::FnPtr; + +def_package!(crate:BasicFnPackage:"Basic Fn functions.", lib, { + lib.set_fn_1_mut("name", |f: &mut FnPtr| Ok(f.get_fn_name().clone())); + lib.set_getter_fn("name", |f: &mut FnPtr| Ok(f.get_fn_name().clone())); + +}); diff --git a/src/packages/mod.rs b/src/packages/mod.rs index 3b89b9f4..d75e0adc 100644 --- a/src/packages/mod.rs +++ b/src/packages/mod.rs @@ -9,6 +9,7 @@ use crate::stdlib::any::TypeId; pub(crate) mod arithmetic; mod array_basic; mod eval; +mod fn_basic; mod iter_basic; mod logic; mod map_basic; @@ -23,6 +24,8 @@ pub use arithmetic::ArithmeticPackage; #[cfg(not(feature = "no_index"))] pub use array_basic::BasicArrayPackage; pub use eval::EvalPackage; +#[cfg(not(feature = "no_function"))] +pub use fn_basic::BasicFnPackage; pub use iter_basic::BasicIteratorPackage; pub use logic::LogicPackage; #[cfg(not(feature = "no_object"))] diff --git a/src/packages/pkg_core.rs b/src/packages/pkg_core.rs index 9ceae0d9..284c5997 100644 --- a/src/packages/pkg_core.rs +++ b/src/packages/pkg_core.rs @@ -1,4 +1,5 @@ use super::arithmetic::ArithmeticPackage; +use super::fn_basic::BasicFnPackage; use super::iter_basic::BasicIteratorPackage; use super::logic::LogicPackage; use super::string_basic::BasicStringPackage; @@ -10,4 +11,5 @@ def_package!(crate:CorePackage:"_Core_ package containing basic facilities.", li LogicPackage::init(lib); BasicStringPackage::init(lib); BasicIteratorPackage::init(lib); + BasicFnPackage::init(lib); }); diff --git a/src/packages/string_basic.rs b/src/packages/string_basic.rs index 195ee33a..2e405004 100644 --- a/src/packages/string_basic.rs +++ b/src/packages/string_basic.rs @@ -1,5 +1,6 @@ use crate::def_package; use crate::engine::{FN_TO_STRING, KEYWORD_DEBUG, KEYWORD_PRINT}; +use crate::fn_native::FnPtr; use crate::module::FuncReturn; use crate::parser::{ImmutableString, INT}; @@ -34,8 +35,9 @@ macro_rules! reg_op { } def_package!(crate:BasicStringPackage:"Basic string utilities, including printing.", lib, { - reg_op!(lib, KEYWORD_PRINT, to_string, INT, bool, char); - reg_op!(lib, FN_TO_STRING, to_string, INT, bool, char); + reg_op!(lib, KEYWORD_PRINT, to_string, INT, bool, char, FnPtr); + reg_op!(lib, FN_TO_STRING, to_string, INT, bool, char, FnPtr); + lib.set_fn_1_mut(KEYWORD_DEBUG, |f: &mut FnPtr| Ok(f.to_string())); lib.set_fn_0(KEYWORD_PRINT, || Ok("".to_string())); lib.set_fn_1(KEYWORD_PRINT, |_: ()| Ok("".to_string())); diff --git a/tests/functions.rs b/tests/functions.rs index 6a94256a..c59f0717 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -45,3 +45,44 @@ fn test_functions() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_function_pointers() -> Result<(), Box> { + let engine = Engine::new(); + + assert_eq!(engine.eval::(r#"type_of(Fn("abc"))"#)?, "Fn"); + + assert_eq!( + engine.eval::( + r#" + fn foo(x) { 40 + x } + + let fn_name = "f"; + fn_name += "oo"; + + let f = Fn(fn_name); + f.call(2) + "# + )?, + 42 + ); + + assert!(matches!( + *engine.eval::(r#"let f = Fn("abc"); f.call(0)"#).expect_err("should error"), + EvalAltResult::ErrorFunctionNotFound(f, _) if f.starts_with("abc (") + )); + + assert_eq!( + engine.eval::( + r#" + fn foo(x) { 40 + x } + + let x = #{ action: Fn("foo") }; + x.action.call(2) + "# + )?, + 42 + ); + + Ok(()) +}