Merge pull request #248 from schungx/master

New FileModuleResolver.
This commit is contained in:
Stephen Chung 2020-09-28 22:32:11 +08:00 committed by GitHub
commit b1cbf0ec0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 1935 additions and 1146 deletions

View File

@ -23,22 +23,23 @@ Standard features
-----------------
* Easy-to-use language similar to JavaScript+Rust with dynamic typing.
* Fairly low compile-time overhead.
* Fairly efficient evaluation (1 million iterations in 0.3 sec on a single core, 2.3 GHz Linux VM).
* Tight integration with native Rust [functions](https://schungx.github.io/rhai/rust/functions.html) and [types]([#custom-types-and-methods](https://schungx.github.io/rhai/rust/custom.html)), including [getters/setters](https://schungx.github.io/rhai/rust/getters-setters.html), [methods](https://schungx.github.io/rhai/rust/custom.html) and [indexers](https://schungx.github.io/rhai/rust/indexers.html).
* Freely pass Rust variables/constants into a script via an external [`Scope`](https://schungx.github.io/rhai/rust/scope.html).
* Easily [call a script-defined function](https://schungx.github.io/rhai/engine/call-fn.html) from Rust.
* Fairly low compile-time overhead.
* Fairly efficient evaluation (1 million iterations in 0.3 sec on a single core, 2.3 GHz Linux VM).
* Relatively little `unsafe` code (yes there are some for performance reasons).
* Few dependencies (currently only [`smallvec`](https://crates.io/crates/smallvec)).
* Re-entrant scripting engine can be made `Send + Sync` (via the `sync` feature).
* [Function overloading](https://schungx.github.io/rhai/language/overload.html).
* [Operator overloading](https://schungx.github.io/rhai/rust/operators.html).
* Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html) with additional support for [currying](https://schungx.github.io/rhai/language/fn-curry.html) and [closures](https://schungx.github.io/rhai/language/fn-closure.html).
* Some support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html).
* Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations.
* Easy custom API development via [plugins](https://schungx.github.io/rhai/plugins/index.html) system powered by procedural macros.
* [Function overloading](https://schungx.github.io/rhai/language/overload.html) and [operator overloading](https://schungx.github.io/rhai/rust/operators.html).
* Dynamic dispatch via [function pointers](https://schungx.github.io/rhai/language/fn-ptr.html) with additional support for [currying](https://schungx.github.io/rhai/language/fn-curry.html).
* [Closures](https://schungx.github.io/rhai/language/fn-closure.html) (anonymous functions) that can capture shared values.
* Some syntactic support for [object-oriented programming (OOP)](https://schungx.github.io/rhai/language/oop.html).
* Organize code base with dynamically-loadable [modules](https://schungx.github.io/rhai/language/modules.html).
* Serialization/deserialization support via [serde](https://crates.io/crates/serde) (requires the `serde` feature).
* Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations.
* Support for [minimal builds](https://schungx.github.io/rhai/start/builds/minimal.html) by excluding unneeded language [features](https://schungx.github.io/rhai/start/features.html).
* Easy custom API development via [plugins](https://schungx.github.io/rhai/plugins/index.html) system powered by procedural macros.
Protection against attacks
--------------------------
@ -69,13 +70,11 @@ Scripts can be evaluated directly from the editor.
License
-------
Licensed under either:
Licensed under either of the following, at your choice:
* [Apache License, Version 2.0](https://github.com/jonathandturner/rhai/blob/master/LICENSE-APACHE.txt), or
* [MIT license](https://github.com/jonathandturner/rhai/blob/master/LICENSE-MIT.txt)
at your choice.
Unless explicitly stated otherwise, any contribution intentionally submitted
for inclusion in this crate, as defined in the Apache-2.0 license, shall
be dual-licensed as above, without any additional terms or conditions.

View File

@ -4,6 +4,9 @@ Rhai Release Notes
Version 0.19.0
==============
The major new feature for this version is _Plugins_ support, powered by procedural macros.
Plugins make it extremely easy to develop and register Rust functions with an `Engine`.
Bug fixes
---------
@ -17,17 +20,23 @@ Breaking changes
----------------
* `Engine::register_set_result` and `Engine::register_indexer_set_result` now take a function that returns `Result<(), Box<EvalAltResult>>`.
* `Engine::register_indexer_XXX` and `Module::set_indexer_XXX` panic when the type is `Arrary`, `Map` or `String`.
* `Engine::register_indexer_XXX` and `Module::set_indexer_XXX` panic when the type is `Array`, `Map` or `String`.
* `EvalAltResult` has a new variant `ErrorInModule` which holds errors when loading an external module.
* `Module::eval_ast_as_new` now takes an extra boolean parameter, indicating whether to encapsulate the entire module into a separate namespace.
* Functions in `FileModuleResolver` loaded modules now can cross-call each other in addition to functions in the global namespace. For the old behavior, use `MergingFileModuleResolver` instead.
* New `EvalAltResult::ErrorInModule` variant capturing errors when loading a module from a script file.
New features
------------
* Plugins support via procedural macros.
* Scripted functions are allowed in packages.
* `parse_int` and `parse_float` functions.
* `parse_int` and `parse_float` functions for parsing numbers; `split` function for splitting strings.
* `AST::iter_functions` and `Module::iter_script_fn_info` to iterate functions.
* Functions iteration functions now take `FnMut` instead of `Fn`.
* Functions iteration functions for `AST` and `Module` now take `FnMut` instead of `Fn`.
* New `FileModuleResolver` that encapsulates the entire `AST` of the module script, allowing function cross-calling. The old version is renamed `MergingFileModuleResolver`.
* `+` and `-` operators for timestamps to increment/decrement by seconds.
Version 0.18.3
==============

View File

@ -43,6 +43,30 @@ impl Default for FnSpecialAccess {
}
}
impl FnSpecialAccess {
pub fn get_fn_name(&self) -> Option<(String, String, proc_macro2::Span)> {
match self {
FnSpecialAccess::None => None,
FnSpecialAccess::Property(Property::Get(ref g)) => {
Some((format!("get${}", g.to_string()), g.to_string(), g.span()))
}
FnSpecialAccess::Property(Property::Set(ref s)) => {
Some((format!("set${}", s.to_string()), s.to_string(), s.span()))
}
FnSpecialAccess::Index(Index::Get) => Some((
FN_IDX_GET.to_string(),
"index_get".to_string(),
proc_macro2::Span::call_site(),
)),
FnSpecialAccess::Index(Index::Set) => Some((
FN_IDX_SET.to_string(),
"index_set".to_string(),
proc_macro2::Span::call_site(),
)),
}
}
}
#[derive(Debug, Default)]
pub(crate) struct ExportedFnParams {
pub name: Option<Vec<String>>,
@ -363,22 +387,8 @@ impl ExportedFn {
})
.unwrap_or_else(|| Vec::new());
match self.params.special {
FnSpecialAccess::None => {}
FnSpecialAccess::Property(Property::Get(ref g)) => literals.push(syn::LitStr::new(
&format!("get${}", g.to_string()),
g.span(),
)),
FnSpecialAccess::Property(Property::Set(ref s)) => literals.push(syn::LitStr::new(
&format!("set${}", s.to_string()),
s.span(),
)),
FnSpecialAccess::Index(Index::Get) => {
literals.push(syn::LitStr::new(FN_IDX_GET, proc_macro2::Span::call_site()))
}
FnSpecialAccess::Index(Index::Set) => {
literals.push(syn::LitStr::new(FN_IDX_SET, proc_macro2::Span::call_site()))
}
if let Some((s, _, span)) = self.params.special.get_fn_name() {
literals.push(syn::LitStr::new(&s, span));
}
if literals.is_empty() {

View File

@ -3,7 +3,7 @@ use std::collections::HashMap;
use quote::{quote, ToTokens};
use crate::attrs::ExportScope;
use crate::function::ExportedFn;
use crate::function::{ExportedFn, FnSpecialAccess};
use crate::module::Module;
pub(crate) type ExportedConst = (String, Box<syn::Type>, syn::Expr);
@ -183,17 +183,12 @@ pub(crate) fn flatten_type_groups(ty: &syn::Type) -> &syn::Type {
}
pub(crate) fn check_rename_collisions(fns: &Vec<ExportedFn>) -> Result<(), syn::Error> {
let mut renames = HashMap::<String, proc_macro2::Span>::new();
let mut names = HashMap::<String, proc_macro2::Span>::new();
for itemfn in fns.iter() {
if let Some(ref names) = itemfn.params().name {
for name in names {
let current_span = itemfn.params().span.as_ref().unwrap();
let key = itemfn.arg_list().fold(name.clone(), |mut argstr, fnarg| {
fn make_key(name: impl ToString, itemfn: &ExportedFn) -> String {
itemfn
.arg_list()
.fold(name.to_string(), |mut argstr, fnarg| {
let type_string: String = match fnarg {
syn::FnArg::Receiver(_) => {
unimplemented!("receiver rhai_fns not implemented")
}
syn::FnArg::Receiver(_) => unimplemented!("receiver rhai_fns not implemented"),
syn::FnArg::Typed(syn::PatType { ref ty, .. }) => {
ty.as_ref().to_token_stream().to_string()
}
@ -201,22 +196,43 @@ pub(crate) fn check_rename_collisions(fns: &Vec<ExportedFn>) -> Result<(), syn::
argstr.push('.');
argstr.push_str(&type_string);
argstr
});
})
}
let mut renames = HashMap::<String, proc_macro2::Span>::new();
let mut fn_defs = HashMap::<String, proc_macro2::Span>::new();
for itemfn in fns.iter() {
if itemfn.params().name.is_some() || itemfn.params().special != FnSpecialAccess::None {
let mut names = itemfn
.params()
.name
.as_ref()
.map(|v| v.iter().map(|n| (n.clone(), n.clone())).collect())
.unwrap_or_else(|| Vec::new());
if let Some((s, n, _)) = itemfn.params().special.get_fn_name() {
names.push((s, n));
}
for (name, fn_name) in names {
let current_span = itemfn.params().span.as_ref().unwrap();
let key = make_key(&name, itemfn);
if let Some(other_span) = renames.insert(key, *current_span) {
let mut err = syn::Error::new(
*current_span,
format!("duplicate Rhai signature for '{}'", &name),
format!("duplicate Rhai signature for '{}'", &fn_name),
);
err.combine(syn::Error::new(
other_span,
format!("duplicated function renamed '{}'", &name),
format!("duplicated function renamed '{}'", &fn_name),
));
return Err(err);
}
}
} else {
let ident = itemfn.name();
if let Some(other_span) = names.insert(ident.to_string(), ident.span()) {
if let Some(other_span) = fn_defs.insert(ident.to_string(), ident.span()) {
let mut err = syn::Error::new(
ident.span(),
format!("duplicate function '{}'", ident.to_string()),
@ -227,21 +243,20 @@ pub(crate) fn check_rename_collisions(fns: &Vec<ExportedFn>) -> Result<(), syn::
));
return Err(err);
}
}
}
for (new_name, attr_span) in renames.drain() {
let new_name = new_name.split('.').next().unwrap();
if let Some(fn_span) = names.get(new_name) {
let key = make_key(ident, itemfn);
if let Some(fn_span) = renames.get(&key) {
let mut err = syn::Error::new(
attr_span,
format!("duplicate Rhai signature for '{}'", &new_name),
ident.span(),
format!("duplicate Rhai signature for '{}'", &ident),
);
err.combine(syn::Error::new(
*fn_span,
format!("duplicated function '{}'", &new_name),
format!("duplicated function '{}'", &ident),
));
return Err(err);
}
}
}
Ok(())
}

View File

@ -1,15 +1,15 @@
error: duplicate Rhai signature for 'foo'
--> $DIR/rhai_fn_rename_collision_oneattr.rs:12:15
|
12 | #[rhai_fn(name = "foo")]
| ^^^^^^^^^^^^
error: duplicated function 'foo'
--> $DIR/rhai_fn_rename_collision_oneattr.rs:17:12
|
17 | pub fn foo(input: Point) -> bool {
| ^^^
error: duplicated function 'foo'
--> $DIR/rhai_fn_rename_collision_oneattr.rs:12:15
|
12 | #[rhai_fn(name = "foo")]
| ^^^^^^^^^^^^
error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
--> $DIR/rhai_fn_rename_collision_oneattr.rs:27:8
|

View File

@ -1,15 +1,15 @@
error: duplicate Rhai signature for 'foo'
error: duplicate Rhai signature for 'bar'
--> $DIR/rhai_fn_rename_collision_oneattr_multiple.rs:17:15
|
17 | #[rhai_fn(get = "bar")]
| ^^^^^^^^^^^
error: duplicated function renamed 'bar'
--> $DIR/rhai_fn_rename_collision_oneattr_multiple.rs:12:15
|
12 | #[rhai_fn(name = "foo", get = "bar")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^
error: duplicated function 'foo'
--> $DIR/rhai_fn_rename_collision_oneattr_multiple.rs:18:12
|
18 | pub fn foo(input: Point) -> bool {
| ^^^
error[E0433]: failed to resolve: use of undeclared crate or module `test_module`
--> $DIR/rhai_fn_rename_collision_oneattr_multiple.rs:25:8
|

View File

@ -81,15 +81,15 @@ The Rhai Scripting Language
2. [Overloading](language/overload.md)
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)
5. [Currying](language/fn-curry.md)
6. [Anonymous Functions](language/fn-anon.md)
7. [Closures](language/fn-closure.md)
16. [Print and Debug](language/print-debug.md)
17. [Modules](language/modules/index.md)
1. [Export Variables, Functions and Sub-Modules](language/modules/export.md)
2. [Import Modules](language/modules/import.md)
3. [Create from Rust](rust/modules/create.md)
4. [Create from AST](language/modules/ast.md)
4. [Create from AST](rust/modules/ast.md)
5. [Module Resolvers](rust/modules/resolvers.md)
1. [Custom Implementation](rust/modules/imp-resolver.md)
18. [Eval Statement](language/eval.md)

View File

@ -37,7 +37,7 @@ Dynamic
* Dynamic dispatch via [function pointers] with additional support for [currying].
* Closures via [automatic currying] with capturing shared variables from the external scope.
* [Closures] that can capture shared variables.
* Some support for [object-oriented programming (OOP)][OOP].

View File

@ -4,7 +4,7 @@ Keywords List
{{#include ../links.md}}
| Keyword | Description | Inactive under | Overloadable |
| :-------------------: | ---------------------------------------- | :-------------: | :----------: |
| :-------------------: | ------------------------------------------- | :-------------: | :----------: |
| `true` | boolean true literal | | no |
| `false` | boolean false literal | | no |
| `let` | variable declaration | | no |
@ -15,9 +15,9 @@ Keywords List
| `while` | while loop | | no |
| `loop` | infinite loop | | no |
| `for` | for loop | | no |
| `in` | containment test, part of for loop | | no |
| `in` | 1) containment test<br/>2) part of for loop | | no |
| `continue` | continue a loop at the next iteration | | no |
| `break` | loop breaking | | no |
| `break` | break out of loop iteration | | no |
| `return` | return value | | no |
| `throw` | throw exception | | no |
| `import` | import module | [`no_module`] | no |
@ -25,7 +25,7 @@ Keywords List
| `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`) | function to create a [function pointer] | | yes |
| `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 |

View File

@ -4,11 +4,11 @@ Literals Syntax
{{#include ../links.md}}
| Type | Literal syntax |
| :--------------------------------: | :------------------------------------------------------------------------------: |
| `INT` | `42`, `-123`, `0`,<br/>`0x????..` (hex), `0b????..` (binary), `0o????..` (octal) |
| :--------------------------------: | :-----------------------------------------------------------------------------------------: |
| `INT` | decimal: `42`, `-123`, `0`<br/>hex: `0x????..`<br/>binary: `0b????..`<br/>octal: `0o????..` |
| `FLOAT` | `42.0`, `-123.456`, `0.0` |
| [String] | `"... \x?? \u???? \U???????? ..."` |
| Character | `"... \x?? \u???? \U???????? ..."` |
| Character | single: `'?'`<br/>ASCII hex: `'\x??'`<br/>Unicode: `'\u????'`, `'\U????????'` |
| [`Array`] | `[ ???, ???, ??? ]` |
| [Object map] | `#{ a: ???, b: ???, c: ???, "def": ??? }` |
| Boolean true | `true` |

View File

@ -40,13 +40,13 @@ Symbols
| ------------ | ------------------------ |
| `:` | property value separator |
| `::` | module path separator |
| `#` | _Reserved_ |
| `=>` | _Reserved_ |
| `->` | _Reserved_ |
| `<-` | _Reserved_ |
| `===` | _Reserved_ |
| `!==` | _Reserved_ |
| `:=` | _Reserved_ |
| `::<` .. `>` | _Reserved_ |
| `@` | _Reserved_ |
| `(*` .. `*)` | _Reserved_ |
| `#` | _reserved_ |
| `=>` | _reserved_ |
| `->` | _reserved_ |
| `<-` | _reserved_ |
| `===` | _reserved_ |
| `!==` | _reserved_ |
| `:=` | _reserved_ |
| `::<` .. `>` | _reserved_ |
| `@` | _reserved_ |
| `(*` .. `*)` | _reserved_ |

View File

@ -4,7 +4,7 @@ Extend Rhai with Custom Syntax
{{#include ../links.md}}
For the ultimate advantageous, there is a built-in facility to _extend_ the Rhai language
For the ultimate adventurous, there is a built-in facility to _extend_ the Rhai language
with custom-defined _syntax_.
But before going off to define the next weird statement type, heed this warning:
@ -28,7 +28,7 @@ Where This Might Be Useful
* Where a custom syntax _significantly_ simplifies the code and _significantly_ enhances understanding of the code's intent.
* Where certain logic cannot be easily encapsulated inside a function. This is usually the case where _closures_ are required, because Rhai does not have closures.
* Where certain logic cannot be easily encapsulated inside a function.
* Where you just want to confuse your user and make their lives miserable, because you can.
@ -154,10 +154,10 @@ let result = engine.eval_expression_tree(context, scope, expr)?;
New variables maybe declared (usually with a variable name that is passed in via `$ident$).
It can simply be pushed into the [`scope`].
It can simply be pushed into the [`Scope`].
However, beware that all new variables must be declared _prior_ to evaluating any expression tree.
In other words, any `scope.push(...)` calls must come _before_ any `engine::eval_expression_tree(...)` calls.
In other words, any `Scope::push` calls must come _before_ any `Engine::eval_expression_tree` calls.
```rust
let var_name = inputs[0].get_variable_name().unwrap().to_string();

View File

@ -1,4 +1,3 @@
Raw `Engine`
===========

View File

@ -31,19 +31,20 @@ let mut scope = Scope::new();
scope
.push("y", 42_i64)
.push("z", 999_i64)
.push_constant("MY_NUMBER", 123_i64) // constants can also be added
.set_value("s", "hello, world!".to_string()); //'set_value' adds a variable when one doesn't exist
// remember to use 'String', not '&str'
// First invocation
engine.eval_with_scope::<()>(&mut scope, r"
let x = 4 + 5 - y + z + s.len;
let x = 4 + 5 - y + z + MY_NUMBER + s.len;
y = 1;
")?;
// Second invocation using the same state
let result = engine.eval_with_scope::<i64>(&mut scope, "x")?;
println!("result: {}", result); // prints 979
println!("result: {}", result); // prints 1102
// Variable y is changed in the script - read it with 'get_value'
assert_eq!(scope.get_value::<i64>("y").expect("variable y should exist"), 1);

View File

@ -31,19 +31,19 @@ Built-in Functions
The following methods (mostly defined in the [`BasicArrayPackage`][packages] but excluded if using a [raw `Engine`]) operate on arrays:
| Function | Parameter(s) | Description |
| ------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| ------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `push` | element to insert | inserts an element at the end |
| `+=` operator | array, element to insert (not another array) | inserts an element at the end |
| `append` | array to append | concatenates the second array to the end of the first |
| `+=` operator | array, array to append | concatenates the second array to the end of the first |
| `+` operator | first array, second array | concatenates the first array with the second |
| `insert` | element to insert, position<br/>(beginning if <= 0, end if >= length) | inserts an element at a certain index |
| `+=` operator | 1) array<br/>2) element to insert (not another array) | inserts an element at the end |
| `+=` operator | 1) array<br/>2) array to append | concatenates the second array to the end of the first |
| `+` operator | 1) first array<br/>2) second array | concatenates the first array with the second |
| `insert` | 1) element to insert<br/>2) position (beginning if <= 0, end if >= length) | inserts an element at a certain index |
| `pop` | _none_ | removes the last element and returns it ([`()`] if empty) |
| `shift` | _none_ | removes the first element and returns it ([`()`] if empty) |
| `remove` | index | removes an element at a particular index and returns it, or returns [`()`] if the index is not valid |
| `reverse` | _none_ | reverses the array |
| `len` method and property | _none_ | returns the number of elements |
| `pad` | element to pad, target length | pads the array with an element to at least a specified length |
| `pad` | 1) target length<br/>2) element to pad | pads the array with an element to at least a specified length |
| `clear` | _none_ | empties the array |
| `truncate` | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) |

View File

@ -16,8 +16,45 @@ x = 123; // <- syntax error: cannot assign to constant
```
Unlike variables which need not have initial values (default to [`()`]),
constants must be assigned one, and it must be a constant _value_, not an expression.
constants must be assigned one, and it must be a [_literal value_](../appendix/literals.md),
not an expression.
```rust
const x = 40 + 2; // <- syntax error: cannot assign expression to constant
```
Manually Add Constant into Custom Scope
--------------------------------------
It is possible to add a constant into a custom [`Scope`] so it'll be available to scripts
running with that [`Scope`].
When added to a custom [`Scope`], a constant can hold any value, not just a literal value.
It is very useful to have a constant value hold a [custom type], which essentially acts
as a [_singleton_](../patterns/singleton.md). The singleton object can be modified via its
registered API - being a constant only prevents it from being re-assigned or operated upon by Rhai;
mutating it via a Rust function is still allowed.
```rust
use rhai::{Engine, Scope};
struct TestStruct(i64); // custom type
let engine = Engine::new()
.register_type_with_name::<TestStruct>("TestStruct") // register custom type
.register_get_set("value",
|obj: &mut TestStruct| obj.0, // property getter
|obj: &mut TestStruct, value: i64| obj.0 = value // property setter
);
let mut scope = Scope::new(); // create custom scope
scope.push_constant("MY_NUMBER", TestStruct(123_i64)); // add constant variable
engine.consume_with_scope(&mut scope, r"
MY_NUMBER.value = 42; // constant objects can be modified
print(MY_NUMBER.value); // prints 42
")?;
```

View File

@ -3,6 +3,10 @@ Value Conversions
{{#include ../links.md}}
Convert Between Integer and Floating-Point
-----------------------------------------
The `to_float` function converts a supported number to `FLOAT` (defaults to `f64`).
The `to_int` function converts a supported number to `INT` (`i32` or `i64` depending on [`only_i32`]).
@ -22,3 +26,31 @@ let c = 'X'; // character
print("c is '" + c + "' and its code is " + c.to_int()); // prints "c is 'X' and its code is 88"
```
Parse String into Number
------------------------
The `parse_float` function converts a [string] into a `FLOAT` (defaults to `f64`).
The `parse_int` function converts a [string] into an `INT` (`i32` or `i64` depending on [`only_i32`]).
An optional radix (2-36) can be provided to parse the [string] into a number of the specified radix.
```rust
let x = parse_float("123.4"); // parse as floating-point
x == 123.4;
type_of(x) == "f64";
let dec = parse_int("42"); // parse as decimal
let dec = parse_int("42", 10); // radix = 10 is the default
dec == 42;
type_of(dec) == "i64";
let bin = parse_int("110", 2); // parse as binary (radix = 2)
bin == 0b110;
type_of(bin) == "i64";
let hex = parse_int("ab", 16); // parse as hex (radix = 16)
hex == 0xab;
type_of(hex) == "i64";
```

View File

@ -83,10 +83,14 @@ match item.type_name() { // 'type_name' returns the name
"i64" => ...
"alloc::string::String" => ...
"bool" => ...
"path::to::module::TestStruct" => ...
"crate::path::to::module::TestStruct" => ...
}
```
**Note:** `type_name` always returns the _full_ Rust path name of the type, even when the type
has been registered with a friendly name via `Engine::register_type_with_name`. This behavior
is different from that of the [`type_of`][`type_of()`] function in Rhai.
Conversion Traits
----------------
@ -100,4 +104,5 @@ The following conversion traits are implemented for `Dynamic`:
* `From<String>`
* `From<char>`
* `From<Vec<T>>` (into an [array])
* `From<HashMap<String, T>>` (into an [object map]).
* `From<HashMap<String, T>>` (into an [object map])
* `From<Instant>` (into a [timestamp] if not [`no_std`])

View File

@ -23,43 +23,25 @@ Therefore, similar to closures in many languages, these captured shared values p
reference counting, and may be read or modified even after the variables that hold them
go out of scope and no longer exist.
Use the `is_shared` function to check whether a particular value is a shared value.
Use the `Dynamic::is_shared` function to check whether a particular value is a shared value.
Automatic currying can be turned off via the [`no_closure`] feature.
Actual Implementation
---------------------
The actual implementation de-sugars to:
1. Keeping track of what variables are accessed inside the anonymous function,
2. If a variable is not defined within the anonymous function's scope, it is looked up _outside_ the function and
in the current execution scope - where the anonymous function is created.
3. The variable is added to the parameters list of the anonymous function, at the front.
4. The variable is then converted into a **reference-counted shared value**.
An [anonymous function] which captures an external variable is the only way to create a reference-counted shared value in Rhai.
5. The shared value is then [curried][currying] into the [function pointer] itself, essentially carrying a reference to that shared value
and inserting it into future calls of the function.
This process is called _Automatic Currying_, and is the mechanism through which Rhai simulates normal closures.
Examples
--------
```rust
let x = 1; // a normal variable
x.is_shared() == false;
let f = |y| x + y; // variable 'x' is auto-curried (captured) into 'f'
x.is_shared() == true; // 'x' is now a shared value!
f.call(2) == 3; // 1 + 2 == 3
x = 40; // changing 'x'...
f.call(2) == 42; // the value of 'x' is 40 because 'x' is shared
@ -117,6 +99,8 @@ will occur and the script will terminate with an error.
```rust
let x = 20;
x.is_shared() == false; // 'x' is not shared, so no data race is possible
let f = |a| this += x + a; // 'x' is captured in this closure
x.is_shared() == true; // now 'x' is shared
@ -152,6 +136,26 @@ x.call(f, 2);
TL;DR
-----
### Q: How is it actually implemented?
The actual implementation of closures de-sugars to:
1. Keeping track of what variables are accessed inside the anonymous function,
2. If a variable is not defined within the anonymous function's scope, it is looked up _outside_ the function and
in the current execution scope - where the anonymous function is created.
3. The variable is added to the parameters list of the anonymous function, at the front.
4. The variable is then converted into a **reference-counted shared value**.
An [anonymous function] which captures an external variable is the only way to create a reference-counted shared value in Rhai.
5. The shared value is then [curried][currying] into the [function pointer] itself, essentially carrying a reference to that shared value
and inserting it into future calls of the function.
This process is called _Automatic Currying_, and is the mechanism through which Rhai simulates normal closures.
### Q: Why are closures implemented as automatic currying?
In concept, a closure _closes_ over captured variables from the outer scope - that's why

View File

@ -10,7 +10,7 @@ Each Function is a Separate Compilation Unit
This means that individual functions can be separated, exported, re-grouped, imported,
and generally mix-'n-match-ed with other completely unrelated scripts.
For example, the `AST::merge` method allows Global all functions in one [`AST`] into another,
For example, the `AST::merge` method allows merging all functions in one [`AST`] into another,
forming a new, combined, group of functions.
In general, there are two types of _namespaces_ where functions are looked up:
@ -43,7 +43,9 @@ This aspect is very similar to JavaScript before ES6 modules.
// Compile a script into AST
let ast1 = engine.compile(
r#"
fn get_message() { "Hello!" } // greeting message
fn get_message() {
"Hello!" // greeting message
}
fn say_hello() {
print(get_message()); // prints message
@ -136,7 +138,7 @@ the subsequent call using the _namespace-qualified_ function name fails to find
function named '`message`' in the global namespace.
Therefore, when writing functions for a [module] intended for the [`GlobalFileModuleResolver`][module resolver],
make sure that those functions are as _pure_ as possible and avoid cross-calling them from each other.
make sure that those functions are as independent as possible and avoid cross-calling them from each other.
A [function pointer] is a valid technique to call another function in an environment-independent manner:

View File

@ -61,8 +61,8 @@ hello.call(0); // error: function not found - 'hello_world (i64)'
Global Namespace Only
--------------------
Because of their dynamic nature, function pointers cannot refer to functions in a _module_ [namespace][function namespace]
(i.e. functions in [`import`]-ed modules). They can only refer to functions within the global [namespace][function namespace].
Because of their dynamic nature, function pointers cannot refer to functions in [`import`]-ed [modules].
They can only refer to functions within the global [namespace][function namespace].
See [function namespaces] for more details.
```rust

View File

@ -44,10 +44,13 @@ Representation of Numbers
------------------------
JSON numbers are all floating-point while Rhai supports integers (`INT`) and floating-point (`FLOAT`) if
the [`no_float`] feature is not used. Most common generators of JSON data distinguish between
integer and floating-point values by always serializing a floating-point number with a decimal point
(i.e. `123.0` instead of `123` which is assumed to be an integer). This style can be used successfully
with Rhai [object maps].
the [`no_float`] feature is not used.
Most common generators of JSON data distinguish between integer and floating-point values by always
serializing a floating-point number with a decimal point (i.e. `123.0` instead of `123` which is
assumed to be an integer).
This style can be used successfully with Rhai [object maps].
Parse JSON with Sub-Objects
@ -68,9 +71,10 @@ A JSON object hash starting with `#{` is handled transparently by `Engine::parse
// JSON with sub-object 'b'.
let json = r#"{"a":1, "b":{"x":true, "y":false}}"#;
let new_json = json.replace("{" "#{");
// Our JSON text does not contain the '{' character, so off we go!
let new_json = json.replace("{", "#{");
// The leading '{' will also be replaced to '#{', but parse_json can handle this.
// The leading '{' will also be replaced to '#{', but 'parse_json' handles this just fine.
let map = engine.parse_json(&new_json, false)?;
map.len() == 2; // 'map' contains two properties: 'a' and 'b'

View File

@ -48,9 +48,9 @@ an equivalent method coded in script, where the object is accessed via the `this
The following table illustrates the differences:
| Function type | Parameters | Object reference | Function signature |
| :-----------: | :--------: | :--------------------: | :-----------------------------------------------------: |
| Native Rust | _n_ + 1 | first `&mut` parameter | `fn method<T, U, V>`<br/>`(obj: &mut T, x: U, y: V) {}` |
| Rhai script | _n_ | `this` | `fn method(x, y) {}` |
| :-----------: | :--------: | :-----------------------: | :---------------------------: |
| Native Rust | _N_ + 1 | first `&mut T` parameter | `Fn(obj: &mut T, x: U, y: V)` |
| Rhai script | _N_ | `this` (of type `&mut T`) | `Fn(x: U, y: V)` |
`&mut` is Efficient, Except for `ImmutableString`

View File

@ -1,53 +0,0 @@
Create a Module from an AST
==========================
{{#include ../../links.md}}
It is easy to convert a pre-compiled [`AST`] into a module: just use `Module::eval_ast_as_new`.
Don't forget the [`export`] statement, otherwise there will be no variables exposed by the module
other than non-[`private`] functions (unless that's intentional).
```rust
use rhai::{Engine, Module};
let engine = Engine::new();
// Compile a script into an 'AST'
let ast = engine.compile(r#"
// Functions become module functions
fn calc(x) {
x + 1
}
fn add_len(x, y) {
x + y.len
}
// Imported modules can become sub-modules
import "another module" as extra;
// Variables defined at global level can become module variables
const x = 123;
let foo = 41;
let hello;
// Variable values become constant module variable values
foo = calc(foo);
hello = "hello, " + foo + " worlds!";
// Finally, export the variables and modules
export
x as abc, // aliased variable name
foo,
hello,
extra as foobar; // export sub-module
"#)?;
// Convert the 'AST' into a module, using the 'Engine' to evaluate it first
let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
// 'module' now can be loaded into a custom 'Scope' for future use. It contains:
// - sub-module: 'foobar' (renamed from 'extra')
// - functions: 'calc', 'add_len'
// - variables: 'abc' (renamed from 'x'), 'foo', 'hello'
```

View File

@ -3,27 +3,16 @@ Export Variables, Functions and Sub-Modules in Module
{{#include ../../links.md}}
A _module_ is a single script (or pre-compiled [`AST`]) containing global variables, functions and sub-modules.
A module can be created from a script via the `Module::eval_ast_as_new` method. When given an [`AST`],
it is first evaluated, then the following items are exposed as members of the new module:
* Global variables - essentially all variables that remain in the [`Scope`] at the end of a script run - that are exported. Variables not exported (via the `export` statement) remain hidden.
* Functions not specifically marked `private`.
* Global modules that remain in the [`Scope`] at the end of a script run.
Global Variables
----------------
Export Global Variables
----------------------
The `export` statement, which can only be at global level, exposes selected variables as members of a module.
Variables not exported are _private_ and hidden. They are merely used to initialize the module,
but cannot be accessed from outside.
Everything exported from a module is **constant** (**read-only**).
Everything exported from a module is **constant** (i.e. read-only).
```rust
// This is a module script.
@ -45,8 +34,8 @@ export x as answer; // the variable 'x' is exported under the alias 'answer'
```
Functions
---------
Export Functions
----------------
All functions are automatically exported, _unless_ it is explicitly opt-out with the [`private`] prefix.

View File

@ -10,25 +10,23 @@ If an [object map]'s property holds a [function pointer], the property can simpl
a normal method in method-call syntax. This is a _short-hand_ to avoid the more verbose syntax
of using the `call` function keyword.
When a property holding a [function pointer] (which incudes [closures]) is called like a method,
When a property holding a [function pointer] or a [closure] is called like a method,
what happens next depends on whether the target function is a native Rust function or
a script-defined function.
If it is a registered native Rust method function, it is called directly.
* If it is a registered native Rust function, it is called directly in _method-call_ style with the [object map] inserted as the first argument.
If it is a script-defined function, the `this` variable within the function body is bound
to the [object map] before the function is called. There is no way to simulate this behavior
via a normal function-call syntax because all scripted function arguments are passed by value.
* If it is a script-defined function, the `this` variable within the function body is bound to the [object map] before the function is called.
```rust
let obj = #{
data: 40,
action: || this.data += x // 'action' holds a function pointer which is a closure
action: || this.data += x // 'action' holds a closure
};
obj.action(2); // Calls the function pointer with `this` bound to 'obj'
obj.action(2); // calls the function pointer with `this` bound to 'obj'
obj.call(obj.action, 2); // The above de-sugars to this
obj.call(obj.action, 2); // <- the above de-sugars to this
obj.data == 42;

View File

@ -58,13 +58,13 @@ The following methods (defined in the [`BasicMapPackage`][packages] but excluded
operate on object maps:
| Function | Parameter(s) | Description |
| ---------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| ---------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `has` | property name | does the object map contain a property of a particular name? |
| `len` | _none_ | returns the number of properties |
| `clear` | _none_ | empties the object map |
| `remove` | property name | removes a certain property and returns it ([`()`] if the property does not exist) |
| `+=` operator, `mixin` | second object map | mixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values) |
| `+` operator | first object map, second object map | merges the first object map with the second |
| `+` operator | 1) first object map<br/>2) second object map | merges the first object map with the second |
| `fill_with` | second object map | adds in all properties of the second object map that do not exist in the object map |
| `keys` | _none_ | returns an [array] of all the property names (in random order), not available under [`no_index`] |
| `values` | _none_ | returns an [array] of all the property values (in random order), not available under [`no_index`] |

View File

@ -7,17 +7,18 @@ The following standard methods (mostly defined in the [`MoreStringPackage`][pack
using a [raw `Engine`]) operate on [strings]:
| Function | Parameter(s) | Description |
| ------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| ------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `len` method and property | _none_ | returns the number of characters (not number of bytes) in the string |
| `pad` | character to pad, target length | pads the string with an character to at least a specified length |
| `pad` | 1) character to pad<br/>2) target length | pads the string with an character to at least a specified length |
| `+=` operator, `append` | character/string to append | Adds a character or a string to the end of another string |
| `clear` | _none_ | empties the string |
| `truncate` | target length | cuts off the string at exactly a specified number of characters |
| `contains` | character/sub-string to search for | checks if a certain character or sub-string occurs in the string |
| `index_of` | character/sub-string to search for, start index _(optional)_ | returns the index that a certain character or sub-string occurs in the string, or -1 if not found |
| `sub_string` | start index, length _(optional)_ | extracts a sub-string (to the end of the string if length is not specified) |
| `crop` | start index, length _(optional)_ | retains only a portion of the string (to the end of the string if length is not specified) |
| `replace` | target character/sub-string, replacement character/string | replaces a sub-string with another |
| `index_of` | 1) character/sub-string to search for<br/>2) start index _(optional)_ | returns the index that a certain character or sub-string occurs in the string, or -1 if not found |
| `sub_string` | 1) start index<br/>2) length _(optional)_ | extracts a sub-string (to the end of the string if length is not specified) |
| `split` | delimiter character/string | splits the string by the specified delimiter, returning an [array] of string segments; not available under [`no_index`] |
| `crop` | 1) start index<br/>2) length _(optional)_ | retains only a portion of the string (to the end of the string if length is not specified) |
| `replace` | 1) target character/sub-string<br/>2) replacement character/string | replaces a sub-string with another |
| `trim` | _none_ | trims the string of whitespace at the beginning and end |
Examples

View File

@ -44,16 +44,16 @@ Hex sequences map to ASCII characters, while '`\u`' maps to 16-bit common Unicod
Standard escape sequences:
| Escape sequence | Meaning |
| --------------- | ------------------------------ |
| --------------- | -------------------------------- |
| `\\` | back-slash `\` |
| `\t` | tab |
| `\r` | carriage-return `CR` |
| `\n` | line-feed `LF` |
| `\"` | double-quote `"` in strings |
| `\'` | single-quote `'` in characters |
| `\x`_xx_ | Unicode in 2-digit hex |
| `\u`_xxxx_ | Unicode in 4-digit hex |
| `\U`_xxxxxxxx_ | Unicode in 8-digit hex |
| `\"` | double-quote `"` |
| `\'` | single-quote `'` |
| `\x`_xx_ | ASCII character in 2-digit hex |
| `\u`_xxxx_ | Unicode character in 4-digit hex |
| `\U`_xxxxxxxx_ | Unicode character in 8-digit hex |
Differences from Rust Strings

View File

@ -19,9 +19,11 @@ Built-in Functions
The following methods (defined in the [`BasicTimePackage`][packages] but excluded if using a [raw `Engine`]) operate on timestamps:
| Function | Parameter(s) | Description |
| ----------------------------- | ---------------------------------- | -------------------------------------------------------- |
| ----------------------------- | ------------------------------------------- | -------------------------------------------------------- |
| `elapsed` method and property | _none_ | returns the number of seconds since the timestamp |
| `-` operator | later timestamp, earlier timestamp | returns the number of seconds between the two timestamps |
| `-` operator | 1) later timestamp<br/>2) earlier timestamp | returns the number of seconds between the two timestamps |
| `+` operator | number of seconds to add | returns a new timestamp |
| `-` operator | number of seconds to subtract | returns a new timestamp |
Examples

View File

@ -24,3 +24,24 @@ if type_of(x) == "string" {
do_something_with_string(x);
}
```
Custom Types
------------
`type_of()` a [custom type] returns:
* if registered via `Engine::register_type_with_name` - the registered name
* if registered via `Engine::register_type` - the full Rust path name
```rust
struct TestStruct1;
struct TestStruct2;
engine
// type_of(struct1) == "crate::path::to::module::TestStruct1"
.register_type::<TestStruct1>()
// type_of(struct2) == "MyStruct"
.register_type_with_name::<TestStruct2>("MyStruct");
```

View File

@ -49,6 +49,8 @@
[`Dynamic`]: {{rootUrl}}/language/dynamic.md
[`to_int`]: {{rootUrl}}/language/convert.md
[`to_float`]: {{rootUrl}}/language/convert.md
[`parse_int`]: {{rootUrl}}/language/convert.md
[`parse_float`]: {{rootUrl}}/language/convert.md
[custom type]: {{rootUrl}}/rust/custom.md
[custom types]: {{rootUrl}}/rust/custom.md

View File

@ -51,6 +51,12 @@ let config: Rc<RefCell<Config>> = Rc::new(RefCell::new(Default::default()));
### Register Config API
The trick to building a Config API is to clone the shared configuration object and
move it into each function registration as a closure.
It is not possible to use a [plugin module] to achieve this, so each function must
be registered one after another.
```rust
// Notice 'move' is used to move the shared configuration object into the closure.
let cfg = config.clone();

View File

@ -64,6 +64,12 @@ let bunny: Rc<RefCell<EnergizerBunny>> = Rc::new(RefCell::(EnergizerBunny::new()
### Register Control API
The trick to building a Control API is to clone the shared API object and
move it into each function registration as a closure.
It is not possible to use a [plugin module] to achieve this, so each function must
be registered one after another.
```rust
// Notice 'move' is used to move the shared API object into the closure.
let b = bunny.clone();

View File

@ -58,7 +58,7 @@ impl EnergizerBunny {
pub fn new () -> Self { ... }
pub fn go (&mut self) { ... }
pub fn stop (&mut self) { ... }
pub fn is_going (&self) { ... }
pub fn is_going (&self) -> bol { ... }
pub fn get_speed (&self) -> i64 { ... }
pub fn set_speed (&mut self, speed: i64) { ... }
pub fn turn (&mut self, left_turn: bool) { ... }
@ -77,13 +77,23 @@ let SharedBunnyType = Rc<RefCell<EnergizerBunny>>;
engine.register_type_with_name::<SharedBunnyType>("EnergizerBunny");
```
### Register Methods and Getters/Setters
### Develop a Plugin with Methods and Getters/Setters
The easiest way to develop a complete set of API for a [custom type] is via a [plugin module].
```rust
engine
.register_get_set("power",
|bunny: &mut SharedBunnyType| bunny.borrow().is_going(),
|bunny: &mut SharedBunnyType, on: bool| {
use rhai::plugins::*;
#[export_module]
pub mod bunny_api {
pub const MAX_SPEED: i64 = 100;
#[rhai_fn(get = "power")]
pub fn get_power(bunny: &mut SharedBunnyType) -> bool {
bunny.borrow().is_going()
}
#[rhai_fn(set = "power")]
pub fn set_power(bunny: &mut SharedBunnyType, on: bool) {
if on {
if bunny.borrow().is_going() {
println!("Still going...");
@ -98,16 +108,21 @@ engine
}
}
}
).register_get("speed", |bunny: &mut SharedBunnyType| {
#[rhai_fn(get = "speed")]
pub fn get_speed(bunny: &mut SharedBunnyType) -> i64 {
if bunny.borrow().is_going() {
bunny.borrow().get_speed()
} else {
0
}
}).register_set_result("speed", |bunny: &mut SharedBunnyType, speed: i64| {
}
#[rhai_fn(set = "speed", return_raw)]
pub fn set_speed(bunny: &mut SharedBunnyType, speed: i64)
-> Result<Dynamic, Box<EvalAltResult>>
{
if speed <= 0 {
Err("Speed must be positive!".into())
} else if speed > 100 {
} else if speed > MAX_SPEED {
Err("Bunny will be going too fast!".into())
} else if !bunny.borrow().is_going() {
Err("Bunny is not yet going!".into())
@ -115,15 +130,20 @@ engine
b.borrow_mut().set_speed(speed);
Ok(().into())
}
}).register_fn("turn_left", |bunny: &mut SharedBunnyType| {
}
pub fn turn_left(bunny: &mut SharedBunnyType) {
if bunny.borrow().is_going() {
bunny.borrow_mut().turn(true);
}
}).register_fn("turn_right", |bunny: &mut SharedBunnyType| {
}
pub fn turn_right(bunny: &mut SharedBunnyType) {
if bunny.borrow().is_going() {
bunny.borrow_mut().turn(false);
}
});
}
}
engine.load_package(exported_module!(bunny_api));
```
### Push Constant Command Object into Custom Scope
@ -132,7 +152,9 @@ engine
let bunny: SharedBunnyType = Rc::new(RefCell::(EnergizerBunny::new()));
let mut scope = Scope::new();
scope.push_constant("BUNNY", bunny.clone());
// Add the command object into a custom Scope.
scope.push_constant("Bunny", bunny.clone());
engine.consume_with_scope(&mut scope, script)?;
```
@ -140,11 +162,11 @@ engine.consume_with_scope(&mut scope, script)?;
### Use the Command API in Script
```rust
// Access the command object via constant variable 'BUNNY'.
// Access the command object via constant variable 'Bunny'.
if !BUNNY.power { BUNNY.power = true; }
if !Bunny.power { Bunny.power = true; }
if BUNNY.speed > 50 { BUNNY.speed = 50; }
if Bunny.speed > 50 { Bunny.speed = 50; }
BUNNY.turn_left();
Bunny.turn_left();
```

View File

@ -11,11 +11,11 @@ individual functions instead of a full-blown [plugin module].
Macros
------
| Macro | Apply to | Description |
| ----------------------- | --------------------------------------------------------------- | ------------------------------------------------------------- |
| `#[export_fn]` | rust function defined in a Rust module | exports the function |
| `register_exported_fn!` | [`Engine`] instance, register name string, use path to function | registers the function into an [`Engine`] under specific name |
| `set_exported_fn!` | [`Module`] instance, register name string, use path to function | registers the function into an [`Module`] under specific name |
| Macro | Signature | Description |
| ----------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------- |
| `#[export_fn]` | apply to rust function defined in a Rust module | exports the function |
| `register_exported_fn!` | `register_exported_fn!(&mut `_engine_`, "`_name_`", `_function_`)` | registers the function into an [`Engine`] under a specific name |
| `set_exported_fn!` | `set_exported_fn!(&mut `_module_`, "`_name_`", `_function_`)` | registers the function into a [`Module`] under a specific name |
`#[export_fn]` and `register_exported_fn!`

View File

@ -29,9 +29,9 @@ use rhai::plugins::*; // a "prelude" import for macros
#[export_module]
mod my_module {
// This constant will be registered as the constant variable 'SOME_NUMBER'.
// This constant will be registered as the constant variable 'MY_NUMBER'.
// Ignored when loaded as a package.
pub const SOME_NUMBER: i64 = 42;
pub const MY_NUMBER: i64 = 42;
// This function will be registered as 'greet'.
pub fn greet(name: &str) -> String {
@ -261,11 +261,11 @@ Inner attributes can be applied to the inner items of a module to tweak the expo
Parameters should be set on inner attributes to specify the desired behavior.
| Attribute Parameter | Use with | Apply to | Description |
| ------------------- | --------------------------- | -------------------------------------------------------- | ------------------------------------------------------ |
| ------------------- | --------------------------- | ----------------------------------------------------- | ------------------------------------------------------ |
| `skip` | `#[rhai_fn]`, `#[rhai_mod]` | function or sub-module | do not export this function/sub-module |
| `name = "..."` | `#[rhai_fn]`, `#[rhai_mod]` | function or sub-module | registers function/sub-module under the specified name |
| `get = "..."` | `#[rhai_fn]` | function with `&mut` first parameter | registers a getter for the named property |
| `set = "..."` | `#[rhai_fn]` | function with `&mut` first parameter | registers a setter for the named property |
| `index_get` | `#[rhai_fn]` | function with `&mut` first parameter | registers an index getter |
| `index_set` | `#[rhai_fn]` | function with `&mut` first parameter | registers an index setter |
| `return_raw` | `#[rhai_fn]` | function returning `Result<Dynamic, Box<EvalAltResult>>` | marks this as a [fallible function] |
| `get = "..."` | `#[rhai_fn]` | `pub fn (&mut Type) -> Value` | registers a getter for the named property |
| `set = "..."` | `#[rhai_fn]` | `pub fn (&mut Type, Value)` | registers a setter for the named property |
| `index_get` | `#[rhai_fn]` | `pub fn (&mut Type, INT) -> Value` | registers an index getter |
| `index_set` | `#[rhai_fn]` | `pub fn (&mut Type, INT, Value)` | registers an index setter |
| `return_raw` | `#[rhai_fn]` | `pub fn (...) -> Result<Dynamic, Box<EvalAltResult>>` | marks this as a [fallible function] |

View File

@ -0,0 +1,88 @@
Create a Module from an AST
==========================
{{#include ../../links.md}}
`Module::eval_ast_as_new`
------------------------
A _module_ can be created from a single script (or pre-compiled [`AST`]) containing global variables,
functions and sub-modules via the `Module::eval_ast_as_new` method.
When given an [`AST`], it is first evaluated, then the following items are exposed as members of the new module:
* Global variables - essentially all variables that remain in the [`Scope`] at the end of a script run - that are exported. Variables not exported (via the `export` statement) remain hidden.
* Functions not specifically marked `private`.
* Global modules that remain in the [`Scope`] at the end of a script run.
`merge_namespaces` Parameter
---------------------------
The parameter `merge_namespaces` in `Module::eval_ast_as_new` determines the exact behavior of
functions exposed by the module and the namespace that they can access:
| `merge_namespaces` value | Description | Namespace | Performance | Call global functions | Call functions in same module |
| :----------------------: | ------------------------------------------------ | :-----------------: | :---------: | :-------------------: | :---------------------------: |
| `true` | encapsulate entire `AST` into each function call | module, then global | slower | yes | yes |
| `false` | register each function independently | global only | fast | yes | no |
Examples
--------
Don't forget the [`export`] statement, otherwise there will be no variables exposed by the module
other than non-[`private`] functions (unless that's intentional).
```rust
use rhai::{Engine, Module};
let engine = Engine::new();
// Compile a script into an 'AST'
let ast = engine.compile(r#"
// Functions become module functions
fn calc(x) {
x + 1
}
fn add_len(x, y) {
x + y.len
}
// Imported modules can become sub-modules
import "another module" as extra;
// Variables defined at global level can become module variables
const x = 123;
let foo = 41;
let hello;
// Variable values become constant module variable values
foo = calc(foo);
hello = "hello, " + foo + " worlds!";
// Finally, export the variables and modules
export
x as abc, // aliased variable name
foo,
hello,
extra as foobar; // export sub-module
"#)?;
// Convert the 'AST' into a module, using the 'Engine' to evaluate it first
//
// The second parameter ('merge_namespaces'), when set to true, will encapsulate
// a copy of the entire 'AST' into each function, allowing functions in the module script
// to cross-call each other.
//
// This incurs additional overhead, avoidable by setting 'merge_namespaces' to false.
let module = Module::eval_ast_as_new(Scope::new(), &ast, true, &engine)?;
// 'module' now contains:
// - sub-module: 'foobar' (renamed from 'extra')
// - functions: 'calc', 'add_len'
// - constants: 'abc' (renamed from 'x'), 'foo', 'hello'
```

View File

@ -22,19 +22,38 @@ For the complete `Module` API, refer to the [documentation](https://docs.rs/rhai
Make the `Module` Available to the `Engine`
------------------------------------------
In order to _use_ a custom module, there must be a [module resolver], which serves the module when
loaded via `import` statements.
`Engine::load_package` supports loading a [module] as a [package].
Since it acts as a [package], all functions will be registered into the _global_ namespace
and can be accessed without _module qualifiers_.
```rust
use rhai::{Engine, Module};
let mut module = Module::new(); // new module
module.set_fn_1("inc", |x: i64| Ok(x+1)); // use the 'set_fn_XXX' API to add functions
// Load the module into the Engine as a new package.
let mut engine = Engine::new();
engine.load_package(module);
engine.eval::<i64>("inc(41)")? == 42; // no need to import module
```
Make the `Module` Dynamically Loadable
-------------------------------------
In order to dynamically load a custom module, there must be a [module resolver] which serves
the module when loaded via `import` statements.
The easiest way is to use, for example, the [`StaticModuleResolver`][module resolver] to hold such
a custom module.
```rust
use rhai::{Engine, Scope, Module, i64};
use rhai::{Engine, Scope, Module};
use rhai::module_resolvers::StaticModuleResolver;
let mut engine = Engine::new();
let mut scope = Scope::new();
let mut module = Module::new(); // new module
module.set_var("answer", 41_i64); // variable 'answer' under module
module.set_fn_1("inc", |x: i64| Ok(x+1)); // use the 'set_fn_XXX' API to add functions
@ -47,11 +66,12 @@ let mut resolver = StaticModuleResolver::new();
resolver.insert("question", module);
// Set the module resolver into the 'Engine'
let mut engine = Engine::new();
engine.set_module_resolver(Some(resolver));
// Use module-qualified variables
engine.eval::<i64>(&scope, r#"import "question" as q; q::answer + 1"#)? == 42;
engine.eval::<i64>(r#"import "question" as q; q::answer + 1"#)? == 42;
// Call module-qualified functions
engine.eval::<i64>(&scope, r#"import "question" as q; q::inc(q::answer)"#)? == 42;
engine.eval::<i64>(r#"import "question" as q; q::inc(q::answer)"#)? == 42;
```

View File

@ -11,8 +11,14 @@ A module resolver must implement the trait [`rhai::ModuleResolver`][traits],
which contains only one function: `resolve`.
When Rhai prepares to load a module, `ModuleResolver::resolve` is called with the name
of the _module path_ (i.e. the path specified in the [`import`] statement). Upon success, it should
return a [`Module`]; if the module cannot be load, return `EvalAltResult::ErrorModuleNotFound`.
of the _module path_ (i.e. the path specified in the [`import`] statement).
* Upon success, it should return a [`Module`].
* If the path does not resolve to a valid module, return `EvalAltResult::ErrorModuleNotFound`.
* If the module failed to load, return `EvalAltResult::ErrorInModule`.
Example
-------
@ -35,9 +41,12 @@ impl ModuleResolver for MyModuleResolver {
// Check module path.
if is_valid_module_path(path) {
// Load the custom module.
let module: Module = load_secret_module(path);
Ok(module)
load_secret_module(path).map_err(|err|
// Return EvalAltResult::ErrorInModule upon loading error
EvalAltResult::ErrorInModule(err.to_string(), pos).into()
)
} else {
// Return EvalAltResult::ErrorModuleNotFound if the path is invalid
Err(EvalAltResult::ErrorModuleNotFound(path.into(), pos).into())
}
}
@ -50,7 +59,7 @@ engine.set_module_resolver(Some(MyModuleResolver {}));
engine.consume(r#"
import "hello" as foo; // this 'import' statement will call
// 'MyModuleResolver::resolve' with "hello" as path
// 'MyModuleResolver::resolve' with "hello" as `path`
foo:bar();
"#)?;
```

View File

@ -12,16 +12,120 @@ Built-In Module Resolvers
------------------------
There are a number of standard resolvers built into Rhai, the default being the `FileModuleResolver`
which simply loads a script file based on the path (with `.rhai` extension attached) and execute it to form a module.
which simply loads a script file based on the path (with `.rhai` extension attached)
and execute it to form a module.
Built-in module resolvers are grouped under the `rhai::module_resolvers` module namespace.
| Module Resolver | Description | Namespace |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------: |
| `FileModuleResolver` | The default module resolution service, not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.<br/>The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.<br/>`FileModuleResolver::create_module()` loads a script file and returns a module. | Module (cannot access global namespace) |
| `GlobalFileModuleResolver` | A simpler but more efficient version of `FileModuleResolver`, intended for short utility modules. Not available under [`no_std`] or [WASM] builds. Loads a script file (based off the current directory) with `.rhai` extension.<br/>**Note:** All functions are assumed absolutely _pure_ and cannot cross-call each other.<br/>The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.<br/>`FileModuleResolver::create_module()` loads a script file and returns a module. | Global |
| `StaticModuleResolver` | Loads modules that are statically added. This can be used under [`no_std`]. | Global |
| `ModuleResolversCollection` | A collection of module resolvers. Modules will be resolved from each resolver in sequential order.<br/>This is useful when multiple types of modules are needed simultaneously. | Global |
`FileModuleResolver` (default)
-----------------------------
The _default_ module resolution service, not available for [`no_std`] or [WASM] builds.
Loads a script file (based off the current directory) with `.rhai` extension.
All functions in the _global_ namespace, plus all those defined in the same module,
are _merged_ into a _unified_ namespace.
```rust
------------------
| my_module.rhai |
------------------
private fn inner_message() { "hello! from module!" }
fn greet(callback) { print(callback.call()); }
-------------
| main.rhai |
-------------
fn main_message() { "hi! from main!" }
import "my_module" as m;
m::greet(|| "hello, " + "world!"); // works - anonymous function in global
m::greet(|| inner_message()); // works - function in module
m::greet(|| main_message()); // works - function in global
```
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.
`GlobalFileModuleResolver`
-------------------------
A simpler but more efficient version of `FileModuleResolver`, intended for short utility modules.
Not available for [`no_std`] or [WASM] builds.
Loads a script file (based off the current directory) with `.rhai` extension.
All functions are assumed **independent** and _cannot_ cross-call each other.
Functions are searched _only_ in the _global_ namespace.
```rust
------------------
| my_module.rhai |
------------------
private fn inner_message() { "hello! from module!" }
fn greet_inner() {
print(inner_message()); // cross-calling a module function!
// there will be trouble because each function
// in the module is supposed to be independent
// of each other
}
fn greet() {
print(main_message()); // function is searched in global namespace
}
-------------
| main.rhai |
-------------
fn main_message() { "hi! from main!" }
import "my_module" as m;
m::greet_inner(); // <- function not found: 'inner_message'
m::greet(); // works because 'main_message' exists in
// the global namespace
```
The base directory can be changed via the `FileModuleResolver::new_with_path` constructor function.
`GlobalFileModuleResolver::create_module` loads a script file and returns a module.
`StaticModuleResolver`
---------------------
Loads modules that are statically added. This can be used under [`no_std`].
Functions are searched in the _global_ namespace by default.
```rust
use rhai::{Module, module_resolvers::StaticModuleResolver};
let module: Module = create_a_module();
let mut resolver = StaticModuleResolver::new();
resolver.insert("my_module", module);
```
`ModuleResolversCollection`
--------------------------
A collection of module resolvers. Modules will be resolved from each resolver in sequential order.
This is useful when multiple types of modules are needed simultaneously.
Set into `Engine`
@ -30,8 +134,14 @@ Set into `Engine`
An [`Engine`]'s module resolver is set via a call to `Engine::set_module_resolver`:
```rust
// Use the 'StaticModuleResolver'
let resolver = rhai::module_resolvers::StaticModuleResolver::new();
use rhai::module_resolvers::StaticModuleResolver;
// Create a module resolver
let resolver = StaticModuleResolver::new();
// Register functions into 'resolver'...
// Use the module resolver
engine.set_module_resolver(Some(resolver));
// Effectively disable 'import' statements by setting module resolver to 'None'

View File

@ -34,9 +34,11 @@ Macro Parameters
```rust
// Import necessary types and traits.
use rhai::{
def_package,
packages::Package,
packages::{ArithmeticPackage, BasicArrayPackage, BasicMapPackage, LogicPackage}
def_package, // 'def_package!' macro
packages::Package, // 'Package' trait
packages::{ // pre-defined packages
ArithmeticPackage, BasicArrayPackage, BasicMapPackage, LogicPackage
}
};
// Define the package 'MyPackage'.

View File

@ -9,10 +9,12 @@ Packages reside under `rhai::packages::*` and the trait `rhai::packages::Package
packages to be used.
Packages typically contain Rust functions that are callable within a Rhai script.
All functions registered in a package is loaded under the _global namespace_ (i.e. they're available without module qualifiers).
All functions registered in a package is loaded under the _global namespace_
(i.e. they're available without module qualifiers).
Once a package is created (e.g. via `Package::new`), it can be _shared_ (via `Package::get`) among multiple instances of [`Engine`],
even across threads (under [`sync`]). Therefore, a package only has to be created _once_.
Once a package is created (e.g. via `Package::new`), it can be _shared_ (via `Package::get`)
among multiple instances of [`Engine`], even across threads (under [`sync`]).
Therefore, a package only has to be created _once_.
```rust
use rhai::Engine;

View File

@ -85,10 +85,10 @@ Extract Arguments
To extract an argument from the `args` parameter (`&mut [&mut Dynamic]`), use the following:
| Argument type | Access (`n` = argument position) | Result |
| ------------------------------ | ------------------------------------- | --------------------------------------------------------- |
| ------------------------------ | ------------------------------------- | ----------------------------------------------------- |
| [Primary type][standard types] | `args[n].clone().cast::<T>()` | copy of value |
| Custom type | `args[n].read_lock::<T>().unwrap()` | immutable reference to value |
| Custom type (consumed) | `std::mem::take(args[n]).cast::<T>()` | the _consumed_ value.<br/>The original value becomes `()` |
| [Custom type] | `args[n].read_lock::<T>().unwrap()` | immutable reference to value |
| [Custom type] (consumed) | `std::mem::take(args[n]).cast::<T>()` | the _consumed_ value; the original value becomes `()` |
| `this` object | `args[0].write_lock::<T>().unwrap()` | mutable reference to value |
When there is a mutable reference to the `this` object (i.e. the first argument),

View File

@ -105,6 +105,17 @@ let x: MyStruct = from_dynamic(&result)?;
```
Cannot Deserialize Shared Values
-------------------------------
A [`Dynamic`] containing a _shared_ value cannot be deserialized - i.e. it will give a type error.
Use `Dynamic::flatten` to obtain a cloned copy before deserialization
(if the value is not shared, it is simply returned and not cloned).
Shared values are turned off via the [`no_closure`] feature.
Lighter Alternative
-------------------

View File

@ -11,12 +11,16 @@ Furthermore, an [`Engine`] created non-`mut` cannot mutate any state, including
It is highly recommended that [`Engine`]'s be created immutable as much as possible.
```rust
// Use the fluent API to configure an 'Engine' and then keep an immutable instance.
let engine = Engine::new()
.register_get("field", get_field)
let mut engine = Engine::new();
// Use the fluent API to configure an 'Engine'
engine.register_get("field", get_field)
.register_set("field", set_field)
.register_fn("do_work", action);
// Then turn it into an immutable instance
let engine = engine;
// 'engine' is immutable...
```

View File

@ -159,6 +159,8 @@ pub enum Union {
#[cfg(not(feature = "no_object"))]
Map(Box<Map>),
FnPtr(Box<FnPtr>),
#[cfg(not(feature = "no_std"))]
TimeStamp(Box<Instant>),
Variant(Box<Box<dyn Variant>>),
@ -313,6 +315,8 @@ impl Dynamic {
#[cfg(not(feature = "no_object"))]
Union::Map(_) => TypeId::of::<Map>(),
Union::FnPtr(_) => TypeId::of::<FnPtr>(),
#[cfg(not(feature = "no_std"))]
Union::TimeStamp(_) => TypeId::of::<Instant>(),
Union::Variant(value) => (***value).type_id(),
@ -345,9 +349,9 @@ impl Dynamic {
#[cfg(not(feature = "no_object"))]
Union::Map(_) => "map",
Union::FnPtr(_) => "Fn",
#[cfg(not(feature = "no_std"))]
Union::Variant(value) if value.is::<Instant>() => "timestamp",
Union::TimeStamp(_) => "timestamp",
Union::Variant(value) => (***value).type_name(),
#[cfg(not(feature = "no_closure"))]
@ -375,10 +379,6 @@ pub(crate) fn map_std_type_name(name: &str) -> &str {
} else if name == type_name::<FnPtr>() {
"Fn"
} else {
#[cfg(not(feature = "no_std"))]
if name == type_name::<Instant>() {
return "timestamp";
}
#[cfg(not(feature = "no_index"))]
if name == type_name::<Array>() {
return "array";
@ -387,6 +387,10 @@ pub(crate) fn map_std_type_name(name: &str) -> &str {
if name == type_name::<Map>() {
return "map";
}
#[cfg(not(feature = "no_std"))]
if name == type_name::<Instant>() {
return "timestamp";
}
name
}
@ -410,9 +414,9 @@ impl fmt::Display for Dynamic {
fmt::Debug::fmt(value, f)
}
Union::FnPtr(value) => fmt::Display::fmt(value, f),
#[cfg(not(feature = "no_std"))]
Union::Variant(value) if value.is::<Instant>() => f.write_str("<timestamp>"),
Union::TimeStamp(_) => f.write_str("<timestamp>"),
Union::Variant(value) => f.write_str((*value).type_name()),
#[cfg(not(feature = "no_closure"))]
@ -449,9 +453,9 @@ impl fmt::Debug for Dynamic {
fmt::Debug::fmt(value, f)
}
Union::FnPtr(value) => fmt::Debug::fmt(value, f),
#[cfg(not(feature = "no_std"))]
Union::Variant(value) if value.is::<Instant>() => write!(f, "<timestamp>"),
Union::TimeStamp(_) => write!(f, "<timestamp>"),
Union::Variant(value) => write!(f, "{}", (*value).type_name()),
#[cfg(not(feature = "no_closure"))]
@ -485,6 +489,8 @@ impl Clone for Dynamic {
#[cfg(not(feature = "no_object"))]
Union::Map(ref value) => Self(Union::Map(value.clone())),
Union::FnPtr(ref value) => Self(Union::FnPtr(value.clone())),
#[cfg(not(feature = "no_std"))]
Union::TimeStamp(ref value) => Self(Union::TimeStamp(value.clone())),
Union::Variant(ref value) => (***value).clone_into_dynamic(),
@ -601,6 +607,14 @@ impl Dynamic {
Err(val) => val,
};
#[cfg(not(feature = "no_std"))]
{
boxed = match unsafe_cast_box::<_, Instant>(boxed) {
Ok(timestamp) => return (*timestamp).into(),
Err(val) => val,
}
}
Self(Union::Variant(Box::new(boxed)))
}
@ -629,7 +643,7 @@ impl Dynamic {
};
#[cfg(feature = "no_closure")]
unimplemented!()
panic!("converting into a shared value is not supported under 'no_closure'");
}
/// Convert the `Dynamic` value into specific type.
@ -738,6 +752,14 @@ impl Dynamic {
};
}
#[cfg(not(feature = "no_std"))]
if TypeId::of::<T>() == TypeId::of::<Instant>() {
return match self.0 {
Union::TimeStamp(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v),
_ => None,
};
}
if TypeId::of::<T>() == TypeId::of::<()>() {
return match self.0 {
Union::Unit(value) => unsafe_try_cast(value),
@ -781,7 +803,20 @@ impl Dynamic {
/// ```
#[inline(always)]
pub fn cast<T: Variant + Clone>(self) -> T {
self.try_cast::<T>().unwrap()
let self_type_name = if self.is_shared() {
// Avoid panics/deadlocks with shared values
"<shared>"
} else {
self.type_name()
};
self.try_cast::<T>().unwrap_or_else(|| {
panic!(
"cannot cast {} value and to {}",
self_type_name,
type_name::<T>()
)
})
}
/// Flatten the `Dynamic` and clone it.
@ -980,6 +1015,13 @@ impl Dynamic {
_ => None,
};
}
#[cfg(not(feature = "no_std"))]
if TypeId::of::<T>() == TypeId::of::<Instant>() {
return match &self.0 {
Union::TimeStamp(value) => <dyn Any>::downcast_ref::<T>(value.as_ref()),
_ => None,
};
}
if TypeId::of::<T>() == TypeId::of::<()>() {
return match &self.0 {
Union::Unit(value) => <dyn Any>::downcast_ref::<T>(value),
@ -1055,6 +1097,13 @@ impl Dynamic {
_ => None,
};
}
#[cfg(not(feature = "no_std"))]
if TypeId::of::<T>() == TypeId::of::<Instant>() {
return match &mut self.0 {
Union::TimeStamp(value) => <dyn Any>::downcast_mut::<T>(value.as_mut()),
_ => None,
};
}
if TypeId::of::<T>() == TypeId::of::<()>() {
return match &mut self.0 {
Union::Unit(value) => <dyn Any>::downcast_mut::<T>(value),
@ -1256,3 +1305,10 @@ impl From<Box<FnPtr>> for Dynamic {
Self(Union::FnPtr(value))
}
}
#[cfg(not(feature = "no_std"))]
impl From<Instant> for Dynamic {
#[inline(always)]
fn from(value: Instant) -> Self {
Self(Union::TimeStamp(Box::new(value)))
}
}

View File

@ -769,7 +769,7 @@ impl Engine {
let args = &mut [val, &mut idx_val2, &mut new_val];
self.exec_fn_call(
state, lib, FN_IDX_SET, 0, args, is_ref, true, false, None, None,
state, lib, FN_IDX_SET, 0, args, is_ref, true, false, None, &None,
level,
)
.map_err(|err| match *err {
@ -798,8 +798,9 @@ impl Engine {
// xxx.fn_name(arg_expr_list)
Expr::FnCall(x) if x.1.is_none() => {
let ((name, native, _, pos), _, hash, _, def_val) = x.as_ref();
let def_val = def_val.map(Into::<Dynamic>::into);
self.make_method_call(
state, lib, name, *hash, target, idx_val, *def_val, *native, false,
state, lib, name, *hash, target, idx_val, &def_val, *native, false,
level,
)
.map_err(|err| err.new_position(*pos))
@ -833,7 +834,7 @@ impl Engine {
let mut new_val = new_val;
let mut args = [target.as_mut(), new_val.as_mut().unwrap()];
self.exec_fn_call(
state, lib, setter, 0, &mut args, is_ref, true, false, None, None,
state, lib, setter, 0, &mut args, is_ref, true, false, None, &None,
level,
)
.map(|(v, _)| (v, true))
@ -844,7 +845,7 @@ impl Engine {
let ((_, getter, _), pos) = x.as_ref();
let mut args = [target.as_mut()];
self.exec_fn_call(
state, lib, getter, 0, &mut args, is_ref, true, false, None, None,
state, lib, getter, 0, &mut args, is_ref, true, false, None, &None,
level,
)
.map(|(v, _)| (v, false))
@ -865,9 +866,10 @@ impl Engine {
// {xxx:map}.fn_name(arg_expr_list)[expr] | {xxx:map}.fn_name(arg_expr_list).expr
Expr::FnCall(x) if x.1.is_none() => {
let ((name, native, _, pos), _, hash, _, def_val) = x.as_ref();
let def_val = def_val.map(Into::<Dynamic>::into);
let (val, _) = self
.make_method_call(
state, lib, name, *hash, target, idx_val, *def_val,
state, lib, name, *hash, target, idx_val, &def_val,
*native, false, level,
)
.map_err(|err| err.new_position(*pos))?;
@ -898,7 +900,7 @@ impl Engine {
let (mut val, updated) = self
.exec_fn_call(
state, lib, getter, 0, args, is_ref, true, false, None,
None, level,
&None, level,
)
.map_err(|err| err.new_position(*pos))?;
@ -924,7 +926,7 @@ impl Engine {
arg_values[1] = val;
self.exec_fn_call(
state, lib, setter, 0, arg_values, is_ref, true, false,
None, None, level,
None, &None, level,
)
.or_else(
|err| match *err {
@ -942,9 +944,10 @@ impl Engine {
// xxx.fn_name(arg_expr_list)[expr] | xxx.fn_name(arg_expr_list).expr
Expr::FnCall(x) if x.1.is_none() => {
let ((name, native, _, pos), _, hash, _, def_val) = x.as_ref();
let def_val = def_val.map(Into::<Dynamic>::into);
let (mut val, _) = self
.make_method_call(
state, lib, name, *hash, target, idx_val, *def_val,
state, lib, name, *hash, target, idx_val, &def_val,
*native, false, level,
)
.map_err(|err| err.new_position(*pos))?;
@ -1202,7 +1205,7 @@ impl Engine {
let mut idx = idx;
let args = &mut [val, &mut idx];
self.exec_fn_call(
state, _lib, FN_IDX_GET, 0, args, is_ref, true, false, None, None, _level,
state, _lib, FN_IDX_GET, 0, args, is_ref, true, false, None, &None, _level,
)
.map(|(v, _)| v.into())
.map_err(|err| match *err {
@ -1245,8 +1248,9 @@ impl Engine {
let op = "==";
// Call the `==` operator to compare each value
let def_value = Some(false.into());
for value in rhs_value.iter_mut() {
let def_value = Some(false);
let args = &mut [&mut lhs_value.clone(), value];
// Qualifiers (none) + function name + number of arguments + argument `TypeId`'s.
@ -1254,7 +1258,7 @@ impl Engine {
calc_fn_hash(empty(), op, args.len(), args.iter().map(|a| a.type_id()));
if self
.call_native_fn(state, lib, op, hash, args, false, false, def_value)
.call_native_fn(state, lib, op, hash, args, false, false, &def_value)
.map_err(|err| err.new_position(rhs.position()))?
.0
.as_bool()
@ -1264,7 +1268,7 @@ impl Engine {
}
}
Ok(false.into())
Ok(def_value.unwrap())
}
#[cfg(not(feature = "no_object"))]
Dynamic(Union::Map(rhs_value)) => match lhs_value {
@ -1395,7 +1399,7 @@ impl Engine {
// Run function
let (value, _) = self
.exec_fn_call(
state, lib, op, 0, args, false, false, false, None, None,
state, lib, op, 0, args, false, false, false, None, &None,
level,
)
.map_err(|err| err.new_position(*op_pos))?;
@ -1431,7 +1435,7 @@ impl Engine {
&mut rhs_val,
];
self.exec_fn_call(
state, lib, op, 0, args, false, false, false, None, None, level,
state, lib, op, 0, args, false, false, false, None, &None, level,
)
.map(|(v, _)| v)
.map_err(|err| err.new_position(*op_pos))?
@ -1499,8 +1503,9 @@ impl Engine {
// Normal function call
Expr::FnCall(x) if x.1.is_none() => {
let ((name, native, capture, pos), _, hash, args_expr, def_val) = x.as_ref();
let def_val = def_val.map(Into::<Dynamic>::into);
self.make_function_call(
scope, mods, state, lib, this_ptr, name, args_expr, *def_val, *hash, *native,
scope, mods, state, lib, this_ptr, name, args_expr, &def_val, *hash, *native,
false, *capture, level,
)
.map_err(|err| err.new_position(*pos))

View File

@ -211,7 +211,7 @@ impl Engine {
args: &mut FnCallArgs,
is_ref: bool,
pub_only: bool,
def_val: Option<bool>,
def_val: &Option<Dynamic>,
) -> Result<(Dynamic, bool), Box<EvalAltResult>> {
self.inc_operations(state)?;
@ -221,6 +221,7 @@ impl Engine {
let func = self
.global_module
.get_fn(hash_fn, pub_only)
.or_else(|| lib.get_fn(hash_fn, pub_only))
.or_else(|| self.packages.get_fn(hash_fn, pub_only));
if let Some(func) = func {
@ -280,14 +281,14 @@ impl Engine {
// Return default value (if any)
if let Some(val) = def_val {
return Ok((val.into(), false));
return Ok((val.clone(), false));
}
// Getter function not found?
if let Some(prop) = extract_prop_from_getter(fn_name) {
return EvalAltResult::ErrorDotExpr(
format!(
"Unknown property '{}' for {}, or it is write-only",
"Failed to get property '{}' of '{}' - the property may not exist, or it may be write-only",
prop,
self.map_type_name(args[0].type_name())
),
@ -300,9 +301,10 @@ impl Engine {
if let Some(prop) = extract_prop_from_setter(fn_name) {
return EvalAltResult::ErrorDotExpr(
format!(
"Unknown property '{}' for {}, or it is read-only",
"Failed to set property '{}' of '{}' - the property may not exist, may be read-only, or '{}' is the wrong type",
prop,
self.map_type_name(args[0].type_name())
self.map_type_name(args[0].type_name()),
self.map_type_name(args[1].type_name()),
),
Position::none(),
)
@ -439,9 +441,9 @@ impl Engine {
// First check script-defined functions
lib.contains_fn(hash_script, pub_only)
//|| lib.contains_fn(hash_fn, pub_only)
|| lib.contains_fn(hash_fn, pub_only)
// Then check registered functions
//|| self.global_module.contains_fn(hash_script, pub_only)
|| self.global_module.contains_fn(hash_script, pub_only)
|| self.global_module.contains_fn(hash_fn, pub_only)
// Then check packages
|| self.packages.contains_fn(hash_script, pub_only)
@ -467,7 +469,7 @@ impl Engine {
_is_method: bool,
pub_only: bool,
_capture: Option<Scope>,
def_val: Option<bool>,
def_val: &Option<Dynamic>,
_level: usize,
) -> Result<(Dynamic, bool), Box<EvalAltResult>> {
// Check for data race.
@ -522,17 +524,22 @@ impl Engine {
.into()
}
// Normal script function call
// Script-like function found
#[cfg(not(feature = "no_function"))]
_ if lib.contains_fn(hash_script, pub_only)
_ if self.global_module.contains_fn(hash_script, pub_only)
|| lib.contains_fn(hash_script, pub_only)
|| self.packages.contains_fn(hash_script, pub_only) =>
{
// Get scripted function
let func = lib
// Get function
let func = self
.global_module
.get_fn(hash_script, pub_only)
.or_else(|| lib.get_fn(hash_script, pub_only))
.or_else(|| self.packages.get_fn(hash_script, pub_only))
.unwrap()
.get_fn_def();
.unwrap();
if func.is_script() {
let func = func.get_fn_def();
let scope = &mut Scope::new();
let mods = &mut Imports::new();
@ -574,6 +581,19 @@ impl Engine {
};
Ok((result, false))
} else {
// If it is a native function, redirect it
self.call_native_fn(
state,
lib,
fn_name,
hash_script,
args,
is_ref,
pub_only,
def_val,
)
}
}
// Normal native function call
@ -649,7 +669,7 @@ impl Engine {
hash_script: u64,
target: &mut Target,
idx_val: Dynamic,
def_val: Option<bool>,
def_val: &Option<Dynamic>,
native: bool,
pub_only: bool,
level: usize,
@ -795,7 +815,7 @@ impl Engine {
this_ptr: &mut Option<&mut Dynamic>,
name: &str,
args_expr: &[Expr],
def_val: Option<bool>,
def_val: &Option<Dynamic>,
mut hash_script: u64,
native: bool,
pub_only: bool,

View File

@ -145,7 +145,7 @@ impl FnPtr {
has_this,
true,
None,
None,
&None,
0,
)
.map(|(v, _)| v)

View File

@ -93,8 +93,13 @@ pub use result::EvalAltResult;
pub use scope::Scope;
pub use syntax::{EvalContext, Expression};
pub use token::Position;
#[cfg(feature = "internals")]
pub use utils::calc_fn_hash;
#[cfg(not(feature = "internals"))]
pub(crate) use utils::calc_fn_hash;
pub use rhai_codegen::*;
#[cfg(not(feature = "no_function"))]
@ -118,8 +123,6 @@ pub use parser::FLOAT;
pub use module::ModuleResolver;
/// Module containing all built-in _module resolvers_ available to Rhai.
///
/// Not available under the `no_module` feature.
#[cfg(not(feature = "no_module"))]
pub mod module_resolvers {
pub use crate::module::resolvers::*;

View File

@ -3,7 +3,7 @@
use crate::any::{Dynamic, Variant};
use crate::calc_fn_hash;
use crate::engine::Engine;
use crate::fn_native::{CallableFunction as Func, FnCallArgs, IteratorFn, SendSync};
use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync};
use crate::fn_register::by_value as cast_arg;
use crate::parser::{FnAccess, FnAccess::Public};
use crate::result::EvalAltResult;
@ -38,19 +38,17 @@ use crate::stdlib::{
vec::Vec,
};
#[cfg(not(feature = "no_std"))]
#[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "sync"))]
use crate::stdlib::cell::RefCell;
#[cfg(not(feature = "no_std"))]
#[cfg(not(feature = "no_module"))]
#[cfg(feature = "sync")]
use crate::stdlib::sync::RwLock;
/// Return type of module-level Rust function.
pub type FuncReturn<T> = Result<T, Box<EvalAltResult>>;
pub type FuncInfo = (
String,
FnAccess,
usize,
Option<StaticVec<TypeId>>,
CallableFunction,
);
/// An imported module, which may contain variables, sub-modules,
/// external Rust functions, and script-defined functions.
///
@ -67,18 +65,14 @@ pub struct Module {
all_variables: HashMap<u64, Dynamic, StraightHasherBuilder>,
/// External Rust functions.
functions: HashMap<
u64,
(String, FnAccess, usize, Option<StaticVec<TypeId>>, Func),
StraightHasherBuilder,
>,
functions: HashMap<u64, FuncInfo, StraightHasherBuilder>,
/// Iterator functions, keyed by the type producing the iterator.
type_iterators: HashMap<TypeId, IteratorFn>,
/// Flattened collection of all external Rust functions, native or scripted,
/// including those in sub-modules.
all_functions: HashMap<u64, Func, StraightHasherBuilder>,
all_functions: HashMap<u64, CallableFunction, StraightHasherBuilder>,
/// Is the module indexed?
indexed: bool,
@ -391,7 +385,7 @@ impl Module {
name: impl Into<String>,
access: FnAccess,
arg_types: &[TypeId],
func: Func,
func: CallableFunction,
) -> u64 {
let name = name.into();
@ -491,11 +485,18 @@ impl Module {
let f = move |engine: &Engine, lib: &Module, args: &mut FnCallArgs| {
func(engine, lib, args).map(Dynamic::from)
};
self.set_fn(name, Public, arg_types, Func::from_method(Box::new(f)))
self.set_fn(
name,
Public,
arg_types,
CallableFunction::from_method(Box::new(f)),
)
}
/// Set a raw function but with a signature that is a scripted function, but the implementation is in Rust.
/// Set a raw function but with a signature that is a scripted function (meaning that the types
/// are not determined), but the implementation is in Rust.
#[cfg(not(feature = "no_function"))]
#[cfg(not(feature = "no_module"))]
pub(crate) fn set_raw_fn_as_scripted(
&mut self,
name: impl Into<String>,
@ -513,7 +514,7 @@ impl Module {
FnAccess::Public,
num_args,
None,
Func::from_pure(Box::new(f)),
CallableFunction::from_pure(Box::new(f)),
),
);
self.indexed = false;
@ -540,7 +541,12 @@ impl Module {
) -> u64 {
let f = move |_: &Engine, _: &Module, _: &mut FnCallArgs| func().map(Dynamic::from);
let arg_types = [];
self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f)))
self.set_fn(
name,
Public,
&arg_types,
CallableFunction::from_pure(Box::new(f)),
)
}
/// Set a Rust function taking one parameter into the module, returning a hash key.
@ -565,7 +571,12 @@ impl Module {
func(cast_arg::<A>(&mut args[0])).map(Dynamic::from)
};
let arg_types = [TypeId::of::<A>()];
self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f)))
self.set_fn(
name,
Public,
&arg_types,
CallableFunction::from_pure(Box::new(f)),
)
}
/// Set a Rust function taking one mutable parameter into the module, returning a hash key.
@ -590,7 +601,12 @@ impl Module {
func(&mut args[0].write_lock::<A>().unwrap()).map(Dynamic::from)
};
let arg_types = [TypeId::of::<A>()];
self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f)))
self.set_fn(
name,
Public,
&arg_types,
CallableFunction::from_method(Box::new(f)),
)
}
/// Set a Rust getter function taking one mutable parameter, returning a hash key.
@ -642,7 +658,12 @@ impl Module {
func(a, b).map(Dynamic::from)
};
let arg_types = [TypeId::of::<A>(), TypeId::of::<B>()];
self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f)))
self.set_fn(
name,
Public,
&arg_types,
CallableFunction::from_pure(Box::new(f)),
)
}
/// Set a Rust function taking two parameters (the first one mutable) into the module,
@ -673,7 +694,12 @@ impl Module {
func(a, b).map(Dynamic::from)
};
let arg_types = [TypeId::of::<A>(), TypeId::of::<B>()];
self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f)))
self.set_fn(
name,
Public,
&arg_types,
CallableFunction::from_method(Box::new(f)),
)
}
/// Set a Rust setter function taking two parameters (the first one mutable) into the module,
@ -778,7 +804,12 @@ impl Module {
func(a, b, c).map(Dynamic::from)
};
let arg_types = [TypeId::of::<A>(), TypeId::of::<B>(), TypeId::of::<C>()];
self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f)))
self.set_fn(
name,
Public,
&arg_types,
CallableFunction::from_pure(Box::new(f)),
)
}
/// Set a Rust function taking three parameters (the first one mutable) into the module,
@ -815,7 +846,12 @@ impl Module {
func(a, b, c).map(Dynamic::from)
};
let arg_types = [TypeId::of::<A>(), TypeId::of::<B>(), TypeId::of::<C>()];
self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f)))
self.set_fn(
name,
Public,
&arg_types,
CallableFunction::from_method(Box::new(f)),
)
}
/// Set a Rust index setter taking three parameters (the first one mutable) into the module,
@ -871,7 +907,7 @@ impl Module {
FN_IDX_SET,
Public,
&arg_types,
Func::from_method(Box::new(f)),
CallableFunction::from_method(Box::new(f)),
)
}
@ -955,7 +991,12 @@ impl Module {
TypeId::of::<C>(),
TypeId::of::<D>(),
];
self.set_fn(name, Public, &arg_types, Func::from_pure(Box::new(f)))
self.set_fn(
name,
Public,
&arg_types,
CallableFunction::from_pure(Box::new(f)),
)
}
/// Set a Rust function taking four parameters (the first one mutable) into the module,
@ -999,14 +1040,19 @@ impl Module {
TypeId::of::<C>(),
TypeId::of::<D>(),
];
self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f)))
self.set_fn(
name,
Public,
&arg_types,
CallableFunction::from_method(Box::new(f)),
)
}
/// Get a Rust function.
///
/// The `u64` hash is calculated by the function `crate::calc_fn_hash`.
/// It is also returned by the `set_fn_XXX` calls.
pub(crate) fn get_fn(&self, hash_fn: u64, public_only: bool) -> Option<&Func> {
pub(crate) fn get_fn(&self, hash_fn: u64, public_only: bool) -> Option<&CallableFunction> {
if hash_fn == 0 {
None
} else {
@ -1025,7 +1071,7 @@ impl Module {
///
/// The `u64` hash is calculated by the function `crate::calc_fn_hash` and must match
/// the hash calculated by `index_all_sub_modules`.
pub(crate) fn get_qualified_fn(&self, hash_qualified_fn: u64) -> Option<&Func> {
pub(crate) fn get_qualified_fn(&self, hash_qualified_fn: u64) -> Option<&CallableFunction> {
self.all_functions.get(&hash_qualified_fn)
}
@ -1090,7 +1136,9 @@ impl Module {
.iter()
.filter(|(_, (_, _, _, _, v))| match v {
#[cfg(not(feature = "no_function"))]
Func::Script(ref f) => _filter(f.access, f.name.as_str(), f.params.len()),
CallableFunction::Script(f) => {
_filter(f.access, f.name.as_str(), f.params.len())
}
_ => true,
})
.map(|(&k, v)| (k, v.clone())),
@ -1112,7 +1160,7 @@ impl Module {
mut filter: impl FnMut(FnAccess, &str, usize) -> bool,
) -> &mut Self {
self.functions.retain(|_, (_, _, _, _, v)| match v {
Func::Script(ref f) => filter(f.access, f.name.as_str(), f.params.len()),
CallableFunction::Script(f) => filter(f.access, f.name.as_str(), f.params.len()),
_ => true,
});
@ -1141,9 +1189,7 @@ impl Module {
}
/// Get an iterator to the functions in the module.
pub(crate) fn iter_fn(
&self,
) -> impl Iterator<Item = &(String, FnAccess, usize, Option<StaticVec<TypeId>>, Func)> {
pub(crate) fn iter_fn(&self) -> impl Iterator<Item = &FuncInfo> {
self.functions.values()
}
@ -1162,13 +1208,23 @@ impl Module {
self.functions
.iter()
.for_each(|(_, (_, _, _, _, v))| match v {
Func::Script(ref f) => action(f.access, f.name.as_str(), f.params.len()),
CallableFunction::Script(f) => action(f.access, f.name.as_str(), f.params.len()),
_ => (),
});
}
/// Create a new `Module` by evaluating an `AST`.
///
/// ### `merge_namespaces` parameter
///
/// * If `true`, the entire `AST` is encapsulated into each function, allowing functions
/// to cross-call each other. Functions in the global namespace, plus all functions
/// defined in the module, are _merged_ into a _unified_ namespace before each call.
/// Therefore, all functions will be found, at the expense of some performance.
///
/// * If `false`, each function is registered independently and cannot cross-call
/// each other. Functions are searched in the global namespace.
///
/// # Examples
///
/// ```
@ -1177,14 +1233,19 @@ impl Module {
///
/// let engine = Engine::new();
/// let ast = engine.compile("let answer = 42; export answer;")?;
/// let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
/// let module = Module::eval_ast_as_new(Scope::new(), &ast, true, &engine)?;
/// assert!(module.contains_var("answer"));
/// assert_eq!(module.get_var_value::<i64>("answer").unwrap(), 42);
/// # Ok(())
/// # }
/// ```
#[cfg(not(feature = "no_module"))]
pub fn eval_ast_as_new(mut scope: Scope, ast: &AST, engine: &Engine) -> FuncReturn<Self> {
pub fn eval_ast_as_new(
mut scope: Scope,
ast: &AST,
merge_namespaces: bool,
engine: &Engine,
) -> FuncReturn<Self> {
let mut mods = Imports::new();
// Run the script
@ -1207,7 +1268,44 @@ impl Module {
module.modules.insert(alias.to_string(), m);
});
#[cfg(not(feature = "no_function"))]
if merge_namespaces {
ast.iter_functions(|access, name, num_args| match access {
FnAccess::Private => (),
FnAccess::Public => {
let fn_name = name.to_string();
let ast_lib = ast.lib().clone();
module.set_raw_fn_as_scripted(
name,
num_args,
move |engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| {
let mut lib_merged = lib.clone();
lib_merged.merge(&ast_lib);
engine
.call_fn_dynamic_raw(
&mut Scope::new(),
&lib_merged,
&fn_name,
&mut None,
args,
)
.map_err(|err| {
// Wrap the error in a module-error
Box::new(EvalAltResult::ErrorInModule(
"".to_string(),
err,
Position::none(),
))
})
},
);
}
});
} else {
module.merge(ast.lib());
}
Ok(module)
}
@ -1221,7 +1319,7 @@ impl Module {
module: &'a Module,
qualifiers: &mut Vec<&'a str>,
variables: &mut Vec<(u64, Dynamic)>,
functions: &mut Vec<(u64, Func)>,
functions: &mut Vec<(u64, CallableFunction)>,
) {
for (name, m) in &module.modules {
// Index all the sub-modules first.
@ -1372,573 +1470,10 @@ impl ModuleRef {
}
}
/// Trait that encapsulates a module resolution service.
pub trait ModuleResolver: SendSync {
/// Resolve a module based on a path string.
fn resolve(&self, _: &Engine, path: &str, pos: Position) -> Result<Module, Box<EvalAltResult>>;
}
/// Re-export module resolver trait.
#[cfg(not(feature = "no_module"))]
pub use resolvers::ModuleResolver;
/// Re-export module resolvers.
#[cfg(not(feature = "no_module"))]
pub mod resolvers {
pub use super::collection::ModuleResolversCollection;
#[cfg(not(feature = "no_std"))]
#[cfg(not(target_arch = "wasm32"))]
pub use super::file::{FileModuleResolver, GlobalFileModuleResolver};
pub use super::stat::StaticModuleResolver;
}
#[cfg(feature = "no_module")]
pub mod resolvers {}
/// Script file-based module resolver.
#[cfg(not(feature = "no_module"))]
#[cfg(not(feature = "no_std"))]
#[cfg(not(target_arch = "wasm32"))]
mod file {
use super::*;
use crate::stdlib::path::PathBuf;
/// Module resolution service that loads module script files from the file system.
///
/// All functions in each module are treated as strictly _pure_ and cannot refer to
/// other functions within the same module. Functions are searched in the _global_ namespace.
///
/// For simple utility libraries, this usually performs better than the full `FileModuleResolver`.
///
/// Script files are cached so they are are not reloaded and recompiled in subsequent requests.
///
/// The `new_with_path` and `new_with_path_and_extension` constructor functions
/// allow specification of a base directory with module path used as a relative path offset
/// to the base directory. The script file is then forced to be in a specified extension
/// (default `.rhai`).
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::GlobalFileModuleResolver;
///
/// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.x'.
/// let resolver = GlobalFileModuleResolver::new_with_path_and_extension("./scripts", "x");
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
#[derive(Debug)]
pub struct GlobalFileModuleResolver {
path: PathBuf,
extension: String,
#[cfg(not(feature = "sync"))]
cache: RefCell<HashMap<PathBuf, AST>>,
#[cfg(feature = "sync")]
cache: RwLock<HashMap<PathBuf, AST>>,
}
impl Default for GlobalFileModuleResolver {
fn default() -> Self {
Self::new_with_path(PathBuf::default())
}
}
impl GlobalFileModuleResolver {
/// Create a new `GlobalFileModuleResolver` with a specific base path.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::GlobalFileModuleResolver;
///
/// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.rhai' (the default).
/// let resolver = GlobalFileModuleResolver::new_with_path("./scripts");
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new_with_path<P: Into<PathBuf>>(path: P) -> Self {
Self::new_with_path_and_extension(path, "rhai")
}
/// Create a new `GlobalFileModuleResolver` with a specific base path and file extension.
///
/// The default extension is `.rhai`.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::GlobalFileModuleResolver;
///
/// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.x'.
/// let resolver = GlobalFileModuleResolver::new_with_path_and_extension("./scripts", "x");
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new_with_path_and_extension<P: Into<PathBuf>, E: Into<String>>(
path: P,
extension: E,
) -> Self {
Self {
path: path.into(),
extension: extension.into(),
cache: Default::default(),
}
}
/// Create a new `GlobalFileModuleResolver` with the current directory as base path.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::GlobalFileModuleResolver;
///
/// // Create a new 'GlobalFileModuleResolver' loading scripts from the current directory
/// // with file extension '.rhai' (the default).
/// let resolver = GlobalFileModuleResolver::new();
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new() -> Self {
Default::default()
}
/// Create a `Module` from a file path.
pub fn create_module<P: Into<PathBuf>>(
&self,
engine: &Engine,
path: &str,
) -> Result<Module, Box<EvalAltResult>> {
self.resolve(engine, path, Default::default())
}
}
impl ModuleResolver for GlobalFileModuleResolver {
fn resolve(
&self,
engine: &Engine,
path: &str,
pos: Position,
) -> Result<Module, Box<EvalAltResult>> {
// Construct the script file path
let mut file_path = self.path.clone();
file_path.push(path);
file_path.set_extension(&self.extension); // Force extension
let scope = Default::default();
// See if it is cached
let (module, ast) = {
#[cfg(not(feature = "sync"))]
let c = self.cache.borrow();
#[cfg(feature = "sync")]
let c = self.cache.read().unwrap();
if let Some(ast) = c.get(&file_path) {
(
Module::eval_ast_as_new(scope, ast, engine)
.map_err(|err| err.new_position(pos))?,
None,
)
} else {
// Load the file and compile it if not found
let ast = engine
.compile_file(file_path.clone())
.map_err(|err| err.new_position(pos))?;
(
Module::eval_ast_as_new(scope, &ast, engine)
.map_err(|err| err.new_position(pos))?,
Some(ast),
)
}
};
if let Some(ast) = ast {
// Put it into the cache
#[cfg(not(feature = "sync"))]
self.cache.borrow_mut().insert(file_path, ast);
#[cfg(feature = "sync")]
self.cache.write().unwrap().insert(file_path, ast);
}
Ok(module)
}
}
/// Module resolution service that loads module script files from the file system.
///
/// Script files are cached so they are are not reloaded and recompiled in subsequent requests.
///
/// The `new_with_path` and `new_with_path_and_extension` constructor functions
/// allow specification of a base directory with module path used as a relative path offset
/// to the base directory. The script file is then forced to be in a specified extension
/// (default `.rhai`).
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::FileModuleResolver;
///
/// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.x'.
/// let resolver = FileModuleResolver::new_with_path_and_extension("./scripts", "x");
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
#[derive(Debug)]
pub struct FileModuleResolver {
path: PathBuf,
extension: String,
#[cfg(not(feature = "sync"))]
cache: RefCell<HashMap<PathBuf, AST>>,
#[cfg(feature = "sync")]
cache: RwLock<HashMap<PathBuf, AST>>,
}
impl Default for FileModuleResolver {
fn default() -> Self {
Self::new_with_path(PathBuf::default())
}
}
impl FileModuleResolver {
/// Create a new `FileModuleResolver` with a specific base path.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::FileModuleResolver;
///
/// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.rhai' (the default).
/// let resolver = FileModuleResolver::new_with_path("./scripts");
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new_with_path<P: Into<PathBuf>>(path: P) -> Self {
Self::new_with_path_and_extension(path, "rhai")
}
/// Create a new `FileModuleResolver` with a specific base path and file extension.
///
/// The default extension is `.rhai`.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::FileModuleResolver;
///
/// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.x'.
/// let resolver = FileModuleResolver::new_with_path_and_extension("./scripts", "x");
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new_with_path_and_extension<P: Into<PathBuf>, E: Into<String>>(
path: P,
extension: E,
) -> Self {
Self {
path: path.into(),
extension: extension.into(),
cache: Default::default(),
}
}
/// Create a new `FileModuleResolver` with the current directory as base path.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::FileModuleResolver;
///
/// // Create a new 'FileModuleResolver' loading scripts from the current directory
/// // with file extension '.rhai' (the default).
/// let resolver = FileModuleResolver::new();
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new() -> Self {
Default::default()
}
/// Create a `Module` from a file path.
pub fn create_module<P: Into<PathBuf>>(
&self,
engine: &Engine,
path: &str,
) -> Result<Module, Box<EvalAltResult>> {
self.resolve(engine, path, Default::default())
}
}
impl ModuleResolver for FileModuleResolver {
fn resolve(
&self,
engine: &Engine,
path: &str,
pos: Position,
) -> Result<Module, Box<EvalAltResult>> {
// Construct the script file path
let mut file_path = self.path.clone();
file_path.push(path);
file_path.set_extension(&self.extension); // Force extension
// See if it is cached
let exists = {
#[cfg(not(feature = "sync"))]
let c = self.cache.borrow();
#[cfg(feature = "sync")]
let c = self.cache.read().unwrap();
c.contains_key(&file_path)
};
if !exists {
// Load the file and compile it if not found
let ast = engine
.compile_file(file_path.clone())
.map_err(|err| err.new_position(pos))?;
// Put it into the cache
#[cfg(not(feature = "sync"))]
self.cache.borrow_mut().insert(file_path.clone(), ast);
#[cfg(feature = "sync")]
self.cache.write().unwrap().insert(file_path.clone(), ast);
}
#[cfg(not(feature = "sync"))]
let c = self.cache.borrow();
#[cfg(feature = "sync")]
let c = self.cache.read().unwrap();
let ast = c.get(&file_path).unwrap();
let mut _module = Module::eval_ast_as_new(Scope::new(), ast, engine)?;
#[cfg(not(feature = "no_function"))]
ast.iter_functions(|access, name, num_args| match access {
FnAccess::Private => (),
FnAccess::Public => {
let fn_name = name.to_string();
let ast_lib = ast.lib().clone();
_module.set_raw_fn_as_scripted(
name,
num_args,
move |engine: &Engine, _, args: &mut [&mut Dynamic]| {
engine.call_fn_dynamic_raw(
&mut Scope::new(),
&ast_lib,
&fn_name,
&mut None,
args,
)
},
);
}
});
Ok(_module)
}
}
}
/// Static module resolver.
#[cfg(not(feature = "no_module"))]
mod stat {
use super::*;
/// Module resolution service that serves modules added into it.
///
/// # Examples
///
/// ```
/// use rhai::{Engine, Module};
/// use rhai::module_resolvers::StaticModuleResolver;
///
/// let mut resolver = StaticModuleResolver::new();
///
/// let module = Module::new();
/// resolver.insert("hello".to_string(), module);
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
#[derive(Debug, Clone, Default)]
pub struct StaticModuleResolver(HashMap<String, Module>);
impl StaticModuleResolver {
/// Create a new `StaticModuleResolver`.
///
/// # Examples
///
/// ```
/// use rhai::{Engine, Module};
/// use rhai::module_resolvers::StaticModuleResolver;
///
/// let mut resolver = StaticModuleResolver::new();
///
/// let module = Module::new();
/// resolver.insert("hello", module);
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new() -> Self {
Default::default()
}
}
impl StaticModuleResolver {
/// Add a module keyed by its path.
pub fn insert<S: Into<String>>(&mut self, path: S, mut module: Module) {
module.index_all_sub_modules();
self.0.insert(path.into(), module);
}
/// Remove a module given its path.
pub fn remove(&mut self, path: &str) -> Option<Module> {
self.0.remove(path)
}
/// Does the path exist?
pub fn contains_path(&self, path: &str) -> bool {
self.0.contains_key(path)
}
/// Get an iterator of all the modules.
pub fn iter(&self) -> impl Iterator<Item = (&str, &Module)> {
self.0.iter().map(|(k, v)| (k.as_str(), v))
}
/// Get a mutable iterator of all the modules.
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut Module)> {
self.0.iter_mut().map(|(k, v)| (k.as_str(), v))
}
/// Get an iterator of all the module paths.
pub fn paths(&self) -> impl Iterator<Item = &str> {
self.0.keys().map(String::as_str)
}
/// Get an iterator of all the modules.
pub fn values(&self) -> impl Iterator<Item = &Module> {
self.0.values()
}
/// Get a mutable iterator of all the modules.
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut Module> {
self.0.values_mut()
}
/// Remove all modules.
pub fn clear(&mut self) {
self.0.clear();
}
}
impl ModuleResolver for StaticModuleResolver {
fn resolve(
&self,
_: &Engine,
path: &str,
pos: Position,
) -> Result<Module, Box<EvalAltResult>> {
self.0
.get(path)
.cloned()
.ok_or_else(|| EvalAltResult::ErrorModuleNotFound(path.into(), pos).into())
}
}
}
/// Module resolver collection.
#[cfg(not(feature = "no_module"))]
mod collection {
use super::*;
/// Module resolution service that holds a collection of module resolves,
/// to be searched in sequential order.
///
/// # Examples
///
/// ```
/// use rhai::{Engine, Module};
/// use rhai::module_resolvers::{StaticModuleResolver, ModuleResolversCollection};
///
/// let mut collection = ModuleResolversCollection::new();
///
/// let resolver = StaticModuleResolver::new();
/// collection.push(resolver);
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(collection));
/// ```
#[derive(Default)]
pub struct ModuleResolversCollection(Vec<Box<dyn ModuleResolver>>);
impl ModuleResolversCollection {
/// Create a new `ModuleResolversCollection`.
///
/// # Examples
///
/// ```
/// use rhai::{Engine, Module};
/// use rhai::module_resolvers::{StaticModuleResolver, ModuleResolversCollection};
///
/// let mut collection = ModuleResolversCollection::new();
///
/// let resolver = StaticModuleResolver::new();
/// collection.push(resolver);
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(collection));
/// ```
pub fn new() -> Self {
Default::default()
}
}
impl ModuleResolversCollection {
/// Add a module keyed by its path.
pub fn push(&mut self, resolver: impl ModuleResolver + 'static) {
self.0.push(Box::new(resolver));
}
/// Get an iterator of all the module resolvers.
pub fn iter(&self) -> impl Iterator<Item = &dyn ModuleResolver> {
self.0.iter().map(|v| v.as_ref())
}
/// Remove all module resolvers.
pub fn clear(&mut self) {
self.0.clear();
}
}
impl ModuleResolver for ModuleResolversCollection {
fn resolve(
&self,
engine: &Engine,
path: &str,
pos: Position,
) -> Result<Module, Box<EvalAltResult>> {
for resolver in self.0.iter() {
if let Ok(module) = resolver.resolve(engine, path, pos) {
return Ok(module);
}
}
EvalAltResult::ErrorModuleNotFound(path.into(), pos).into()
}
}
}
pub mod resolvers;

View File

@ -0,0 +1,85 @@
use crate::engine::Engine;
use crate::module::{Module, ModuleResolver};
use crate::result::EvalAltResult;
use crate::token::Position;
use crate::stdlib::{boxed::Box, vec::Vec};
/// Module resolution service that holds a collection of module resolves,
/// to be searched in sequential order.
///
/// # Examples
///
/// ```
/// use rhai::{Engine, Module};
/// use rhai::module_resolvers::{StaticModuleResolver, ModuleResolversCollection};
///
/// let mut collection = ModuleResolversCollection::new();
///
/// let resolver = StaticModuleResolver::new();
/// collection.push(resolver);
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(collection));
/// ```
#[derive(Default)]
pub struct ModuleResolversCollection(Vec<Box<dyn ModuleResolver>>);
impl ModuleResolversCollection {
/// Create a new `ModuleResolversCollection`.
///
/// # Examples
///
/// ```
/// use rhai::{Engine, Module};
/// use rhai::module_resolvers::{StaticModuleResolver, ModuleResolversCollection};
///
/// let mut collection = ModuleResolversCollection::new();
///
/// let resolver = StaticModuleResolver::new();
/// collection.push(resolver);
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(collection));
/// ```
pub fn new() -> Self {
Default::default()
}
}
impl ModuleResolversCollection {
/// Add a module keyed by its path.
pub fn push(&mut self, resolver: impl ModuleResolver + 'static) {
self.0.push(Box::new(resolver));
}
/// Get an iterator of all the module resolvers.
pub fn iter(&self) -> impl Iterator<Item = &dyn ModuleResolver> {
self.0.iter().map(|v| v.as_ref())
}
/// Remove all module resolvers.
pub fn clear(&mut self) {
self.0.clear();
}
}
impl ModuleResolver for ModuleResolversCollection {
fn resolve(
&self,
engine: &Engine,
path: &str,
pos: Position,
) -> Result<Module, Box<EvalAltResult>> {
for resolver in self.0.iter() {
match resolver.resolve(engine, path, pos) {
Ok(module) => return Ok(module),
Err(err) => match *err {
EvalAltResult::ErrorModuleNotFound(_, _) => continue,
EvalAltResult::ErrorInModule(_, err, _) => return Err(err),
_ => panic!("ModuleResolver::resolve returns error that is not ErrorModuleNotFound or ErrorInModule"),
},
}
}
EvalAltResult::ErrorModuleNotFound(path.into(), pos).into()
}
}

View File

@ -0,0 +1,183 @@
use crate::engine::Engine;
use crate::module::{Module, ModuleResolver};
use crate::parser::AST;
use crate::result::EvalAltResult;
use crate::token::Position;
use crate::stdlib::{boxed::Box, collections::HashMap, path::PathBuf, string::String};
#[cfg(not(feature = "sync"))]
use crate::stdlib::cell::RefCell;
#[cfg(feature = "sync")]
use crate::stdlib::sync::RwLock;
/// Module resolution service that loads module script files from the file system.
///
/// Script files are cached so they are are not reloaded and recompiled in subsequent requests.
///
/// The `new_with_path` and `new_with_path_and_extension` constructor functions
/// allow specification of a base directory with module path used as a relative path offset
/// to the base directory. The script file is then forced to be in a specified extension
/// (default `.rhai`).
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::FileModuleResolver;
///
/// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.x'.
/// let resolver = FileModuleResolver::new_with_path_and_extension("./scripts", "x");
///
/// let mut engine = Engine::new();
///
/// engine.set_module_resolver(Some(resolver));
/// ```
#[derive(Debug)]
pub struct FileModuleResolver {
path: PathBuf,
extension: String,
#[cfg(not(feature = "sync"))]
cache: RefCell<HashMap<PathBuf, AST>>,
#[cfg(feature = "sync")]
cache: RwLock<HashMap<PathBuf, AST>>,
}
impl Default for FileModuleResolver {
fn default() -> Self {
Self::new_with_path(PathBuf::default())
}
}
impl FileModuleResolver {
/// Create a new `FileModuleResolver` with a specific base path.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::FileModuleResolver;
///
/// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.rhai' (the default).
/// let resolver = FileModuleResolver::new_with_path("./scripts");
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new_with_path<P: Into<PathBuf>>(path: P) -> Self {
Self::new_with_path_and_extension(path, "rhai")
}
/// Create a new `FileModuleResolver` with a specific base path and file extension.
///
/// The default extension is `.rhai`.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::FileModuleResolver;
///
/// // Create a new 'FileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.x'.
/// let resolver = FileModuleResolver::new_with_path_and_extension("./scripts", "x");
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new_with_path_and_extension<P: Into<PathBuf>, E: Into<String>>(
path: P,
extension: E,
) -> Self {
Self {
path: path.into(),
extension: extension.into(),
cache: Default::default(),
}
}
/// Create a new `FileModuleResolver` with the current directory as base path.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::FileModuleResolver;
///
/// // Create a new 'FileModuleResolver' loading scripts from the current directory
/// // with file extension '.rhai' (the default).
/// let resolver = FileModuleResolver::new();
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new() -> Self {
Default::default()
}
/// Create a `Module` from a file path.
pub fn create_module<P: Into<PathBuf>>(
&self,
engine: &Engine,
path: &str,
) -> Result<Module, Box<EvalAltResult>> {
self.resolve(engine, path, Default::default())
}
}
impl ModuleResolver for FileModuleResolver {
fn resolve(
&self,
engine: &Engine,
path: &str,
pos: Position,
) -> Result<Module, Box<EvalAltResult>> {
// Construct the script file path
let mut file_path = self.path.clone();
file_path.push(path);
file_path.set_extension(&self.extension); // Force extension
let scope = Default::default();
let module;
// See if it is cached
let ast = {
#[cfg(not(feature = "sync"))]
let c = self.cache.borrow();
#[cfg(feature = "sync")]
let c = self.cache.read().unwrap();
if let Some(ast) = c.get(&file_path) {
module = Module::eval_ast_as_new(scope, ast, true, engine).map_err(|err| {
Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos))
})?;
None
} else {
// Load the file and compile it if not found
let ast = engine.compile_file(file_path.clone()).map_err(|err| {
Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos))
})?;
module = Module::eval_ast_as_new(scope, &ast, true, engine).map_err(|err| {
Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos))
})?;
Some(ast)
}
};
if let Some(ast) = ast {
// Put it into the cache
#[cfg(not(feature = "sync"))]
self.cache.borrow_mut().insert(file_path, ast);
#[cfg(feature = "sync")]
self.cache.write().unwrap().insert(file_path, ast);
}
Ok(module)
}
}

View File

@ -0,0 +1,188 @@
use crate::engine::Engine;
use crate::module::{Module, ModuleResolver};
use crate::parser::AST;
use crate::result::EvalAltResult;
use crate::token::Position;
use crate::stdlib::{boxed::Box, collections::HashMap, path::PathBuf, string::String};
#[cfg(not(feature = "sync"))]
use crate::stdlib::cell::RefCell;
#[cfg(feature = "sync")]
use crate::stdlib::sync::RwLock;
/// Module resolution service that loads module script files from the file system.
///
/// All functions in each module are treated as strictly independent and cannot refer to
/// other functions within the same module. Functions are searched in the _global_ namespace.
///
/// For simple utility libraries, this usually performs better than the full `FileModuleResolver`.
///
/// Script files are cached so they are are not reloaded and recompiled in subsequent requests.
///
/// The `new_with_path` and `new_with_path_and_extension` constructor functions
/// allow specification of a base directory with module path used as a relative path offset
/// to the base directory. The script file is then forced to be in a specified extension
/// (default `.rhai`).
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::GlobalFileModuleResolver;
///
/// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.x'.
/// let resolver = GlobalFileModuleResolver::new_with_path_and_extension("./scripts", "x");
///
/// let mut engine = Engine::new();
///
/// engine.set_module_resolver(Some(resolver));
/// ```
#[derive(Debug)]
pub struct GlobalFileModuleResolver {
path: PathBuf,
extension: String,
#[cfg(not(feature = "sync"))]
cache: RefCell<HashMap<PathBuf, AST>>,
#[cfg(feature = "sync")]
cache: RwLock<HashMap<PathBuf, AST>>,
}
impl Default for GlobalFileModuleResolver {
fn default() -> Self {
Self::new_with_path(PathBuf::default())
}
}
impl GlobalFileModuleResolver {
/// Create a new `GlobalFileModuleResolver` with a specific base path.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::GlobalFileModuleResolver;
///
/// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.rhai' (the default).
/// let resolver = GlobalFileModuleResolver::new_with_path("./scripts");
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new_with_path<P: Into<PathBuf>>(path: P) -> Self {
Self::new_with_path_and_extension(path, "rhai")
}
/// Create a new `GlobalFileModuleResolver` with a specific base path and file extension.
///
/// The default extension is `.rhai`.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::GlobalFileModuleResolver;
///
/// // Create a new 'GlobalFileModuleResolver' loading scripts from the 'scripts' subdirectory
/// // with file extension '.x'.
/// let resolver = GlobalFileModuleResolver::new_with_path_and_extension("./scripts", "x");
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new_with_path_and_extension<P: Into<PathBuf>, E: Into<String>>(
path: P,
extension: E,
) -> Self {
Self {
path: path.into(),
extension: extension.into(),
cache: Default::default(),
}
}
/// Create a new `GlobalFileModuleResolver` with the current directory as base path.
///
/// # Examples
///
/// ```
/// use rhai::Engine;
/// use rhai::module_resolvers::GlobalFileModuleResolver;
///
/// // Create a new 'GlobalFileModuleResolver' loading scripts from the current directory
/// // with file extension '.rhai' (the default).
/// let resolver = GlobalFileModuleResolver::new();
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new() -> Self {
Default::default()
}
/// Create a `Module` from a file path.
pub fn create_module<P: Into<PathBuf>>(
&self,
engine: &Engine,
path: &str,
) -> Result<Module, Box<EvalAltResult>> {
self.resolve(engine, path, Default::default())
}
}
impl ModuleResolver for GlobalFileModuleResolver {
fn resolve(
&self,
engine: &Engine,
path: &str,
pos: Position,
) -> Result<Module, Box<EvalAltResult>> {
// Construct the script file path
let mut file_path = self.path.clone();
file_path.push(path);
file_path.set_extension(&self.extension); // Force extension
let scope = Default::default();
let module;
// See if it is cached
let ast = {
#[cfg(not(feature = "sync"))]
let c = self.cache.borrow();
#[cfg(feature = "sync")]
let c = self.cache.read().unwrap();
if let Some(ast) = c.get(&file_path) {
module = Module::eval_ast_as_new(scope, ast, false, engine).map_err(|err| {
Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos))
})?;
None
} else {
// Load the file and compile it if not found
let ast = engine.compile_file(file_path.clone()).map_err(|err| {
Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos))
})?;
module = Module::eval_ast_as_new(scope, &ast, false, engine).map_err(|err| {
Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos))
})?;
Some(ast)
}
};
if let Some(ast) = ast {
// Put it into the cache
#[cfg(not(feature = "sync"))]
self.cache.borrow_mut().insert(file_path, ast);
#[cfg(feature = "sync")]
self.cache.write().unwrap().insert(file_path, ast);
}
Ok(module)
}
}

View File

@ -0,0 +1,40 @@
use crate::engine::Engine;
use crate::fn_native::SendSync;
use crate::module::Module;
use crate::result::EvalAltResult;
use crate::token::Position;
use crate::stdlib::boxed::Box;
mod collection;
pub use collection::ModuleResolversCollection;
#[cfg(not(feature = "no_std"))]
#[cfg(not(target_arch = "wasm32"))]
mod file;
#[cfg(not(feature = "no_std"))]
#[cfg(not(target_arch = "wasm32"))]
pub use file::FileModuleResolver;
#[cfg(not(feature = "no_std"))]
#[cfg(not(target_arch = "wasm32"))]
mod global_file;
#[cfg(not(feature = "no_std"))]
#[cfg(not(target_arch = "wasm32"))]
pub use global_file::GlobalFileModuleResolver;
mod stat;
pub use stat::StaticModuleResolver;
/// Trait that encapsulates a module resolution service.
pub trait ModuleResolver: SendSync {
/// Resolve a module based on a path string.
fn resolve(
&self,
engine: &Engine,
path: &str,
pos: Position,
) -> Result<Module, Box<EvalAltResult>>;
}

View File

@ -0,0 +1,96 @@
use crate::engine::Engine;
use crate::module::{Module, ModuleResolver};
use crate::result::EvalAltResult;
use crate::token::Position;
use crate::stdlib::{boxed::Box, collections::HashMap, string::String};
/// Module resolution service that serves modules added into it.
///
/// # Examples
///
/// ```
/// use rhai::{Engine, Module};
/// use rhai::module_resolvers::StaticModuleResolver;
///
/// let mut resolver = StaticModuleResolver::new();
///
/// let module = Module::new();
/// resolver.insert("hello".to_string(), module);
///
/// let mut engine = Engine::new();
///
/// engine.set_module_resolver(Some(resolver));
/// ```
#[derive(Debug, Clone, Default)]
pub struct StaticModuleResolver(HashMap<String, Module>);
impl StaticModuleResolver {
/// Create a new `StaticModuleResolver`.
///
/// # Examples
///
/// ```
/// use rhai::{Engine, Module};
/// use rhai::module_resolvers::StaticModuleResolver;
///
/// let mut resolver = StaticModuleResolver::new();
///
/// let module = Module::new();
/// resolver.insert("hello", module);
///
/// let mut engine = Engine::new();
/// engine.set_module_resolver(Some(resolver));
/// ```
pub fn new() -> Self {
Default::default()
}
}
impl StaticModuleResolver {
/// Add a module keyed by its path.
pub fn insert<S: Into<String>>(&mut self, path: S, module: Module) {
self.0.insert(path.into(), module);
}
/// Remove a module given its path.
pub fn remove(&mut self, path: &str) -> Option<Module> {
self.0.remove(path)
}
/// Does the path exist?
pub fn contains_path(&self, path: &str) -> bool {
self.0.contains_key(path)
}
/// Get an iterator of all the modules.
pub fn iter(&self) -> impl Iterator<Item = (&str, &Module)> {
self.0.iter().map(|(k, v)| (k.as_str(), v))
}
/// Get a mutable iterator of all the modules.
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&str, &mut Module)> {
self.0.iter_mut().map(|(k, v)| (k.as_str(), v))
}
/// Get an iterator of all the module paths.
pub fn paths(&self) -> impl Iterator<Item = &str> {
self.0.keys().map(String::as_str)
}
/// Get an iterator of all the modules.
pub fn values(&self) -> impl Iterator<Item = &Module> {
self.0.values()
}
/// Get a mutable iterator of all the modules.
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut Module> {
self.0.values_mut()
}
/// Remove all modules.
pub fn clear(&mut self) {
self.0.clear();
}
}
impl ModuleResolver for StaticModuleResolver {
fn resolve(&self, _: &Engine, path: &str, pos: Position) -> Result<Module, Box<EvalAltResult>> {
self.0
.get(path)
.cloned()
.ok_or_else(|| EvalAltResult::ErrorModuleNotFound(path.into(), pos).into())
}
}

View File

@ -142,7 +142,7 @@ fn call_fn_with_constant_arguments(
arg_values.iter_mut().collect::<StaticVec<_>>().as_mut(),
false,
true,
None,
&None,
)
.ok()
.map(|(v, _)| v)

View File

@ -187,9 +187,8 @@ mod string_functions {
pub fn contains_char(s: &str, ch: char) -> bool {
s.contains(ch)
}
#[rhai_fn(name = "contains")]
#[inline(always)]
pub fn contains_string(s: &str, find: ImmutableString) -> bool {
pub fn contains(s: &str, find: ImmutableString) -> bool {
s.contains(find.as_str())
}
@ -230,13 +229,12 @@ mod string_functions {
.unwrap_or(-1 as INT)
}
#[rhai_fn(name = "index_of")]
pub fn index_of_string(s: &str, find: ImmutableString) -> INT {
pub fn index_of(s: &str, find: ImmutableString) -> INT {
s.find(find.as_str())
.map(|index| s[0..index].chars().count() as INT)
.unwrap_or(-1 as INT)
}
#[rhai_fn(name = "sub_string")]
pub fn sub_string(s: &str, start: INT, len: INT) -> ImmutableString {
let offset = if s.is_empty() || len <= 0 {
return "".to_string().into();
@ -272,7 +270,7 @@ mod string_functions {
}
#[rhai_fn(name = "crop")]
pub fn crop_string(s: &mut ImmutableString, start: INT, len: INT) {
pub fn crop(s: &mut ImmutableString, start: INT, len: INT) {
let offset = if s.is_empty() || len <= 0 {
s.make_mut().clear();
return;
@ -300,12 +298,12 @@ mod string_functions {
#[rhai_fn(name = "crop")]
#[inline(always)]
pub fn crop_string_starting_from(s: &mut ImmutableString, start: INT) {
crop_string(s, start, s.len() as INT);
crop(s, start, s.len() as INT);
}
#[rhai_fn(name = "replace")]
#[inline(always)]
pub fn replace_string(s: &mut ImmutableString, find: ImmutableString, sub: ImmutableString) {
pub fn replace(s: &mut ImmutableString, find: ImmutableString, sub: ImmutableString) {
*s = s.replace(find.as_str(), sub.as_str()).into();
}
#[rhai_fn(name = "replace")]
@ -338,6 +336,18 @@ mod string_functions {
pub fn prepend(x: &mut Array, y: &str) -> String {
format!("{:?}{}", x, y)
}
#[inline(always)]
pub fn split(s: &str, delimiter: ImmutableString) -> Array {
s.split(delimiter.as_str())
.map(Into::<Dynamic>::into)
.collect()
}
#[rhai_fn(name = "split")]
#[inline(always)]
pub fn split_char(s: &str, delimiter: char) -> Array {
s.split(delimiter).map(Into::<Dynamic>::into).collect()
}
}
#[cfg(not(feature = "no_object"))]

View File

@ -1,23 +1,23 @@
#![cfg(not(feature = "no_std"))]
#[cfg(feature = "no_float")]
use super::{arithmetic::make_err, math_basic::MAX_INT};
use super::{arithmetic::make_err as make_arithmetic_err, math_basic::MAX_INT};
use crate::any::Dynamic;
use crate::def_package;
use crate::parser::INT;
use crate::plugin::*;
use crate::result::EvalAltResult;
#[cfg(not(feature = "no_float"))]
use crate::parser::FLOAT;
#[cfg(feature = "no_float")]
use crate::parser::INT;
use crate::stdlib::boxed::Box;
#[cfg(not(target_arch = "wasm32"))]
use crate::stdlib::time::Instant;
use crate::stdlib::time::{Duration, Instant};
#[cfg(target_arch = "wasm32")]
use instant::Instant;
use instant::{Duration, Instant};
def_package!(crate:BasicTimePackage:"Basic timing utilities.", lib, {
// Register date/time functions
@ -43,7 +43,7 @@ mod time_functions {
let seconds = timestamp.elapsed().as_secs();
if cfg!(not(feature = "unchecked")) && seconds > (MAX_INT as u64) {
Err(make_err(format!(
Err(make_arithmetic_err(format!(
"Integer overflow for timestamp.elapsed: {}",
seconds
)))
@ -70,18 +70,18 @@ mod time_functions {
let seconds = (ts2 - ts1).as_secs();
if cfg!(not(feature = "unchecked")) && seconds > (MAX_INT as u64) {
Err(make_err(format!(
Err(make_arithmetic_err(format!(
"Integer overflow for timestamp duration: -{}",
seconds
)))
} else {
Ok(Dynamic::from(-(seconds as INT)))
Ok((-(seconds as INT)).into())
}
} else {
let seconds = (ts1 - ts2).as_secs();
if cfg!(not(feature = "unchecked")) && seconds > (MAX_INT as u64) {
Err(make_err(format!(
Err(make_arithmetic_err(format!(
"Integer overflow for timestamp duration: {}",
seconds
)))
@ -91,6 +91,126 @@ mod time_functions {
}
}
#[cfg(not(feature = "no_float"))]
pub mod float_functions {
fn add_impl(x: Instant, seconds: FLOAT) -> Result<Instant, Box<EvalAltResult>> {
if seconds < 0.0 {
subtract_impl(x, -seconds)
} else if cfg!(not(feature = "unchecked")) {
if seconds > (MAX_INT as FLOAT) {
Err(make_arithmetic_err(format!(
"Integer overflow for timestamp add: {}",
seconds
)))
} else {
x.checked_add(Duration::from_millis((seconds * 1000.0) as u64))
.ok_or_else(|| {
make_arithmetic_err(format!(
"Timestamp overflow when adding {} second(s)",
seconds
))
})
}
} else {
Ok(x + Duration::from_millis((seconds * 1000.0) as u64))
}
}
fn subtract_impl(x: Instant, seconds: FLOAT) -> Result<Instant, Box<EvalAltResult>> {
if seconds < 0.0 {
add_impl(x, -seconds)
} else if cfg!(not(feature = "unchecked")) {
if seconds > (MAX_INT as FLOAT) {
Err(make_arithmetic_err(format!(
"Integer overflow for timestamp add: {}",
seconds
)))
} else {
x.checked_sub(Duration::from_millis((seconds * 1000.0) as u64))
.ok_or_else(|| {
make_arithmetic_err(format!(
"Timestamp overflow when adding {} second(s)",
seconds
))
})
}
} else {
Ok(x - Duration::from_millis((seconds * 1000.0) as u64))
}
}
#[rhai_fn(return_raw, name = "+")]
pub fn add(x: Instant, seconds: FLOAT) -> Result<Dynamic, Box<EvalAltResult>> {
add_impl(x, seconds).map(Into::<Dynamic>::into)
}
#[rhai_fn(return_raw, name = "+=")]
pub fn add_assign(x: &mut Instant, seconds: FLOAT) -> Result<Dynamic, Box<EvalAltResult>> {
*x = add_impl(*x, seconds)?;
Ok(().into())
}
#[rhai_fn(return_raw, name = "-")]
pub fn subtract(x: Instant, seconds: FLOAT) -> Result<Dynamic, Box<EvalAltResult>> {
subtract_impl(x, seconds).map(Into::<Dynamic>::into)
}
#[rhai_fn(return_raw, name = "-=")]
pub fn subtract_assign(
x: &mut Instant,
seconds: FLOAT,
) -> Result<Dynamic, Box<EvalAltResult>> {
*x = subtract_impl(*x, seconds)?;
Ok(().into())
}
}
fn add_impl(x: Instant, seconds: INT) -> Result<Instant, Box<EvalAltResult>> {
if seconds < 0 {
subtract_impl(x, -seconds)
} else if cfg!(not(feature = "unchecked")) {
x.checked_add(Duration::from_secs(seconds as u64))
.ok_or_else(|| {
make_arithmetic_err(format!(
"Timestamp overflow when adding {} second(s)",
seconds
))
})
} else {
Ok(x + Duration::from_secs(seconds as u64))
}
}
fn subtract_impl(x: Instant, seconds: INT) -> Result<Instant, Box<EvalAltResult>> {
if seconds < 0 {
add_impl(x, -seconds)
} else if cfg!(not(feature = "unchecked")) {
x.checked_sub(Duration::from_secs(seconds as u64))
.ok_or_else(|| {
make_arithmetic_err(format!(
"Timestamp overflow when adding {} second(s)",
seconds
))
})
} else {
Ok(x - Duration::from_secs(seconds as u64))
}
}
#[rhai_fn(return_raw, name = "+")]
pub fn add(x: Instant, seconds: INT) -> Result<Dynamic, Box<EvalAltResult>> {
add_impl(x, seconds).map(Into::<Dynamic>::into)
}
#[rhai_fn(return_raw, name = "+=")]
pub fn add_assign(x: &mut Instant, seconds: INT) -> Result<Dynamic, Box<EvalAltResult>> {
*x = add_impl(*x, seconds)?;
Ok(().into())
}
#[rhai_fn(return_raw, name = "-")]
pub fn subtract(x: Instant, seconds: INT) -> Result<Dynamic, Box<EvalAltResult>> {
subtract_impl(x, seconds).map(Into::<Dynamic>::into)
}
#[rhai_fn(return_raw, name = "-=")]
pub fn subtract_assign(x: &mut Instant, seconds: INT) -> Result<Dynamic, Box<EvalAltResult>> {
*x = subtract_impl(*x, seconds)?;
Ok(().into())
}
#[rhai_fn(name = "==")]
#[inline(always)]
pub fn eq(x: Instant, y: Instant) -> bool {

View File

@ -790,7 +790,7 @@ pub enum Expr {
Option<Box<ModuleRef>>,
u64,
StaticVec<Expr>,
Option<bool>,
Option<bool>, // Default value is `bool` in order for `Expr` to be `Hash`.
)>,
),
/// expr op= expr

View File

@ -39,6 +39,9 @@ pub enum EvalAltResult {
/// An error has occurred inside a called function.
/// Wrapped values are the name of the function and the interior error.
ErrorInFunctionCall(String, Box<EvalAltResult>, Position),
/// An error has occurred while loading a module.
/// Wrapped value are the name of the module and the interior error.
ErrorInModule(String, Box<EvalAltResult>, Position),
/// Access to `this` that is not bound.
ErrorUnboundThis(Position),
/// Non-boolean operand encountered for boolean operator. Wrapped value is the operator.
@ -113,6 +116,7 @@ impl EvalAltResult {
Self::ErrorParsing(p, _) => p.desc(),
Self::ErrorInFunctionCall(_, _, _) => "Error in called function",
Self::ErrorInModule(_, _, _) => "Error in module",
Self::ErrorFunctionNotFound(_, _) => "Function not found",
Self::ErrorUnboundThis(_) => "'this' is not bound",
Self::ErrorBooleanArgMismatch(_, _) => "Boolean operator expects boolean operands",
@ -180,6 +184,10 @@ impl fmt::Display for EvalAltResult {
Self::ErrorInFunctionCall(s, err, _) => {
write!(f, "Error in call to function '{}': {}", s, err)?
}
Self::ErrorInModule(s, err, _) if s.is_empty() => {
write!(f, "Error in module: {}", err)?
}
Self::ErrorInModule(s, err, _) => write!(f, "Error in module '{}': {}", s, err)?,
Self::ErrorFunctionNotFound(s, _)
| Self::ErrorVariableNotFound(s, _)
@ -280,6 +288,7 @@ impl EvalAltResult {
Self::ErrorParsing(_, pos)
| Self::ErrorFunctionNotFound(_, pos)
| Self::ErrorInFunctionCall(_, _, pos)
| Self::ErrorInModule(_, _, pos)
| Self::ErrorUnboundThis(pos)
| Self::ErrorBooleanArgMismatch(_, pos)
| Self::ErrorCharMismatch(pos)
@ -321,6 +330,7 @@ impl EvalAltResult {
Self::ErrorParsing(_, pos)
| Self::ErrorFunctionNotFound(_, pos)
| Self::ErrorInFunctionCall(_, _, pos)
| Self::ErrorInModule(_, _, pos)
| Self::ErrorUnboundThis(pos)
| Self::ErrorBooleanArgMismatch(_, pos)
| Self::ErrorCharMismatch(pos)

View File

@ -160,9 +160,8 @@ impl<'de> Deserializer<'de> for &mut DynamicDeserializer<'de> {
#[cfg(not(feature = "no_object"))]
Union::Map(_) => self.deserialize_map(visitor),
Union::FnPtr(_) => self.type_error(),
#[cfg(not(feature = "no_std"))]
Union::Variant(value) if value.is::<Instant>() => self.type_error(),
Union::TimeStamp(_) => self.type_error(),
Union::Variant(value) if value.is::<i8>() => self.deserialize_i8(visitor),
Union::Variant(value) if value.is::<i16>() => self.deserialize_i16(visitor),

View File

@ -262,7 +262,7 @@ fn test_module_from_ast() -> Result<(), Box<EvalAltResult>> {
engine.set_module_resolver(Some(resolver1));
let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
let module = Module::eval_ast_as_new(Scope::new(), &ast, true, &engine)?;
let mut resolver2 = StaticModuleResolver::new();
resolver2.insert("testing", module);

View File

@ -34,12 +34,15 @@ fn test_packages() -> Result<(), Box<EvalAltResult>> {
#[test]
fn test_packages_with_script() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
let ast = engine.compile("fn foo(x) { x + 1 }")?;
let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
let ast = engine.compile("fn foo(x) { x + 1 } fn bar(x) { foo(x) + 1 }")?;
let module = Module::eval_ast_as_new(Scope::new(), &ast, false, &engine)?;
engine.load_package(module);
assert_eq!(engine.eval::<INT>("foo(41)")?, 42);
let module = Module::eval_ast_as_new(Scope::new(), &ast, true, &engine)?;
engine.load_package(module);
assert_eq!(engine.eval::<INT>("bar(40)")?, 42);
Ok(())
}

View File

@ -186,3 +186,25 @@ fn test_string_fn() -> Result<(), Box<EvalAltResult>> {
Ok(())
}
#[cfg(not(feature = "no_object"))]
#[cfg(not(feature = "no_index"))]
#[test]
fn test_string_split() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
assert_eq!(
engine.eval::<INT>(
r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.split(' ').len"#
)?,
3
);
assert_eq!(
engine.eval::<INT>(
r#"let x = "\u2764\u2764\u2764 hello! \u2764\u2764\u2764"; x.split("hello").len"#
)?,
2
);
Ok(())
}

View File

@ -50,3 +50,60 @@ fn test_timestamp() -> Result<(), Box<EvalAltResult>> {
Ok(())
}
#[test]
fn test_timestamp_op() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
#[cfg(not(feature = "no_float"))]
assert!(
(engine.eval::<FLOAT>(
r#"
let time1 = timestamp();
let time2 = time1 + 123.45;
time2 - time1
"#
)? - 123.45)
.abs()
< 0.001
);
#[cfg(not(feature = "no_float"))]
assert!(
(engine.eval::<FLOAT>(
r#"
let time1 = timestamp();
let time2 = time1 - 123.45;
time1 - time2
"#
)? - 123.45)
.abs()
< 0.001
);
#[cfg(feature = "no_float")]
assert_eq!(
engine.eval::<INT>(
r#"
let time1 = timestamp();
let time2 = time1 + 42;
time2 - time1
"#
)?,
42
);
#[cfg(feature = "no_float")]
assert_eq!(
engine.eval::<INT>(
r#"
let time1 = timestamp();
let time2 = time1 - 42;
time1 - time2
"#
)?,
42
);
Ok(())
}