Add is_def_var and is_def_fn.

This commit is contained in:
Stephen Chung 2020-10-03 16:25:58 +08:00
parent eec3f4e1bf
commit fbfb7677c1
10 changed files with 220 additions and 56 deletions

View File

@ -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
==============

View File

@ -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"

View File

@ -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<br/>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<br/>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

View File

@ -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
----------------------------

View File

@ -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;
```

View File

@ -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`

View File

@ -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"))]

View File

@ -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::<FnPtr>() {
// 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::<ImmutableString>()));
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::<ImmutableString>(), TypeId::of::<INT>()]
.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::<ImmutableString>()));

View File

@ -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::<T>()`
///
/// To access a parameter value and avoid cloning, use `std::mem::take(args[n]).cast::<T>()`.
@ -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);
}
});

View File

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