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,
}
}