diff --git a/RELEASES.md b/RELEASES.md index 6248f124..8ba7d09c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -7,6 +7,7 @@ Version 0.18.0 This version adds: * Anonymous functions (in closure syntax). Simplifies creation of ad hoc functions. +* Currying of function pointers. New features ------------ @@ -15,6 +16,8 @@ New features * Disallow many keywords as variables, such as `print`, `eval`, `call`, `this` etc. * `x.call(f, ...)` allows binding `x` to `this` for the function referenced by the function pointer `f`. * Anonymous functions in the syntax of a closure, e.g. `|x, y, z| x + y - z`. +* Custom syntax now works even without the `internals` feature. +* Currying of function pointers is supported via the `curry` keyword. Breaking changes ---------------- @@ -48,7 +51,7 @@ Breaking changes New features ------------ -* New `serde` feature to allow serializating/deserializating to/from `Dynamic` values using [`serde`](https://crates.io/crates/serde). +* New `serde` feature to allow serializing/deserializing to/from `Dynamic` values using [`serde`](https://crates.io/crates/serde). This is particularly useful when converting a Rust `struct` to a `Dynamic` _object map_ and back. * `Engine::disable_symbol` to surgically disable keywords and/or operators. * `Engine::register_custom_operator` to define a custom operator. diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index d86aae6a..4e41d940 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -76,6 +76,7 @@ The Rhai Scripting Language 3. [Namespaces](language/fn-namespaces.md) 4. [Function Pointers](language/fn-ptr.md) 5. [Anonymous Functions](language/fn-anon.md) + 6. [Currying](language/fn-curry.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/appendix/keywords.md b/doc/src/appendix/keywords.md index 660a605c..7c8dbaff 100644 --- a/doc/src/appendix/keywords.md +++ b/doc/src/appendix/keywords.md @@ -26,6 +26,7 @@ Keywords List | `fn` (lower-case `f`) | Function definition | [`no_function`] | | `Fn` (capital `F`) | Function to create a [function pointer] | | | `call` | Call a [function pointer] | | +| `curry` | Curry a [function pointer] | | | `this` | Reference to base object for method call | [`no_function`] | | `type_of` | Get type name of value | | | `print` | Print value | | diff --git a/doc/src/engine/custom-syntax.md b/doc/src/engine/custom-syntax.md index f97e4203..89fdf792 100644 --- a/doc/src/engine/custom-syntax.md +++ b/doc/src/engine/custom-syntax.md @@ -33,18 +33,7 @@ Where This Might Be Useful * Where you just want to confuse your user and make their lives miserable, because you can. -Step One - Start With `internals` --------------------------------- - -Since a custom syntax taps deeply into the `AST` and evaluation process of the `Engine`, -the [`internals`] feature must be on in order to expose these necessary internal data structures. - -Beware that Rhai internal data structures are _volatile_ and may change without warning. - -Caveat emptor. - - -Step Two - Design The Syntax +Step One - Design The Syntax --------------------------- A custom syntax is simply a list of symbols. @@ -116,8 +105,8 @@ print(hello); // variable declared by a custom syntax persists! ``` -Step Three - Implementation --------------------------- +Step Two - Implementation +------------------------- Any custom syntax must include an _implementation_ of it. @@ -164,8 +153,6 @@ let expr = inputs.get(0).unwrap(); let result = engine.eval_expression_tree(context, scope, expr)?; ``` -As can be seem above, most arguments are simply passed straight-through to `engine::eval_expression_tree`. - ### Declare Variables New variables maybe declared (usually with a variable name that is passed in via `$ident$). @@ -179,14 +166,14 @@ In other words, any `scope.push(...)` calls must come _before_ any `engine::eval let var_name = inputs[0].get_variable_name().unwrap().to_string(); let expr = inputs.get(1).unwrap(); -scope.push(var_name, 0 as INT); // do this BEFORE engine.eval_expression_tree! +scope.push(var_name, 0 as INT); // do this BEFORE 'engine.eval_expression_tree'! -let result = engine.eval_expression_tree(scope, mods, state, lib, this_ptr, expr, level)?; +let result = engine.eval_expression_tree(context, scope, expr)?; ``` -Step Four - Register the Custom Syntax -------------------------------------- +Step Three - Register the Custom Syntax +-------------------------------------- Use `Engine::register_custom_syntax` to register a custom syntax. @@ -236,7 +223,7 @@ engine.register_custom_syntax( ``` -Step Five - Disable Unneeded Statement Types +Step Four - Disable Unneeded Statement Types ------------------------------------------- When a DSL needs a custom syntax, most likely than not it is extremely specialized. @@ -254,13 +241,13 @@ custom syntax (plus possibly expressions). But again, Don't Do Itâ„¢ - unless y of what you're doing. -Step Six - Document -------------------- +Step Five - Document +-------------------- For custom syntax, documentation is crucial. Make sure there are _lots_ of examples for users to follow. -Step Seven - Profit! --------------------- +Step Six - Profit! +------------------ diff --git a/doc/src/engine/dsl.md b/doc/src/engine/dsl.md index 3585936b..5b381d4a 100644 --- a/doc/src/engine/dsl.md +++ b/doc/src/engine/dsl.md @@ -11,7 +11,7 @@ Expressions Only In many DSL scenarios, only evaluation of expressions is needed. -The `Engine::eval_expression_XXX`[`eval_expression`] API can be used to restrict +The [`Engine::eval_expression_XXX`][`eval_expression`] API can be used to restrict a script to expressions only. @@ -21,8 +21,7 @@ Disable Keywords and/or Operators In some DSL scenarios, it is necessary to further restrict the language to exclude certain language features that are not necessary or dangerous to the application. -For example, a DSL may disable the `while` loop altogether while keeping all other statement -types intact. +For example, a DSL may disable the `while` loop while keeping all other statement types intact. It is possible, in Rhai, to surgically [disable keywords and operators]. @@ -54,31 +53,29 @@ Custom Syntax For advanced DSL scenarios, it is possible to define entire expression [_syntax_][custom syntax] - essentially custom statement types. -The [`internals`] feature is needed to be able to define [custom syntax] in Rhai. - -For example, the following is a SQL like syntax for some obscure DSL operation: +For example, the following is a SQL-like syntax for some obscure DSL operation: ```rust let table = [..., ..., ..., ...]; -// Syntax = "calculate" $ident$ $ident$ "from" $expr$ "->" $ident$ ":" $expr$ +// Syntax = calculate $ident$ $ident$ from $expr$ -> $ident$ : $expr$ let total = calculate sum price from table -> row : row.weight > 50; -// Note: There is nothing special about the use of symbols; to make it look exactly like SQL: -// Syntax = "SELECT" $ident$ "(" $ident$ ")" "FROM" $expr$ "AS" $ident$ "WHERE" $expr$ +// Note: There is nothing special about those symbols; to make it look exactly like SQL: +// Syntax = SELECT $ident$ ( $ident$ ) FROM $expr$ AS $ident$ WHERE $expr$ let total = SELECT sum(price) FROM table AS row WHERE row.weight > 50; ``` After registering this custom syntax with Rhai, it can be used anywhere inside a script as a normal expression. -For its evaluation, the callback function will receive the following list of parameters: +For its evaluation, the callback function will receive the following list of inputs: -`exprs[0] = "sum"` - math operator -`exprs[1] = "price"` - field name -`exprs[2] = Expression(table)` - data source -`exprs[3] = "row"` - loop variable name -`exprs[4] = Expression(row.wright > 50)` - expression +* `inputs[0] = "sum"` - math operator +* `inputs[1] = "price"` - field name +* `inputs[2] = Expression(table)` - data source +* `inputs[3] = "row"` - loop variable name +* `inputs[4] = Expression(row.wright > 50)` - filter predicate -The other identified, such as `"select"`, `"from"`, as as as symbols `->` and `:` are -parsed in the order defined within the custom syntax. +Other identifiers, such as `"calculate"`, `"from"`, as well as symbols such as `->` and `:`, +are parsed in the order defined within the custom syntax. diff --git a/doc/src/language/fn-curry.md b/doc/src/language/fn-curry.md new file mode 100644 index 00000000..8ee103a9 --- /dev/null +++ b/doc/src/language/fn-curry.md @@ -0,0 +1,30 @@ +Function Pointer Currying +======================== + +{{#include ../links.md}} + +It is possible to _curry_ a [function pointer] by providing partial (or all) arguments. + +Currying is done via the `curry` keyword and produces a new [function pointer] which carries +the curried arguments. + +When the curried [function pointer] is called, the curried arguments are inserted starting from the left. +The actual call arguments should be reduced by the number of curried arguments. + +```rust +fn mul(x, y) { // function with two parameters + x * y +} + +let func = Fn("mul"); + +func.call(21, 2) == 42; // two arguments are required for 'mul' + +let curried = func.curry(21); // currying produces a new function pointer which + // carries 21 as the first argument + +let curried = curry(func, 21); // function-call style also works + +curried.call(2) == 42; // <- de-sugars to 'func.call(21, 2)' + // only one argument is now required +``` diff --git a/doc/src/language/strings-chars.md b/doc/src/language/strings-chars.md index acdb06f4..81ca6258 100644 --- a/doc/src/language/strings-chars.md +++ b/doc/src/language/strings-chars.md @@ -20,9 +20,14 @@ The `ImmutableString` Type ------------------------- All strings in Rhai are implemented as `ImmutableString` (see [standard types]). +An `ImmutableString` does not change and can be shared. -`ImmutableString` should be used in place of the standard Rust type `String` when registering functions -because using `String` is very inefficient (the `String` must always be cloned). +Modifying an `ImmutableString` causes it first to be cloned, and then the modification made to the copy. + +### **IMPORTANT** - Avoid `String` Parameters + +`ImmutableString` should be used in place of `String` for function parameters because using +`String` is very inefficient (the `String` argument is cloned during every call). A alternative is to use `&str` which maps straight to `ImmutableString`. @@ -67,14 +72,6 @@ Individual characters within a Rhai string can also be replaced just as if the s In Rhai, there are also no separate concepts of `String` and `&str` as in Rust. -Immutable Strings ----------------- - -Rhai use _immutable_ strings (type `ImmutableString`) and can be shared. - -Modifying a Rhai string actually causes it first to be cloned, and then the modification made to the copy. - - Examples -------- diff --git a/doc/src/language/values-and-types.md b/doc/src/language/values-and-types.md index 55b03229..11426e84 100644 --- a/doc/src/language/values-and-types.md +++ b/doc/src/language/values-and-types.md @@ -14,7 +14,7 @@ The following primitive types are supported natively: | **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`][packages], disabled with [`no_std`]) | `std::time::Instant` ([`instant::Instant`] if not [WASM] build) | `"timestamp"` | _not supported_ | +| **[Timestamp]** (implemented in the [`BasicTimePackage`][packages], disabled with [`no_std`]) | `std::time::Instant` ([`instant::Instant`] if [WASM] build) | `"timestamp"` | _not supported_ | | **[Function pointer]** | `rhai::FnPtr` | `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. | diff --git a/doc/src/links.md b/doc/src/links.md index a26a34e8..889cee29 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -76,6 +76,7 @@ [functions]: {{rootUrl}}/language/functions.md [function pointer]: {{rootUrl}}/language/fn-ptr.md [function pointers]: {{rootUrl}}/language/fn-ptr.md +[currying]: {{rootUrl}}/language/fn-curry.md [function namespace]: {{rootUrl}}/language/fn-namespaces.md [function namespaces]: {{rootUrl}}/language/fn-namespaces.md [anonymous function]: {{rootUrl}}/language/fn-anon.md diff --git a/doc/src/rust/strings.md b/doc/src/rust/strings.md index 0d54d87b..f1d421ea 100644 --- a/doc/src/rust/strings.md +++ b/doc/src/rust/strings.md @@ -4,19 +4,32 @@ {{#include ../links.md}} +Avoid `String` +-------------- + +As must as possible, avoid using `String` parameters in functions. + +Each `String` argument is cloned during every single call to that function - and the copy +immediately thrown away right after the call. + +Needless to say, it is _extremely_ inefficient to use `String` parameters. + + `&str` Maps to `ImmutableString` ------------------------------- Rust functions accepting parameters of `String` should use `&str` instead because it maps directly to [`ImmutableString`][string] which is the type that Rhai uses to represent [strings] internally. -The parameter type `String` is discouraged because it involves converting an [`ImmutableString`] into a `String`. +The parameter type `String` involves always converting an [`ImmutableString`][string] into a `String` +which mandates cloning it. + Using `ImmutableString` or `&str` is much more efficient. A common mistake made by novice Rhai users is to register functions with `String` parameters. ```rust -fn get_len1(s: String) -> i64 { s.len() as i64 } // <- Rhai finds this function, but very inefficient -fn get_len2(s: &str) -> i64 { s.len() as i64 } // <- Rhai finds this function fine +fn get_len1(s: String) -> i64 { s.len() as i64 } // <- Very inefficient!!! +fn get_len2(s: &str) -> i64 { s.len() as i64 } // <- This is better fn get_len3(s: ImmutableString) -> i64 { s.len() as i64 } // <- the above is equivalent to this engine @@ -24,9 +37,9 @@ engine .register_fn("len2", get_len2) .register_fn("len3", get_len3); -let len = engine.eval::("x.len1()")?; // error: function 'len1 (&str | ImmutableString)' not found -let len = engine.eval::("x.len2()")?; // works fine -let len = engine.eval::("x.len3()")?; // works fine +let len = engine.eval::("x.len1()")?; // 'x' is cloned, very inefficient! +let len = engine.eval::("x.len2()")?; // 'x' is shared +let len = engine.eval::("x.len3()")?; // 'x' is shared ``` @@ -37,9 +50,11 @@ Rhai functions can take a first `&mut` parameter. Usually this is a good idea b cloning of the argument (except for primary types where cloning is cheap), so its use is encouraged even though there is no intention to ever mutate that argument. -`ImmutableString` is an exception to this rule. While `ImmutableString` is cheap to clone (only -incrementing a reference count), taking a mutable reference to it involves making a private clone -of the underlying string because Rhai has no way to find out whether that parameter will be mutated. +[`ImmutableString`][string] is an exception to this rule. + +While `ImmutableString` is cheap to clone (only incrementing a reference count), taking a mutable +reference to it involves making a private clone of the underlying string because Rhai has no way +to find out whether that parameter will be mutated. If the `ImmutableString` is not shared by any other variables, then Rhai just returns a mutable reference to it since nobody else is watching! Otherwise a private copy is made first, diff --git a/src/any.rs b/src/any.rs index 897bc922..7f637187 100644 --- a/src/any.rs +++ b/src/any.rs @@ -137,7 +137,7 @@ pub enum Union { Array(Box), #[cfg(not(feature = "no_object"))] Map(Box), - FnPtr(FnPtr), + FnPtr(Box), Variant(Box>), } @@ -274,7 +274,7 @@ impl fmt::Debug for Dynamic { f.write_str("#")?; fmt::Debug::fmt(value, f) } - Union::FnPtr(value) => fmt::Display::fmt(value, f), + Union::FnPtr(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_std"))] Union::Variant(value) if value.is::() => write!(f, ""), @@ -481,7 +481,7 @@ impl Dynamic { } if type_id == TypeId::of::() { return match self.0 { - Union::FnPtr(value) => unsafe_try_cast(value), + Union::FnPtr(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), _ => None, }; } @@ -582,7 +582,7 @@ impl Dynamic { } if type_id == TypeId::of::() { return match &self.0 { - Union::FnPtr(value) => ::downcast_ref::(value), + Union::FnPtr(value) => ::downcast_ref::(value.as_ref()), _ => None, }; } @@ -656,7 +656,7 @@ impl Dynamic { } if type_id == TypeId::of::() { return match &mut self.0 { - Union::FnPtr(value) => ::downcast_mut::(value), + Union::FnPtr(value) => ::downcast_mut::(value.as_mut()), _ => None, }; } @@ -735,7 +735,7 @@ impl Dynamic { pub(crate) fn take_immutable_string(self) -> Result { match self.0 { Union::Str(s) => Ok(s), - Union::FnPtr(f) => Ok(f.take_fn_name()), + Union::FnPtr(f) => Ok(f.take_data().0), _ => Err(self.type_name()), } } @@ -801,6 +801,11 @@ impl, T: Variant + Clone> From> for Dynam } impl From for Dynamic { fn from(value: FnPtr) -> Self { + Box::new(value).into() + } +} +impl From> for Dynamic { + fn from(value: Box) -> Self { Self(Union::FnPtr(value)) } } diff --git a/src/engine.rs b/src/engine.rs index 18e01781..47c6dc30 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -11,15 +11,13 @@ use crate::parser::{Expr, FnAccess, ImmutableString, ReturnType, ScriptFnDef, St use crate::r#unsafe::unsafe_cast_var_name_to_lifetime; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; +use crate::syntax::{CustomSyntax, EvalContext, Expression}; use crate::token::Position; use crate::utils::StaticVec; #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; -#[cfg(feature = "internals")] -use crate::syntax::{CustomSyntax, EvalContext}; - use crate::stdlib::{ any::{type_name, TypeId}, borrow::Cow, @@ -81,6 +79,7 @@ pub const KEYWORD_TYPE_OF: &str = "type_of"; pub const KEYWORD_EVAL: &str = "eval"; pub const KEYWORD_FN_PTR: &str = "Fn"; pub const KEYWORD_FN_PTR_CALL: &str = "call"; +pub const KEYWORD_FN_PTR_CURRY: &str = "curry"; pub const KEYWORD_THIS: &str = "this"; pub const FN_TO_STRING: &str = "to_string"; pub const FN_GET: &str = "get$"; @@ -88,45 +87,10 @@ 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_ANONYMOUS: &str = "anon$"; - -#[cfg(feature = "internals")] pub const MARKER_EXPR: &str = "$expr$"; -#[cfg(feature = "internals")] pub const MARKER_BLOCK: &str = "$block$"; -#[cfg(feature = "internals")] pub const MARKER_IDENT: &str = "$ident$"; -#[cfg(feature = "internals")] -#[derive(Debug, Clone, Hash)] -pub struct Expression<'a>(&'a Expr); - -#[cfg(feature = "internals")] -impl<'a> From<&'a Expr> for Expression<'a> { - fn from(expr: &'a Expr) -> Self { - Self(expr) - } -} - -#[cfg(feature = "internals")] -impl Expression<'_> { - /// If this expression is a variable name, return it. Otherwise `None`. - #[cfg(feature = "internals")] - pub fn get_variable_name(&self) -> Option<&str> { - match self.0 { - Expr::Variable(x) => Some((x.0).0.as_str()), - _ => None, - } - } - /// Get the expression. - pub(crate) fn expr(&self) -> &Expr { - &self.0 - } - /// Get the position of this expression. - pub fn position(&self) -> Position { - self.0.position() - } -} - /// A type specifying the method of chaining. #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] enum ChainType { @@ -203,7 +167,7 @@ impl Target<'_> { .as_char() .map_err(|_| EvalAltResult::ErrorCharMismatch(Position::none()))?; - let mut chars: StaticVec = s.chars().collect(); + let mut chars = s.chars().collect::>(); let ch = chars[*index]; // See if changed - if so, update the String @@ -316,7 +280,6 @@ pub struct Engine { /// A hashset containing custom keywords and precedence to recognize. pub(crate) custom_keywords: Option>, /// Custom syntax. - #[cfg(feature = "internals")] pub(crate) custom_syntax: Option>, /// Callback closure for implementing the `print` command. @@ -376,8 +339,6 @@ impl Default for Engine { type_names: None, disabled_symbols: None, custom_keywords: None, - - #[cfg(feature = "internals")] custom_syntax: None, // default print/debug implementations @@ -605,8 +566,6 @@ impl Engine { type_names: None, disabled_symbols: None, custom_keywords: None, - - #[cfg(feature = "internals")] custom_syntax: None, print: Box::new(|_| {}), @@ -1066,7 +1025,7 @@ impl Engine { lib: &Module, target: &mut Target, expr: &Expr, - mut idx_val: Dynamic, + idx_val: Dynamic, level: usize, ) -> Result<(Dynamic, bool), Box> { let ((name, native, pos), _, hash, _, def_val) = match expr { @@ -1079,17 +1038,22 @@ impl Engine { // Get a reference to the mutation target Dynamic let obj = target.as_mut(); - let idx = idx_val.downcast_mut::>().unwrap(); + let mut idx = idx_val.cast::>(); let mut fn_name = name.as_ref(); let (result, updated) = if fn_name == KEYWORD_FN_PTR_CALL && obj.is::() { // FnPtr call + let fn_ptr = obj.downcast_ref::().unwrap(); + let mut curry = fn_ptr.curry().iter().cloned().collect::>(); // Redirect function name - let fn_name = obj.as_str().unwrap(); + let fn_name = fn_ptr.fn_name(); // Recalculate hash - let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); - // Arguments are passed as-is - let mut arg_values = idx.iter_mut().collect::>(); + let hash = calc_fn_hash(empty(), fn_name, curry.len() + idx.len(), empty()); + // Arguments are passed as-is, adding the curried arguments + let mut arg_values = curry + .iter_mut() + .chain(idx.iter_mut()) + .collect::>(); let args = arg_values.as_mut(); // Map it to name(args) in function-call style @@ -1098,17 +1062,16 @@ impl Engine { ) } else if fn_name == KEYWORD_FN_PTR_CALL && idx.len() > 0 && idx[0].is::() { // FnPtr call on object + let fn_ptr = idx.remove(0).cast::(); + let mut curry = fn_ptr.curry().iter().cloned().collect::>(); // Redirect function name - let fn_name = idx[0] - .downcast_ref::() - .unwrap() - .get_fn_name() - .clone(); + let fn_name = fn_ptr.get_fn_name().clone(); // Recalculate hash - let hash = calc_fn_hash(empty(), &fn_name, idx.len() - 1, empty()); - // Replace the first argument with the object pointer + let hash = calc_fn_hash(empty(), &fn_name, curry.len() + idx.len(), empty()); + // Replace the first argument with the object pointer, adding the curried arguments let mut arg_values = once(obj) - .chain(idx.iter_mut().skip(1)) + .chain(curry.iter_mut()) + .chain(idx.iter_mut()) .collect::>(); let args = arg_values.as_mut(); @@ -1116,8 +1079,24 @@ impl Engine { self.exec_fn_call( state, lib, &fn_name, *native, hash, args, is_ref, true, *def_val, level, ) + } else if fn_name == KEYWORD_FN_PTR_CURRY && obj.is::() { + // Curry call + let fn_ptr = obj.downcast_ref::().unwrap(); + Ok(( + FnPtr::new_unchecked( + fn_ptr.get_fn_name().clone(), + fn_ptr + .curry() + .iter() + .cloned() + .chain(idx.into_iter()) + .collect(), + ) + .into(), + false, + )) } else { - let redirected: Option; + let redirected; let mut hash = *hash; // Check if it is a map method call in OOP style @@ -1126,9 +1105,8 @@ impl Engine { if let Some(val) = map.get(fn_name) { if let Some(f) = val.downcast_ref::() { // Remap the function name - redirected = Some(f.get_fn_name().clone()); - fn_name = redirected.as_ref().unwrap(); - + redirected = f.get_fn_name().clone(); + fn_name = &redirected; // Recalculate the hash based on the new function name hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); } @@ -1734,8 +1712,6 @@ impl Engine { /// ## WARNING - Low Level API /// /// This function is very low level. It evaluates an expression from an AST. - #[cfg(feature = "internals")] - #[deprecated(note = "this method is volatile and may change")] pub fn eval_expression_tree( &self, context: &mut EvalContext, @@ -1775,7 +1751,7 @@ impl Engine { Expr::FloatConstant(x) => Ok(x.0.into()), Expr::StringConstant(x) => Ok(x.0.to_string().into()), Expr::CharConstant(x) => Ok(x.0.into()), - Expr::FnPointer(x) => Ok(FnPtr::new_unchecked(x.0.clone()).into()), + Expr::FnPointer(x) => Ok(FnPtr::new_unchecked(x.0.clone(), Default::default()).into()), Expr::Variable(x) if (x.0).0 == KEYWORD_THIS => { if let Some(val) = this_ptr { Ok(val.clone()) @@ -1963,6 +1939,34 @@ impl Engine { } } + // Handle curry() + if name == KEYWORD_FN_PTR_CURRY && args_expr.len() > 1 { + let expr = args_expr.get(0); + let fn_ptr = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; + + if !fn_ptr.is::() { + return Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + self.map_type_name(type_name::()).into(), + self.map_type_name(fn_ptr.type_name()).into(), + expr.position(), + ))); + } + + let (fn_name, fn_curry) = fn_ptr.cast::().take_data(); + + let curry: StaticVec<_> = args_expr + .iter() + .skip(1) + .map(|expr| self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)) + .collect::>()?; + + return Ok(FnPtr::new_unchecked( + fn_name, + fn_curry.into_iter().chain(curry.into_iter()).collect(), + ) + .into()); + } + // Handle eval() if name == KEYWORD_EVAL && args_expr.len() == 1 { let hash_fn = @@ -1992,6 +1996,7 @@ impl Engine { let redirected; let mut name = name.as_ref(); let mut args_expr = args_expr.as_ref(); + let mut curry: StaticVec<_> = Default::default(); let mut hash = *hash; if name == KEYWORD_FN_PTR_CALL @@ -1999,20 +2004,22 @@ impl Engine { && !self.has_override(lib, 0, hash) { let expr = args_expr.get(0).unwrap(); - let fn_ptr = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; + let fn_name = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; - if fn_ptr.is::() { + if fn_name.is::() { + let fn_ptr = fn_name.cast::(); + curry = fn_ptr.curry().iter().cloned().collect(); // Redirect function name - redirected = Some(fn_ptr.cast::().take_fn_name()); - name = redirected.as_ref().unwrap(); + redirected = fn_ptr.take_data().0; + name = &redirected; // Skip the first argument args_expr = &args_expr.as_ref()[1..]; // Recalculate hash - hash = calc_fn_hash(empty(), name, args_expr.len(), empty()); + hash = calc_fn_hash(empty(), name, curry.len() + args_expr.len(), empty()); } else { return Err(Box::new(EvalAltResult::ErrorMismatchOutputType( self.map_type_name(type_name::()).into(), - fn_ptr.type_name().into(), + fn_name.type_name().into(), expr.position(), ))); } @@ -2023,7 +2030,7 @@ impl Engine { let mut args: StaticVec<_>; let mut is_ref = false; - if args_expr.is_empty() { + if args_expr.is_empty() && curry.is_empty() { // No arguments args = Default::default(); } else { @@ -2046,7 +2053,10 @@ impl Engine { self.inc_operations(state) .map_err(|err| err.new_position(pos))?; - args = once(target).chain(arg_values.iter_mut()).collect(); + args = once(target) + .chain(curry.iter_mut()) + .chain(arg_values.iter_mut()) + .collect(); is_ref = true; } @@ -2059,7 +2069,7 @@ impl Engine { }) .collect::>()?; - args = arg_values.iter_mut().collect(); + args = curry.iter_mut().chain(arg_values.iter_mut()).collect(); } } } @@ -2216,10 +2226,9 @@ impl Engine { Expr::False(_) => Ok(false.into()), Expr::Unit(_) => Ok(().into()), - #[cfg(feature = "internals")] Expr::Custom(x) => { let func = (x.0).1.as_ref(); - let ep: StaticVec<_> = (x.0).0.iter().map(|e| e.into()).collect(); + let ep = (x.0).0.iter().map(|e| e.into()).collect::>(); let mut context = EvalContext { mods, state, diff --git a/src/fn_native.rs b/src/fn_native.rs index 2f36a27d..8b3795f7 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -1,14 +1,15 @@ //! Module containing interfaces with native-Rust functions. use crate::any::Dynamic; use crate::engine::Engine; -use crate::module::Module; +use crate::module::{FuncReturn, Module}; use crate::parser::ScriptFnDef; use crate::plugin::PluginFunction; use crate::result::EvalAltResult; use crate::token::{is_valid_identifier, Position}; -use crate::utils::ImmutableString; +use crate::utils::{ImmutableString, StaticVec}; +use crate::Scope; -use crate::stdlib::{boxed::Box, convert::TryFrom, fmt, rc::Rc, string::String, sync::Arc}; +use crate::stdlib::{boxed::Box, convert::TryFrom, fmt, mem, rc::Rc, string::String, sync::Arc}; /// Trait that maps to `Send + Sync` only under the `sync` feature. #[cfg(feature = "sync")] @@ -50,14 +51,15 @@ 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); +/// A general function pointer, which may carry additional (i.e. curried) argument values +/// to be passed onto a function during a call. +#[derive(Debug, Clone, Default)] +pub struct FnPtr(ImmutableString, Vec); impl FnPtr { /// Create a new function pointer. - pub(crate) fn new_unchecked>(name: S) -> Self { - Self(name.into()) + pub(crate) fn new_unchecked>(name: S, curry: Vec) -> Self { + Self(name.into(), curry) } /// Get the name of the function. pub fn fn_name(&self) -> &str { @@ -67,9 +69,38 @@ impl FnPtr { pub(crate) fn get_fn_name(&self) -> &ImmutableString { &self.0 } - /// Get the name of the function. - pub(crate) fn take_fn_name(self) -> ImmutableString { - self.0 + /// Get the underlying data of the function pointer. + pub(crate) fn take_data(self) -> (ImmutableString, Vec) { + (self.0, self.1) + } + /// Get the curried arguments. + pub fn curry(&self) -> &[Dynamic] { + &self.1 + } + + /// Call the function pointer with curried arguments (if any). + /// + /// ## WARNING + /// + /// All the arguments are _consumed_, meaning that they're replaced by `()`. + /// This is to avoid unnecessarily cloning the arguments. + /// Do not use the arguments after this call. If they are needed afterwards, + /// clone them _before_ calling this function. + pub fn call_dynamic( + &self, + engine: &Engine, + lib: impl AsRef, + this_ptr: Option<&mut Dynamic>, + mut arg_values: impl AsMut<[Dynamic]>, + ) -> FuncReturn { + let args = self + .1 + .iter() + .cloned() + .chain(arg_values.as_mut().iter_mut().map(|v| mem::take(v))) + .collect::>(); + + engine.call_fn_dynamic(&mut Scope::new(), lib, self.0.as_str(), this_ptr, args) } } @@ -84,7 +115,7 @@ impl TryFrom for FnPtr { fn try_from(value: ImmutableString) -> Result { if is_valid_identifier(value.chars()) { - Ok(Self(value)) + Ok(Self(value, Default::default())) } else { Err(Box::new(EvalAltResult::ErrorFunctionNotFound( value.into(), diff --git a/src/lib.rs b/src/lib.rs index dab4b17a..d3657fbb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,7 +95,6 @@ mod scope; mod serde; mod settings; mod stdlib; -#[cfg(feature = "internals")] mod syntax; mod token; mod r#unsafe; @@ -110,6 +109,7 @@ pub use module::Module; pub use parser::{ImmutableString, AST, INT}; pub use result::EvalAltResult; pub use scope::Scope; +pub use syntax::{EvalContext, Expression}; pub use token::Position; pub use utils::calc_fn_spec as calc_fn_hash; @@ -173,11 +173,7 @@ pub use parser::{CustomExpr, Expr, ReturnType, ScriptFnDef, Stmt}; #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] -pub use engine::{Expression, Imports, State as EvalState}; - -#[cfg(feature = "internals")] -#[deprecated(note = "this type is volatile and may change")] -pub use syntax::EvalContext; +pub use engine::{Imports, State as EvalState}; #[cfg(feature = "internals")] #[deprecated(note = "this type is volatile and may change")] diff --git a/src/parser.rs b/src/parser.rs index b3ed0756..b6a3fa08 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,23 +2,19 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; -use crate::engine::{make_getter, make_setter, Engine, FN_ANONYMOUS, KEYWORD_THIS}; +use crate::engine::{ + make_getter, make_setter, Engine, FN_ANONYMOUS, KEYWORD_THIS, MARKER_BLOCK, MARKER_EXPR, + MARKER_IDENT, +}; use crate::error::{LexError, ParseError, ParseErrorType}; +use crate::fn_native::Shared; use crate::module::{Module, ModuleRef}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::scope::{EntryType as ScopeEntryType, Scope}; +use crate::syntax::FnCustomSyntaxEval; use crate::token::{is_valid_identifier, Position, Token, TokenStream}; use crate::utils::{StaticVec, StraightHasherBuilder}; -#[cfg(feature = "internals")] -use crate::engine::{MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT}; - -#[cfg(feature = "internals")] -use crate::fn_native::Shared; - -#[cfg(feature = "internals")] -use crate::syntax::FnCustomSyntaxEval; - use crate::stdlib::{ borrow::Cow, boxed::Box, @@ -587,17 +583,14 @@ impl Stmt { } #[derive(Clone)] -#[cfg(feature = "internals")] pub struct CustomExpr(pub StaticVec, pub Shared); -#[cfg(feature = "internals")] impl fmt::Debug for CustomExpr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) } } -#[cfg(feature = "internals")] impl Hash for CustomExpr { fn hash(&self, state: &mut H) { self.0.hash(state); @@ -683,7 +676,6 @@ pub enum Expr { /// () Unit(Position), /// Custom syntax - #[cfg(feature = "internals")] Custom(Box<(CustomExpr, Position)>), } @@ -781,7 +773,6 @@ impl Expr { Self::Dot(x) | Self::Index(x) => x.0.position(), - #[cfg(feature = "internals")] Self::Custom(x) => x.1, } } @@ -816,8 +807,6 @@ impl Expr { Self::Assignment(x) => x.3 = new_pos, Self::Dot(x) => x.2 = new_pos, Self::Index(x) => x.2 = new_pos, - - #[cfg(feature = "internals")] Self::Custom(x) => x.1 = new_pos, } @@ -925,7 +914,6 @@ impl Expr { _ => false, }, - #[cfg(feature = "internals")] Self::Custom(_) => false, } } @@ -2148,7 +2136,6 @@ fn parse_expr( settings.ensure_level_within_max_limit(state.max_expr_depth)?; // Check if it is a custom syntax. - #[cfg(feature = "internals")] if let Some(ref custom) = state.engine.custom_syntax { let (token, pos) = input.peek().unwrap(); let token_pos = *pos; diff --git a/src/syntax.rs b/src/syntax.rs index 78bf94ff..fd971f1f 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -1,14 +1,13 @@ //! Module containing implementation for custom syntax. -#![cfg(feature = "internals")] - use crate::any::Dynamic; -use crate::engine::{Engine, Expression, Imports, State, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT}; -use crate::error::LexError; +use crate::engine::{Engine, Imports, State, MARKER_BLOCK, MARKER_EXPR, MARKER_IDENT}; +use crate::error::{LexError, ParseError}; use crate::fn_native::{SendSync, Shared}; use crate::module::Module; +use crate::parser::Expr; use crate::result::EvalAltResult; use crate::scope::Scope; -use crate::token::{is_valid_identifier, Token}; +use crate::token::{is_valid_identifier, Position, Token}; use crate::utils::StaticVec; use crate::stdlib::{ @@ -18,7 +17,7 @@ use crate::stdlib::{ sync::Arc, }; -/// A general function trail object. +/// A general expression evaluation trait object. #[cfg(not(feature = "sync"))] pub type FnCustomSyntaxEval = dyn Fn( &Engine, @@ -26,12 +25,40 @@ pub type FnCustomSyntaxEval = dyn Fn( &mut Scope, &[Expression], ) -> Result>; -/// A general function trail object. +/// A general expression evaluation trait object. #[cfg(feature = "sync")] pub type FnCustomSyntaxEval = dyn Fn(&Engine, &mut EvalContext, &mut Scope, &[Expression]) -> Result> + Send + Sync; +/// An expression sub-tree in an AST. +#[derive(Debug, Clone, Hash)] +pub struct Expression<'a>(&'a Expr); + +impl<'a> From<&'a Expr> for Expression<'a> { + fn from(expr: &'a Expr) -> Self { + Self(expr) + } +} + +impl Expression<'_> { + /// If this expression is a variable name, return it. Otherwise `None`. + pub fn get_variable_name(&self) -> Option<&str> { + match self.0 { + Expr::Variable(x) => Some((x.0).0.as_str()), + _ => None, + } + } + /// Get the expression. + pub(crate) fn expr(&self) -> &Expr { + &self.0 + } + /// Get the position of this expression. + pub fn position(&self) -> Position { + self.0.position() + } +} + #[derive(Clone)] pub struct CustomSyntax { pub segments: StaticVec, @@ -45,6 +72,8 @@ impl fmt::Debug for CustomSyntax { } } +/// Context of a script evaluation process. +#[derive(Debug)] pub struct EvalContext<'a, 'b: 'a, 's, 'm, 't, 'd: 't> { pub(crate) mods: &'a mut Imports<'b>, pub(crate) state: &'s mut State, @@ -56,7 +85,7 @@ pub struct EvalContext<'a, 'b: 'a, 's, 'm, 't, 'd: 't> { impl Engine { pub fn register_custom_syntax + ToString>( &mut self, - value: &[S], + keywords: &[S], scope_delta: isize, func: impl Fn( &Engine, @@ -66,15 +95,18 @@ impl Engine { ) -> Result> + SendSync + 'static, - ) -> Result<&mut Self, Box> { - if value.is_empty() { - return Err(Box::new(LexError::ImproperSymbol("".to_string()))); - } - + ) -> Result<&mut Self, Box> { let mut segments: StaticVec<_> = Default::default(); - for s in value { - let seg = match s.as_ref() { + for s in keywords { + let s = s.as_ref().trim(); + + // skip empty keywords + if s.is_empty() { + continue; + } + + let seg = match s { // Markers not in first position MARKER_EXPR | MARKER_BLOCK | MARKER_IDENT if !segments.is_empty() => s.to_string(), // Standard symbols not in first position @@ -115,12 +147,25 @@ impl Engine { s.into() } // Anything else is an error - _ => return Err(Box::new(LexError::ImproperSymbol(s.to_string()))), + _ => { + return Err(LexError::ImproperSymbol(format!( + "Improper symbol for custom syntax: '{}'", + s + )) + .into_err(Position::none()) + .into()); + } }; segments.push(seg); } + // If the syntax has no keywords, just ignore the registration + if segments.is_empty() { + return Ok(self); + } + + // Remove the first keyword as the discriminator let key = segments.remove(0); let syntax = CustomSyntax { diff --git a/src/token.rs b/src/token.rs index dae43624..f2719a7a 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,8 +1,8 @@ //! Main module defining the lexer and parser. use crate::engine::{ - Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL, KEYWORD_PRINT, - KEYWORD_THIS, KEYWORD_TYPE_OF, + Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL, KEYWORD_FN_PTR_CURRY, + KEYWORD_PRINT, KEYWORD_THIS, KEYWORD_TYPE_OF, }; use crate::error::LexError; @@ -404,7 +404,7 @@ impl Token { Reserved(syntax.into()) } KEYWORD_PRINT | KEYWORD_DEBUG | KEYWORD_TYPE_OF | KEYWORD_EVAL | KEYWORD_FN_PTR - | KEYWORD_FN_PTR_CALL | KEYWORD_THIS => Reserved(syntax.into()), + | KEYWORD_FN_PTR_CALL | KEYWORD_FN_PTR_CURRY | KEYWORD_THIS => Reserved(syntax.into()), _ => return None, }) diff --git a/src/utils.rs b/src/utils.rs index 12add69d..845996c8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -387,7 +387,7 @@ impl StaticVec { let value = self.extract_from_list(index); // Move all items one slot to the left - for x in index + 1..self.len - 1 { + for x in index + 1..self.len { let orig_value = self.extract_from_list(x); self.set_into_list(x - 1, orig_value, false); } diff --git a/tests/call_fn.rs b/tests/call_fn.rs index 97767df7..e96ee63d 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -2,6 +2,7 @@ use rhai::{ Dynamic, Engine, EvalAltResult, FnPtr, Func, Module, ParseError, ParseErrorType, Scope, INT, }; +use std::any::TypeId; #[test] fn test_fn() -> Result<(), Box> { @@ -121,9 +122,9 @@ fn test_fn_ptr_raw() -> Result<(), Box> { engine.register_raw_fn( "bar", &[ - std::any::TypeId::of::(), - std::any::TypeId::of::(), - std::any::TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), ], move |engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| { let fp = std::mem::take(args[1]).cast::(); @@ -157,3 +158,35 @@ fn test_fn_ptr_raw() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_fn_ptr_curry_call() -> Result<(), Box> { + let mut module = Module::new(); + + module.set_raw_fn( + "call_with_arg", + &[TypeId::of::(), TypeId::of::()], + |engine: &Engine, module: &Module, args: &mut [&mut Dynamic]| { + let fn_ptr = std::mem::take(args[0]).cast::(); + fn_ptr.call_dynamic(engine, module, None, [std::mem::take(args[1])]) + }, + ); + + let mut engine = Engine::new(); + engine.load_package(module.into()); + + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::( + r#" + let addition = |x, y| { x + y }; + let curried = addition.curry(2); + + call_with_arg(curried, 40) + "# + )?, + 42 + ); + + Ok(()) +} diff --git a/tests/fn_ptr.rs b/tests/fn_ptr.rs index e9121475..833bb20c 100644 --- a/tests/fn_ptr.rs +++ b/tests/fn_ptr.rs @@ -78,3 +78,35 @@ fn test_fn_ptr() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_fn_ptr_curry() -> Result<(), Box> { + let mut engine = Engine::new(); + + engine.register_fn("foo", |x: &mut INT, y: INT| *x + y); + + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::( + r#" + let f = Fn("foo"); + let f2 = f.curry(40); + f2.call(2) + "# + )?, + 42 + ); + + assert_eq!( + engine.eval::( + r#" + let f = Fn("foo"); + let f2 = curry(f, 40); + call(f2, 2) + "# + )?, + 42 + ); + + Ok(()) +} diff --git a/tests/syntax.rs b/tests/syntax.rs index 11074c6a..b5a8d7d9 100644 --- a/tests/syntax.rs +++ b/tests/syntax.rs @@ -1,5 +1,6 @@ -#![cfg(feature = "internals")] -use rhai::{Engine, EvalAltResult, EvalContext, Expression, LexError, ParseErrorType, Scope, INT}; +use rhai::{ + Engine, EvalAltResult, EvalContext, Expression, ParseError, ParseErrorType, Scope, INT, +}; #[test] fn test_custom_syntax() -> Result<(), Box> { @@ -72,7 +73,7 @@ fn test_custom_syntax() -> Result<(), Box> { // The first symbol must be an identifier assert!(matches!( *engine.register_custom_syntax(&["!"], 0, |_, _, _, _| Ok(().into())).expect_err("should error"), - LexError::ImproperSymbol(s) if s == "!" + ParseError(err, _) if *err == ParseErrorType::BadInput("Improper symbol for custom syntax: '!'".to_string()) )); Ok(())