diff --git a/RELEASES.md b/RELEASES.md index 26ae0f99..5f082cb0 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -12,6 +12,11 @@ Breaking changes * The `merge_namespaces` parameter to `Module::eval_ast_as_new` is removed and now defaults to `true`. * `GlobalFileModuleResolver` is removed because its performance gain over the `FileModuleResolver` is no longer very significant. +New features +------------ + +* `is_def_var()` to detect if variable is defined and `is_def_fn()` to detect if script function is defined. + Version 0.19.0 ============== diff --git a/doc/src/about/index.md b/doc/src/about/index.md index ce966635..869016b9 100644 --- a/doc/src/about/index.md +++ b/doc/src/about/index.md @@ -14,7 +14,9 @@ Versions This Book is for version **{{version}}** of Rhai. +{% if rootUrl != "" and not rootUrl is ending_with("vnext") %} For the latest development version, see [here]({{rootUrl}}/vnext/). +{% endif %} Etymology of the name "Rhai" diff --git a/doc/src/appendix/keywords.md b/doc/src/appendix/keywords.md index 47a7b446..b403ac56 100644 --- a/doc/src/appendix/keywords.md +++ b/doc/src/appendix/keywords.md @@ -3,36 +3,38 @@ Keywords List {{#include ../links.md}} -| Keyword | Description | Inactive under | Overloadable | -| :-------------------: | ------------------------------------------- | :-------------: | :----------: | -| `true` | boolean true literal | | no | -| `false` | boolean false literal | | no | -| `let` | variable declaration | | no | -| `const` | constant declaration | | no | -| `is_shared` | is a value shared? | | no | -| `if` | if statement | | no | -| `else` | else block of if statement | | no | -| `while` | while loop | | no | -| `loop` | infinite loop | | no | -| `for` | for loop | | no | -| `in` | 1) containment test
2) part of for loop | | no | -| `continue` | continue a loop at the next iteration | | no | -| `break` | break out of loop iteration | | no | -| `return` | return value | | no | -| `throw` | throw exception | | no | -| `import` | import module | [`no_module`] | no | -| `export` | export variable | [`no_module`] | no | -| `as` | alias for variable export | [`no_module`] | no | -| `private` | mark function private | [`no_function`] | no | -| `fn` (lower-case `f`) | function definition | [`no_function`] | no | -| `Fn` (capital `F`) | create a [function pointer] | | yes | -| `call` | call a [function pointer] | | no | -| `curry` | curry a [function pointer] | | no | -| `this` | reference to base object for method call | [`no_function`] | no | -| `type_of` | get type name of value | | yes | -| `print` | print value | | yes | -| `debug` | print value in debug format | | yes | -| `eval` | evaluate script | | yes | +| Keyword | Description | Inactive under | Is function? | Overloadable | +| :-------------------: | ------------------------------------------- | :-------------: | :----------: | :----------: | +| `true` | boolean true literal | | no | | +| `false` | boolean false literal | | no | | +| `let` | variable declaration | | no | | +| `const` | constant declaration | | no | | +| `is_def_var` | is a variable declared? | | yes | yes | +| `is_shared` | is a value shared? | [`no_closure`] | yes | no | +| `if` | if statement | | no | | +| `else` | else block of if statement | | no | | +| `while` | while loop | | no | | +| `loop` | infinite loop | | no | | +| `for` | for loop | | no | | +| `in` | 1) containment test
2) part of for loop | | no | | +| `continue` | continue a loop at the next iteration | | no | | +| `break` | break out of loop iteration | | no | | +| `return` | return value | | no | | +| `throw` | throw exception | | no | | +| `import` | import module | [`no_module`] | no | | +| `export` | export variable | [`no_module`] | no | | +| `as` | alias for variable export | [`no_module`] | no | | +| `private` | mark function private | [`no_function`] | no | | +| `fn` (lower-case `f`) | function definition | [`no_function`] | no | | +| `Fn` (capital `F`) | create a [function pointer] | | yes | yes | +| `call` | call a [function pointer] | | yes | no | +| `curry` | curry a [function pointer] | | yes | no | +| `this` | reference to base object for method call | [`no_function`] | no | | +| `is_def_fn` | is a scripted function defined? | [`no_function`] | yes | yes | +| `type_of` | get type name of value | | yes | yes | +| `print` | print value | | yes | yes | +| `debug` | print value in debug format | | yes | yes | +| `eval` | evaluate script | | yes | yes | Reserved Keywords diff --git a/doc/src/language/functions.md b/doc/src/language/functions.md index 5b9498a6..362dc057 100644 --- a/doc/src/language/functions.md +++ b/doc/src/language/functions.md @@ -101,6 +101,21 @@ a statement in the script can freely call a function defined afterwards. This is similar to Rust and many other modern languages, such as JavaScript's `function` keyword. +`is_def_fn` +----------- + +Use `is_def_fn` to detect if a function is defined (and therefore callable), based on its name +and the number of parameters. + +```rust +fn foo(x) { x + 1 } + +is_def_fn("foo", 1) == true; + +is_def_fn("bar", 1) == false; +``` + + Arguments are Passed by Value ---------------------------- diff --git a/doc/src/language/variables.md b/doc/src/language/variables.md index 455c075b..fd211e19 100644 --- a/doc/src/language/variables.md +++ b/doc/src/language/variables.md @@ -37,6 +37,8 @@ If none is provided, it defaults to [`()`]. A variable defined within a statement block is _local_ to that block. +Use `is_def_var` to detect if a variable is defined. + ```rust let x; // ok - value is '()' let x = 3; // ok @@ -57,4 +59,10 @@ X == 123; x == 999; // access to local 'x' } x == 42; // the parent block's 'x' is not changed + +is_def_var("x") == true; + +is_def_var("_x") == true; + +is_def_var("y") == false; ``` diff --git a/doc/src/rust/modules/resolvers.md b/doc/src/rust/modules/resolvers.md index c042b9f1..abca9c27 100644 --- a/doc/src/rust/modules/resolvers.md +++ b/doc/src/rust/modules/resolvers.md @@ -32,28 +32,97 @@ are _merged_ into a _unified_ namespace. | my_module.rhai | ------------------ +// This function overrides any in the main script. private fn inner_message() { "hello! from module!" } -fn greet(callback) { print(callback.call()); } +fn greet() { + print(inner_message()); // call function in module script +} + +fn greet_main() { + print(main_message()); // call function not in module script +} ------------- | main.rhai | ------------- -fn main_message() { "hi! from main!" } +// This function is overridden by the module script. +fn inner_message() { "hi! from main!" } + +// This function is found by the module script. +fn main_message() { "main here!" } import "my_module" as m; -m::greet(|| "hello, " + "world!"); // works - anonymous function in global +m::greet(); // prints "hello! from module!" -m::greet(|| inner_message()); // works - function in module - -m::greet(|| main_message()); // works - function in global +m::greet_main(); // prints "main here!" ``` +### Simulating virtual functions + +When calling a namespace-qualified function defined within a module, other functions defined within +the same module script override any similar-named functions (with the same number of parameters) +defined in the global namespace. This is to ensure that a module acts as a self-contained unit and +functions defined in the calling script do not override module code. + +In some situations, however, it is actually beneficial to do it in reverse: have module code call functions +defined in the calling script (i.e. in the global namespace) if they exist, and only call those defined +in the module script if none are found. + +One such situation is the need to provide a _default implementation_ to a simulated _virtual_ function: + +```rust +------------------ +| my_module.rhai | +------------------ + +// Do not do this (it will override the main script): +// fn message() { "hello! from module!" } + +// This function acts as the default implementation. +private fn default_message() { "hello! from module!" } + +// This function depends on a 'virtual' function 'message' +// which is not defined in the module script. +fn greet() { + if is_def_fn("message", 0) { // 'is_def_fn' detects if 'message' is defined. + print(message()); + } else { + print(default_message()); + } +} + +------------- +| main.rhai | +------------- + +// The main script defines 'message' which is needed by the module script. +fn message() { "hi! from main!" } + +import "my_module" as m; + +m::greet(); // prints "hi! from main!" + +-------------- +| main2.rhai | +-------------- + +// The main script does not define 'message' which is needed by the module script. + +import "my_module" as m; + +m::greet(); // prints "hello! from module!" +``` + +### Changing the base directory + The base directory can be changed via the `FileModuleResolver::new_with_path` constructor function. -`FileModuleResolver::create_module` loads a script file and returns a module. +### Returning a module instead + +`FileModuleResolver::create_module` loads a script file and returns a module with the standard behavior. `StaticModuleResolver` diff --git a/src/engine.rs b/src/engine.rs index 29c742af..77f4c1e3 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -99,7 +99,10 @@ 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"; +#[cfg(not(feature = "no_closure"))] pub const KEYWORD_IS_SHARED: &str = "is_shared"; +pub const KEYWORD_IS_DEF_VAR: &str = "is_def_var"; +pub const KEYWORD_IS_DEF_FN: &str = "is_def_fn"; pub const KEYWORD_THIS: &str = "this"; pub const FN_TO_STRING: &str = "to_string"; #[cfg(not(feature = "no_object"))] diff --git a/src/fn_call.rs b/src/fn_call.rs index 9ee74b86..7908c424 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -4,8 +4,8 @@ use crate::any::Dynamic; use crate::calc_fn_hash; use crate::engine::{ search_imports, search_namespace, search_scope_only, Engine, Imports, State, KEYWORD_DEBUG, - KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL, KEYWORD_FN_PTR_CURRY, KEYWORD_IS_SHARED, - KEYWORD_PRINT, KEYWORD_TYPE_OF, + KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL, KEYWORD_FN_PTR_CURRY, KEYWORD_IS_DEF_FN, + KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, KEYWORD_TYPE_OF, }; use crate::error::ParseErrorType; use crate::fn_native::{FnCallArgs, FnPtr}; @@ -33,6 +33,9 @@ use crate::engine::{FN_IDX_GET, FN_IDX_SET}; #[cfg(not(feature = "no_object"))] use crate::engine::{Map, Target, FN_GET, FN_SET}; +#[cfg(not(feature = "no_closure"))] +use crate::engine::KEYWORD_IS_SHARED; + #[cfg(not(feature = "no_closure"))] #[cfg(not(feature = "no_function"))] use crate::scope::Entry as ScopeEntry; @@ -744,15 +747,18 @@ impl Engine { .into(), false, )) - } else if cfg!(not(feature = "no_closure")) - && _fn_name == KEYWORD_IS_SHARED - && idx.is_empty() - { + } else if { + #[cfg(not(feature = "no_closure"))] + { + _fn_name == KEYWORD_IS_SHARED && idx.is_empty() + } + #[cfg(feature = "no_closure")] + false + } { // is_shared call Ok((target.is_shared().into(), false)) } else { - #[cfg(not(feature = "no_object"))] - let redirected; + let _redirected; let mut hash = hash_script; // Check if it is a map method call in OOP style @@ -761,8 +767,8 @@ impl Engine { if let Some(val) = map.get(_fn_name) { if let Some(fn_ptr) = val.read_lock::() { // Remap the function name - redirected = fn_ptr.get_fn_name().clone(); - _fn_name = &redirected; + _redirected = fn_ptr.get_fn_name().clone(); + _fn_name = &_redirected; // Add curried arguments if !fn_ptr.curry().is_empty() { fn_ptr @@ -877,7 +883,8 @@ impl Engine { } // Handle is_shared() - if cfg!(not(feature = "no_closure")) && name == KEYWORD_IS_SHARED && args_expr.len() == 1 { + #[cfg(not(feature = "no_closure"))] + if name == KEYWORD_IS_SHARED && args_expr.len() == 1 { let expr = args_expr.get(0).unwrap(); let value = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; @@ -917,6 +924,48 @@ impl Engine { } } + // Handle is_def_var() + if name == KEYWORD_IS_DEF_VAR && args_expr.len() == 1 { + let hash_fn = calc_fn_hash(empty(), name, 1, once(TypeId::of::())); + + if !self.has_override(lib, hash_fn, hash_script, pub_only) { + let expr = args_expr.get(0).unwrap(); + if let Ok(var_name) = self + .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? + .as_str() + { + return Ok(scope.contains(var_name).into()); + } + } + } + + // Handle is_def_fn() + if name == KEYWORD_IS_DEF_FN && args_expr.len() == 2 { + let hash_fn = calc_fn_hash( + empty(), + name, + 2, + [TypeId::of::(), TypeId::of::()] + .iter() + .cloned(), + ); + + if !self.has_override(lib, hash_fn, hash_script, pub_only) { + let fn_name_expr = args_expr.get(0).unwrap(); + let num_params_expr = args_expr.get(1).unwrap(); + + if let (Ok(fn_name), Ok(num_params)) = ( + self.eval_expr(scope, mods, state, lib, this_ptr, fn_name_expr, level)? + .as_str(), + self.eval_expr(scope, mods, state, lib, this_ptr, num_params_expr, level)? + .as_int(), + ) { + let hash = calc_fn_hash(empty(), fn_name, num_params as usize, empty()); + return Ok(lib.contains_fn(hash, false).into()); + } + } + } + // Handle eval() if name == KEYWORD_EVAL && args_expr.len() == 1 { let hash_fn = calc_fn_hash(empty(), name, 1, once(TypeId::of::())); diff --git a/src/module/mod.rs b/src/module/mod.rs index f888f52b..4bb5ce57 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -454,6 +454,9 @@ impl Module { /// Arguments are simply passed in as a mutable array of `&mut Dynamic`, /// which is guaranteed to contain enough arguments of the correct types. /// + /// The function is assumed to be a _method_, meaning that the first argument should not be consumed. + /// All other arguments can be consumed. + /// /// To access a primary parameter value (i.e. cloning is cheap), use: `args[n].clone().cast::()` /// /// To access a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::()`. @@ -1299,8 +1302,8 @@ impl Module { .into_iter() .for_each(|ScopeEntry { value, alias, .. }| { // Variables with an alias left in the scope become module variables - if alias.is_some() { - module.variables.insert(*alias.unwrap(), value); + if let Some(alias) = alias { + module.variables.insert(*alias, value); } }); diff --git a/src/token.rs b/src/token.rs index 6245310a..bcc758f8 100644 --- a/src/token.rs +++ b/src/token.rs @@ -2,9 +2,12 @@ use crate::engine::{ Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL, KEYWORD_FN_PTR_CURRY, - KEYWORD_IS_SHARED, KEYWORD_PRINT, KEYWORD_THIS, KEYWORD_TYPE_OF, + KEYWORD_IS_DEF_FN, KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, KEYWORD_THIS, KEYWORD_TYPE_OF, }; +#[cfg(not(feature = "no_closure"))] +use crate::engine::KEYWORD_IS_SHARED; + use crate::error::LexError; use crate::parser::INT; use crate::utils::StaticVec; @@ -507,9 +510,11 @@ impl Token { | "await" | "yield" => Reserved(syntax.into()), KEYWORD_PRINT | KEYWORD_DEBUG | KEYWORD_TYPE_OF | KEYWORD_EVAL | KEYWORD_FN_PTR - | KEYWORD_FN_PTR_CALL | KEYWORD_FN_PTR_CURRY | KEYWORD_IS_SHARED | KEYWORD_THIS => { - Reserved(syntax.into()) - } + | KEYWORD_FN_PTR_CALL | KEYWORD_FN_PTR_CURRY | KEYWORD_IS_DEF_VAR + | KEYWORD_IS_DEF_FN | KEYWORD_THIS => Reserved(syntax.into()), + + #[cfg(not(feature = "no_closure"))] + KEYWORD_IS_SHARED => Reserved(syntax.into()), _ => return None, }) @@ -1455,7 +1460,9 @@ pub fn is_keyword_function(name: &str) -> bool { #[cfg(not(feature = "no_closure"))] KEYWORD_IS_SHARED => true, KEYWORD_PRINT | KEYWORD_DEBUG | KEYWORD_TYPE_OF | KEYWORD_EVAL | KEYWORD_FN_PTR - | KEYWORD_FN_PTR_CALL | KEYWORD_FN_PTR_CURRY => true, + | KEYWORD_FN_PTR_CALL | KEYWORD_FN_PTR_CURRY | KEYWORD_IS_DEF_VAR | KEYWORD_IS_DEF_FN => { + true + } _ => false, } } @@ -1465,7 +1472,8 @@ pub fn is_keyword_function(name: &str) -> bool { #[inline(always)] pub fn can_override_keyword(name: &str) -> bool { match name { - KEYWORD_PRINT | KEYWORD_DEBUG | KEYWORD_TYPE_OF | KEYWORD_EVAL | KEYWORD_FN_PTR => true, + KEYWORD_PRINT | KEYWORD_DEBUG | KEYWORD_TYPE_OF | KEYWORD_EVAL | KEYWORD_FN_PTR + | KEYWORD_IS_DEF_VAR | KEYWORD_IS_DEF_FN => true, _ => false, } }