Merge pull request #311 from schungx/master

A bunch of new features and bug fixes.
This commit is contained in:
Stephen Chung 2020-12-21 22:18:11 +08:00 committed by GitHub
commit b1e8f52135
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1122 additions and 302 deletions

View File

@ -41,7 +41,8 @@ no_function = [ "no_closure" ] # no script-defined functions (meaning no closur
no_closure = [] # no automatic sharing and capture of anonymous functions to external variables
no_module = [] # no modules
internals = [] # expose internal data structures
unicode-xid-ident = ["unicode-xid"] # allow Unicode Standard Annex #31 for identifiers.
unicode-xid-ident = ["unicode-xid"] # allow Unicode Standard Annex #31 for identifiers.
metadata = [ "serde", "serde_json"] # enables exporting functions metadata to JSON
# compiling for no-std
no_std = [ "smallvec/union", "num-traits/libm", "hashbrown", "core-error", "libm", "ahash" ]
@ -86,6 +87,12 @@ default_features = false
features = ["derive", "alloc"]
optional = true
[dependencies.serde_json]
version = "1.0.60"
default_features = false
features = ["alloc"]
optional = true
[dependencies.unicode-xid]
version = "0.2.1"
default_features = false

View File

@ -10,9 +10,14 @@ Each function defined in an `AST` can optionally attach _doc-comments_ (which, a
are comments prefixed by either `///` or `/**`). Doc-comments allow third-party tools to
automatically generate documentation for functions defined in a Rhai script.
A new API, `Engine::gen_fn_metadata_to_json`, paired with the new `metadata` feature,
exports the full list of functions metadata (including those in an `AST`) as a JSON document.
Bug fixes
---------
* Unary prefix operators `-`, `+` and `!` now bind correctly when applied to an expression. Previously, `-x.len` is parsed as `(-x).len` which is obviously counter-intuitive.
* Indexing of namespace-qualified variables now work properly, such as `path::to::var[x]`.
* Constants are no longer propagated by the optimizer if shadowed by a non-constant variable.
* Constants passed as the `this` parameter to Rhai functions now throws an error if assigned to.
* Generic type parameter of `Engine::register_iterator` is `IntoIterator` instead of `Iterator`.
@ -22,7 +27,7 @@ Breaking changes
----------------
* `Engine::on_progress` now takes `u64` instead of `&u64`.
* The closure for `Engine::on_debug` now takes an additional `Position` parameter.
* The closure for `Engine::on_debug` now takes two additional parameters: `source: Option<&str>` and `pos: Position`.
* `AST::iter_functions` now returns `ScriptFnMetadata`.
* The parser function passed to `Engine::register_custom_syntax_raw` now takes an additional parameter containing the _look-ahead_ symbol.
@ -30,10 +35,13 @@ New features
------------
* `AST::iter_functions` now returns `ScriptFnMetadata` which includes, among others, _doc-comments_ for functions prefixed by `///` or `/**`.
* _Doc-comments_ can be enabled/disabled with the new `Engine::set_doc_comments` method.
* A new feature `metadata` is added that pulls in `serde_json` and enables `Engine::gen_fn_metadata_to_json` which exports the full list of functions metadata (including those inside an `AST`) in JSON format.
Enhancements
------------
* A functions lookup cache is added to make function call resolution faster.
* Capturing a constant variable in a closure is now supported, with no cloning.
* Provides position info for `debug` statements.
* A _look-ahead_ symbol is provided to custom syntax parsers, which can be used to parse variable-length symbol streams.

View File

@ -55,6 +55,7 @@ The Rhai Scripting Language
2. [Export a Rust Function](plugins/function.md)
5. [Rhai Language Reference](language/index.md)
1. [Comments](language/comments.md)
1. [Doc-Comments](language/doc-comments.md)
2. [Values and Types](language/values-and-types.md)
1. [Dynamic Values](language/dynamic.md)
2. [Serialization/Deserialization with `serde`](rust/serde.md)
@ -79,14 +80,14 @@ The Rhai Scripting Language
9. [If Statement](language/if.md)
10. [Switch Expression](language/switch.md)
11. [While Loop](language/while.md)
11. [Do Loop](language/do.md)
12. [Loop Statement](language/loop.md)
13. [For Loop](language/for.md)
12. [Do Loop](language/do.md)
13. [Loop Statement](language/loop.md)
14. [For Loop](language/for.md)
1. [Iterators for Custom Types](language/iterator.md)
14. [Return Values](language/return.md)
15. [Throw Exception on Error](language/throw.md)
16. [Catch Exceptions](language/try-catch.md)
17. [Functions](language/functions.md)
15. [Return Values](language/return.md)
16. [Throw Exception on Error](language/throw.md)
17. [Catch Exceptions](language/try-catch.md)
18. [Functions](language/functions.md)
1. [Call Method as Function](language/method.md)
2. [Overloading](language/overload.md)
3. [Namespaces](language/fn-namespaces.md)
@ -94,11 +95,11 @@ The Rhai Scripting Language
5. [Currying](language/fn-curry.md)
6. [Anonymous Functions](language/fn-anon.md)
7. [Closures](language/fn-closure.md)
18. [Print and Debug](language/print-debug.md)
19. [Modules](language/modules/index.md)
19. [Print and Debug](language/print-debug.md)
20. [Modules](language/modules/index.md)
1. [Export Variables, Functions and Sub-Modules](language/modules/export.md)
2. [Import Modules](language/modules/import.md)
20. [Eval Function](language/eval.md)
21. [Eval Function](language/eval.md)
6. [Safety and Protection](safety/index.md)
1. [Checked Arithmetic](safety/checked.md)
2. [Sand-Boxing](safety/sandbox.md)
@ -136,7 +137,9 @@ The Rhai Scripting Language
2. [Custom Operators](engine/custom-op.md)
3. [Extending with Custom Syntax](engine/custom-syntax.md)
5. [Multiple Instantiation](patterns/multiple.md)
6. [Get Function Signatures](engine/get_fn_sig.md)
6. [Functions Metadata](engine/metadata/index.md)
1. [Generate Function Signatures](engine/metadata/gen_fn_sig.md)
2. [Export Metadata to JSON](engine/metadata/export_to_json.md)
10. [Appendix](appendix/index.md)
1. [Keywords](appendix/keywords.md)
2. [Operators and Symbols](appendix/operators.md)

View File

@ -27,7 +27,6 @@ Fast
* Fairly low compile-time overhead.
* Fairly efficient evaluation (1 million iterations in 0.3 sec on a single core, 2.3 GHz Linux VM).
An unofficial Fibonacci benchmark puts Rhai somewhere between Wren and Python.
* Scripts are [optimized][script optimization] (useful for template-based machine-generated scripts) for repeated evaluations.

View File

@ -27,8 +27,8 @@ It doesn't attempt to be a new language. For example:
There is, however, support for simulated [closures] via [currying] a [function pointer] with
captured shared variables.
* **No byte-codes/JIT** - Rhai has an optimized AST-walking interpreter which is fast enough for most usage scenarios.
Essential AST data structures are packed and kept together to maximize cache friendliness.
* **No byte-codes/JIT** - Rhai has an optimized AST-walking interpreter which is fast enough for most casual
usage scenarios. Essential AST data structures are packed and kept together to maximize cache friendliness.
Functions are dispatched based on pre-calculated hashes and accessing variables are mostly through pre-calculated
offsets to the variables file (a [`Scope`]), so it is seldom necessary to look something up by text name.

View File

@ -0,0 +1,106 @@
Export Functions Metadata to JSON
================================
{{#include ../../links.md}}
`Engine::gen_fn_metadata_to_json`
--------------------------------
As part of a _reflections_ API, `Engine::gen_fn_metadata_to_json` exports the full list
of [functions metadata] in JSON format.
The [`metadata`] feature must be used to turn on this method, which requires
the [`serde_json`](https://crates.io/crates/serde_json) crate.
### Sources
Functions from the following sources are included:
1) Script-defined functions in an [`AST`], if provided
2) Native Rust functions registered into the global namespace via the `Engine::register_XXX` API
3) _Public_ (i.e. non-[`private`]) functions (native Rust or Rhai scripted) in global sub-modules registered via
[`Engine::register_module`]({{rootUrl}}/rust/modules/create.md)
4) Native Rust functions in registered [packages] (optional)
Notice that if a function has been [overloaded][function overloading], only the overriding function's
metadata is included.
JSON Schema
-----------
The JSON schema used to hold functions metadata is very simple, containing a nested structure of
`modules` and a list of `functions`.
### Modules Schema
```json
{
"modules":
{
"sub_module_1":
{
"modules":
{
"sub_sub_module_A":
{
"functions":
[
{ ... function metadata ... },
{ ... function metadata ... },
{ ... function metadata ... },
{ ... function metadata ... },
...
]
},
"sub_sub_module_B":
{
...
}
}
},
"sub_module_2":
{
...
},
...
},
"functions":
[
{ ... function metadata ... },
{ ... function metadata ... },
{ ... function metadata ... },
{ ... function metadata ... },
...
]
}
```
### Function Metadata Schema
```json
{
"namespace": "internal" | "global",
"access": "public" | "private",
"name": "fn_name",
"type": "native" | "script",
"numParams": 42, /* number of parameters */
"params": /* omitted if no parameters */
[
{ "name": "param_1", "type": "type_1" },
{ "name": "param_2" }, /* no type info */
{ "name": "_", "type": "type_3" },
...
],
"returnType": "ret_type", /* omitted if unknown */
"signature": "[private] fn_name(param_1: type_1, param_2, _: type_3) -> ret_type",
"docComments": /* omitted if none */
[
"/// doc-comment line 1",
"/// doc-comment line 2",
"/** doc-comment block */",
...
]
}
```

View File

@ -1,26 +1,29 @@
Get Function Signatures
=======================
Generate Function Signatures
===========================
{{#include ../links.md}}
{{#include ../../links.md}}
`Engine::gen_fn_signatures`
--------------------------
As part of a _reflections_ API, `Engine::gen_fn_signatures` returns a list of function signatures
(`Vec<String>`), each corresponding to a particular function available to that [`Engine`] instance.
As part of a _reflections_ API, `Engine::gen_fn_signatures` returns a list of function _signatures_
(as `Vec<String>`), each corresponding to a particular function available to that [`Engine`] instance.
> `fn_name ( param_1: type_1, param_2: type_2, ... , param_n : type_n ) -> return_type`
### Sources
Functions from the following sources are included, in order:
1) Functions registered into the global namespace via the `Engine::register_XXX` API,
2) Functions in global sub-modules registered via [`Engine::register_module`]({{rootUrl}}/rust/modules/create.md),
3) Functions in registered [packages] (optional)
Included are both native Rust as well as script-defined functions (except [`private`] ones).
1) Native Rust functions registered into the global namespace via the `Engine::register_XXX` API
2) _Public_ (i.e. non-[`private`]) functions (native Rust or Rhai scripted) in global sub-modules registered via
[`Engine::register_module`]({{rootUrl}}/rust/modules/create.md)
3) Native Rust functions in registered [packages] (optional)
Function Metadata
-----------------
Functions Metadata
------------------
Beware, however, that not all function signatures contain parameters and return value information.
@ -30,7 +33,7 @@ For instance, functions registered via `Engine::register_XXX` contain no informa
the names of parameter and their actual types because Rust simply does not make such metadata
available natively. The return type is also undetermined.
A function registered under the name 'foo' with three parameters and unknown return type:
A function registered under the name `foo` with three parameters and unknown return type:
> `foo(_, _, _)`
@ -41,18 +44,18 @@ Notice that function names do not need to be valid identifiers.
A [property setter][getters/setters] - again, unknown parameters and return type.
Notice that function names do not need to be valid identifiers.
In this case, the first parameter should be '&mut T' of the custom type and the return value is '()':
In this case, the first parameter should be `&mut T` of the custom type and the return value is `()`:
> `set$prop(_, _, _)`
### Script-defined functions
### Script-Defined Functions
Script-defined [function] signatures contain parameter names. Since all parameters, as well as
the return value, are [`Dynamic`] the types are simply not shown.
A script-defined function always takes dynamic arguments, and the return type is also dynamic:
> `foo(x, y, z)`
> `foo(x, y, z) -> Dynamic`
probably defined as:
@ -66,22 +69,22 @@ is the same as:
> `foo(x: Dynamic, y: Dynamic, z: Dynamic) -> Result<Dynamic, Box<EvalAltResult>>`
### Plugin functions
### Plugin Functions
Functions defined in [plugin modules] are the best. They contain all the metadata
describing the functions.
A plugin function `merge`:
For example, a plugin function `merge`:
> `merge(list: &mut MyStruct<i64>, num: usize, name: &str) -> Option<bool>`
Notice that function names do not need to be valid identifiers.
An operator defined as a [fallible function] in a [plugin module] via `#[rhai_fn(name="+=", return_raw)]`
returns `Result<bool, Box<EvalAltResult>>`:
For example, an operator defined as a [fallible function] in a [plugin module] via
`#[rhai_fn(name="+=", return_raw)]` returns `Result<bool, Box<EvalAltResult>>`:
> `+=(list: &mut MyStruct<i64>, num: usize, name: &str) -> Result<bool, Box<EvalAltResult>>`
A [property getter][getters/setters] defined in a [plugin module]:
For example, a [property getter][getters/setters] defined in a [plugin module]:
> `get$prop(obj: &mut MyStruct<i64>) -> String`

View File

@ -0,0 +1,28 @@
Functions Metadata
==================
{{#include ../../links.md}}
The _metadata_ of a [function] means all relevant information related to a function's
definition including:
1. Its callable name
2. Its access mode (public or [private][`private`])
3. Its parameters and types (if any)
4. Its return value and type (if any)
5. Its nature (i.e. native Rust-based or Rhai script-based)
6. Its [namespace][function namespace] (module or global)
7. Its purpose, in the form of [doc-comments]
8. Usage notes, warnings, etc., in the form of [doc-comments]
A function's _signature_ encapsulates the first four pieces of information in a single
concise line of definition:
> `[private] fn_name ( param_1: type_1, param_2: type_2, ... , param_n : type_n ) -> return_type`

View File

@ -5,14 +5,15 @@ Engine Configuration Options
A number of other configuration options are available from the `Engine` to fine-tune behavior and safeguards.
| Method | Not available under | Description |
| ------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `set_optimization_level` | [`no_optimize`] | sets the amount of script _optimizations_ performed. See [script optimization]. |
| `set_max_expr_depths` | [`unchecked`] | sets the maximum nesting levels of an expression/statement. See [maximum statement depth]. |
| `set_max_call_levels` | [`unchecked`] | sets the maximum number of function call levels (default 50) to avoid infinite recursion. See [maximum call stack depth]. |
| `set_max_operations` | [`unchecked`] | sets the maximum number of _operations_ that a script is allowed to consume. See [maximum number of operations]. |
| `set_max_modules` | [`unchecked`] | sets the maximum number of [modules] that a script is allowed to load. See [maximum number of modules]. |
| `set_max_string_size` | [`unchecked`] | sets the maximum length (in UTF-8 bytes) for [strings]. See [maximum length of strings]. |
| `set_max_array_size` | [`unchecked`], [`no_index`] | sets the maximum size for [arrays]. See [maximum size of arrays]. |
| `set_max_map_size` | [`unchecked`], [`no_object`] | sets the maximum number of properties for [object maps]. See [maximum size of object maps]. |
| `disable_symbol` | | disables a certain keyword or operator. See [disable keywords and operators]. |
| Method | Not available under | Description |
| ------------------------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `set_doc_comments` | | enables/disables [doc-comments] |
| `set_optimization_level` | [`no_optimize`] | sets the amount of script _optimizations_ performedSee [script optimization] |
| `set_max_expr_depths` | [`unchecked`] | sets the maximum nesting levels of an expression/statementSee [maximum statement depth] |
| `set_max_call_levels` | [`unchecked`] | sets the maximum number of function call levels (default 50) to avoid infinite recursionSee [maximum call stack depth] |
| `set_max_operations` | [`unchecked`] | sets the maximum number of _operations_ that a script is allowed to consumeSee [maximum number of operations] |
| `set_max_modules` | [`unchecked`] | sets the maximum number of [modules] that a script is allowed to loadSee [maximum number of modules] |
| `set_max_string_size` | [`unchecked`] | sets the maximum length (in UTF-8 bytes) for [strings]See [maximum length of strings] |
| `set_max_array_size` | [`unchecked`], [`no_index`] | sets the maximum size for [arrays]See [maximum size of arrays] |
| `set_max_map_size` | [`unchecked`], [`no_object`] | sets the maximum number of properties for [object maps]See [maximum size of object maps] |
| `disable_symbol` | | disables a certain keyword or operatorSee [disable keywords and operators] |

View File

@ -22,43 +22,3 @@ let /* intruder comment */ name = "Bob";
/*/*/*/*/**/*/*/*/*/
*/
```
Doc-Comments
------------
Similar to Rust, comments starting with `///` (three slashes) or `/**` (two asterisks) are
_doc-comments_.
Doc-comments can only appear in front of [function] definitions, not any other elements:
```rust
/// This is a valid one-line doc-comment
fn foo() {}
/** This is a
** valid block
** doc-comment
**/
fn bar(x) {
/// Syntax error - this doc-comment is invalid
x + 1
}
/** Syntax error - this doc-comment is invalid */
let x = 42;
/// Syntax error - this doc-comment is also invalid
{
let x = 42;
}
```
Doc-comments are stored within the script's [`AST`] after compilation.
The `AST::iter_functions` method provides a `ScriptFnMetadata` instance
for each function defined within the script, which includes doc-comments.
Doc-comments never affect the evaluation of a script nor do they incur
significant performance overhead. However, third party tools can take advantage
of this information to auto-generate documentation for Rhai script functions.

View File

@ -0,0 +1,76 @@
Doc-Comments
============
Similar to Rust, comments starting with `///` (three slashes) or `/**` (two asterisks) are
_doc-comments_.
Doc-comments can only appear in front of [function] definitions, not any other elements:
```rust
/// This is a valid one-line doc-comment
fn foo() {}
/** This is a
** valid block
** doc-comment
**/
fn bar(x) {
/// Syntax error - this doc-comment is invalid
x + 1
}
/** Syntax error - this doc-comment is invalid */
let x = 42;
/// Syntax error - this doc-comment is also invalid
{
let x = 42;
}
```
Special Cases
-------------
Long streams of `//////...` and `/*****...` do _NOT_ form doc-comments.
This is consistent with popular comment block styles for C-like languages.
```rust
/////////////////////////////// <- this is not a doc-comment
// This is not a doc-comment // <- this is a normal comment
/////////////////////////////// <- this is not a doc-comment
// However, watch out for comment lines starting with '///'
////////////////////////////////////////// <- this is not a doc-comment
/// This, however, IS a doc-comment!!! /// <- this starts with '///'
////////////////////////////////////////// <- this is not a doc-comment
/****************************************
* *
* This is also not a doc-comment block *
* so we don't have to put this in *
* front of a function. *
* *
****************************************/
```
Using Doc-Comments
------------------
Doc-comments are stored within the script's [`AST`] after compilation.
The `AST::iter_functions` method provides a `ScriptFnMetadata` instance
for each function defined within the script, which includes doc-comments.
Doc-comments never affect the evaluation of a script nor do they incur
significant performance overhead. However, third party tools can take advantage
of this information to auto-generate documentation for Rhai script functions.
Disabling Doc-Comments
----------------------
Doc-comments can be disabled via the `Engine::set_doc_comments` method.

View File

@ -27,7 +27,7 @@ engine.on_print(|x| println!("hello: {}", x));
// Any function or closure that takes a '&str' and a 'Position' argument can be used to
// override 'debug'.
engine.on_debug(|x, pos| println!("DEBUG at {:?}: {}", pos, x));
engine.on_debug(|x, src, pos| println!("DEBUG of {} at {:?}: {}", src.unwrap_or("unknown"), pos, x));
// Example: quick-'n-dirty logging
let logbook = Arc::new(RwLock::new(Vec::<String>::new()));
@ -37,9 +37,9 @@ let log = logbook.clone();
engine.on_print(move |s| log.write().unwrap().push(format!("entry: {}", s)));
let log = logbook.clone();
engine.on_debug(move |s, pos| log.write().unwrap().push(
format!("DEBUG at {:?}: {}", pos, s)
));
engine.on_debug(move |s, src, pos| log.write().unwrap().push(
format!("DEBUG of {} at {:?}: {}", src.unwrap_or("unknown"), pos, s)
));
// Evaluate script
engine.eval::<()>(script)?;
@ -49,3 +49,24 @@ for entry in logbook.read().unwrap().iter() {
println!("{}", entry);
}
```
`on_debug` Callback Signature
-----------------------------
The function signature passed to `Engine::on_debug` takes the following form:
> `Fn(text: &str, source: Option<&str>, pos: Position) + 'static`
where:
| Parameter | Type | Description |
| --------- | :------------: | --------------------------------------------------------------- |
| `text` | `&str` | text to display |
| `source` | `Option<&str>` | source of the current evaluation, if any |
| `pos` | `Position` | position (line number and character offset) of the `debug` call |
The _source_ of a script evaluation is any text string provided to an [`AST`] via the `AST::set_source` method.
If a [module] is loaded via an [`import`] statement, then the _source_ of functions defined
within the module will be the module's _path_.

View File

@ -13,6 +13,7 @@
[`no_closure`]: {{rootUrl}}/start/features.md
[`no_std`]: {{rootUrl}}/start/features.md
[`no-std`]: {{rootUrl}}/start/features.md
[`metadata`]: {{rootUrl}}/start/features.md
[`internals`]: {{rootUrl}}/start/features.md
[`unicode-xid-ident`]: {{rootUrl}}/start/features.md
@ -40,6 +41,7 @@
[plugin modules]: {{rootUrl}}/plugins/module.md
[plugin function]: {{rootUrl}}/plugins/function.md
[plugin functions]: {{rootUrl}}/plugins/function.md
[functions metadata]: {{rootUrl}}/engine/metadata/index.md
[`Scope`]: {{rootUrl}}/engine/scope.md
[`serde`]: {{rootUrl}}/rust/serde.md
@ -89,6 +91,7 @@
[timestamp]: {{rootUrl}}/language/timestamps.md
[timestamps]: {{rootUrl}}/language/timestamps.md
[doc-comments]: {{rootUrl}}/language/doc-comments.md
[function]: {{rootUrl}}/language/functions.md
[functions]: {{rootUrl}}/language/functions.md
[function overloading]: {{rootUrl}}/rust/functions.md#function-overloading

View File

@ -178,22 +178,21 @@ Use `switch` Through Arrays
---------------------------
Another way to work with Rust enums in a `switch` expression is through exposing the internal data
of each enum variant as a variable-length [array], usually with the name of the variant as
the first item for convenience:
(or at least those that act as effective _discriminants_) of each enum variant as a variable-length
[array], usually with the name of the variant as the first item for convenience:
```rust
use rhai::Array;
engine.register_get("enum_data", |x: &mut Enum| {
match x {
Enum::Foo => vec![
"Foo".into()
] as Array,
Enum::Foo => vec![ "Foo".into() ] as Array,
Enum::Bar(value) => vec![
"Bar".into(), (*value).into()
] as Array,
// Say, skip the data field because it is not
// used as a discriminant
Enum::Bar(value) => vec![ "Bar".into() ] as Array,
// Say, all fields act as discriminants
Enum::Baz(val1, val2) => vec![
"Baz".into(), val1.clone().into(), (*val2).into()
] as Array
@ -208,8 +207,7 @@ Then it is a simple matter to match an enum via the `switch` expression:
// 'enum_data' creates a variable-length array with 'MyEnum' data
let x = switch value.enum_data {
["Foo"] => 1,
["Bar", 42] => 2,
["Bar", 123] => 3,
["Bar"] => value.field_1,
["Baz", "hello", false] => 4,
["Baz", "hello", true] => 5,
_ => 9
@ -220,10 +218,20 @@ x == 5;
// Which is essentially the same as:
let x = switch [value.type, value.field_0, value.field_1] {
["Foo", (), ()] => 1,
["Bar", 42, ()] => 2,
["Bar", 123, ()] => 3,
["Bar", 42, ()] => 42,
["Bar", 123, ()] => 123,
:
["Baz", "hello", false] => 4,
["Baz", "hello", true] => 5,
_ => 9
}
```
Usually, a helper method returns an array of values that can uniquely determine
the switch case based on actual usage requirements - which means that it probably
skips fields that contain data instead of discriminants.
Then `switch` is used to very quickly match through a large number of array shapes
and jump to the appropriate case implementation.
Data fields can then be extracted from the enum independently.

View File

@ -26,10 +26,10 @@ Use Closures to Define Methods
-----------------------------
[Anonymous functions] or [closures] defined as values for [object map] properties take on
a syntactic shape that resembles very closely that of class methods in an OOP language.
a syntactic shape which resembles very closely that of class methods in an OOP language.
Closures also _[capture][automatic currying]_ variables from the defining environment, which is a very
common OOP pattern. Capturing is accomplished via a feature called _[automatic currying]_ and
common language feature. Capturing is accomplished via a feature called _[automatic currying]_ and
can be turned off via the [`no_closure`] feature.
@ -59,3 +59,51 @@ factor = 2;
obj.update(42);
obj.action(); // prints 84
```
Simulating Inheritance With Mixin
--------------------------------
The `fill_with` method of [object maps] can be conveniently used to _polyfill_ default
method implementations from a _base class_, as per OOP lingo.
Do not use the `mixin` method because it _overwrites_ existing fields.
```rust
// Define base class
let BaseClass = #{
factor: 1,
data: 42,
get_data: || this.data * 2,
update: |x| this.data += x * this.factor
};
let obj = #{
// Override base class field
factor: 100,
// Override base class method
// Notice that the base class can also be accessed, if in scope
get_data: || this.call(BaseClass.get_data) * 999,
}
// Polyfill missing fields/methods
obj.fill_with(BaseClass);
// By this point, 'obj' has the following:
//
// #{
// factor: 100
// data: 42,
// get_data: || this.call(BaseClass.get_data) * 999,
// update: |x| this.data += x * this.factor
// }
// obj.get_data() => (this.data (42) * 2) * 999
obj.get_data() == 83916;
obj.update(1);
obj.data == 142
```

View File

@ -11,24 +11,25 @@ Notice that this deviates from Rust norm where features are _additive_.
Excluding unneeded functionalities can result in smaller, faster builds as well as
more control over what a script can (or cannot) do.
| Feature | Additive? | Description |
| ------------------- | :-------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unchecked` | no | disables arithmetic checking (such as over-flows and division by zero), call stack depth limit, operations count limit and modules loading limit.<br/>Beware that a bad script may panic the entire system! |
| `sync` | no | restricts all values types to those that are `Send + Sync`. Under this feature, all Rhai types, including [`Engine`], [`Scope`] and [`AST`], are all `Send + Sync` |
| `no_optimize` | no | disables [script optimization] |
| `no_float` | no | disables floating-point numbers and math |
| `f32_float` | no | sets the system floating-point type to `f32` instead of `f64`. `FLOAT` is set to `f32` |
| `only_i32` | no | sets the system integer type to `i32` and disable all other integer types. `INT` is set to `i32` |
| `only_i64` | no | sets the system integer type to `i64` and disable all other integer types. `INT` is set to `i64` |
| `no_index` | no | disables [arrays] and indexing features |
| `no_object` | no | disables support for [custom types] and [object maps] |
| `no_function` | no | disables script-defined [functions] |
| `no_module` | no | disables loading external [modules] |
| `no_closure` | no | disables [capturing][automatic currying] external variables in [anonymous functions] to simulate _closures_, or [capturing the calling scope]({{rootUrl}}/language/fn-capture.md) in function calls |
| `no_std` | no | builds for `no-std` (implies `no_closure`). Notice that additional dependencies will be pulled in to replace `std` features |
| `serde` | yes | enables serialization/deserialization via `serde`. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies |
| `unicode-xid-ident` | no | allows [Unicode Standard Annex #31](http://www.unicode.org/reports/tr31/) as identifiers |
| `internals` | yes | exposes internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version |
| Feature | Additive? | Description |
| ------------------- | :-------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unchecked` | no | disables arithmetic checking (such as over-flows and division by zero), call stack depth limit, operations count limit and modules loading limit.<br/>Beware that a bad script may panic the entire system! |
| `sync` | no | restricts all values types to those that are `Send + Sync`. Under this feature, all Rhai types, including [`Engine`], [`Scope`] and [`AST`], are all `Send + Sync` |
| `no_optimize` | no | disables [script optimization] |
| `no_float` | no | disables floating-point numbers and math |
| `f32_float` | no | sets the system floating-point type to `f32` instead of `f64`. `FLOAT` is set to `f32` |
| `only_i32` | no | sets the system integer type to `i32` and disable all other integer types. `INT` is set to `i32` |
| `only_i64` | no | sets the system integer type to `i64` and disable all other integer types. `INT` is set to `i64` |
| `no_index` | no | disables [arrays] and indexing features |
| `no_object` | no | disables support for [custom types] and [object maps] |
| `no_function` | no | disables script-defined [functions] (implies `no_closure`) |
| `no_module` | no | disables loading external [modules] |
| `no_closure` | no | disables [capturing][automatic currying] external variables in [anonymous functions] to simulate _closures_, or [capturing the calling scope]({{rootUrl}}/language/fn-capture.md) in function calls |
| `no_std` | no | builds for `no-std` (implies `no_closure`). Notice that additional dependencies will be pulled in to replace `std` features |
| `serde` | yes | enables serialization/deserialization via `serde` (requires the [`serde`](https://crates.io/crates/serde) crate) |
| `unicode-xid-ident` | no | allows [Unicode Standard Annex #31](http://www.unicode.org/reports/tr31/) as identifiers |
| `metadata` | yes | enables exporting [functions metadata] to [JSON format]({{rootUrl}}/engine/metadata/export_to_json.md) (implies `serde` and additionally requires the [`serde_json`](https://crates.io/crates/serde_json) crate) |
| `internals` | yes | exposes internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version |
Example

View File

@ -147,6 +147,15 @@ fn main() {
println!();
continue;
}
// "json" => {
// println!(
// "{}",
// engine
// .gen_fn_metadata_to_json(Some(&main_ast), false)
// .unwrap()
// );
// continue;
// }
_ => (),
}

View File

@ -163,6 +163,8 @@ impl<'a> Into<ScriptFnMetadata<'a>> for &'a ScriptFnDef {
/// Currently, [`AST`] is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`.
#[derive(Debug, Clone)]
pub struct AST {
/// Source of the [`AST`].
source: Option<ImmutableString>,
/// Global statements.
statements: Vec<Stmt>,
/// Script-defined functions.
@ -172,6 +174,7 @@ pub struct AST {
impl Default for AST {
fn default() -> Self {
Self {
source: None,
statements: Vec::with_capacity(16),
functions: Default::default(),
}
@ -186,10 +189,36 @@ impl AST {
functions: impl Into<Shared<Module>>,
) -> Self {
Self {
source: None,
statements: statements.into_iter().collect(),
functions: functions.into(),
}
}
/// Create a new [`AST`] with a source name.
#[inline(always)]
pub fn new_with_source(
statements: impl IntoIterator<Item = Stmt>,
functions: impl Into<Shared<Module>>,
source: impl Into<ImmutableString>,
) -> Self {
Self {
source: Some(source.into()),
statements: statements.into_iter().collect(),
functions: functions.into(),
}
}
/// Get the source.
pub fn source(&self) -> Option<&str> {
self.source.as_ref().map(|s| s.as_str())
}
/// Clone the source.
pub(crate) fn clone_source(&self) -> Option<ImmutableString> {
self.source.clone()
}
/// Set the source.
pub fn set_source<S: Into<ImmutableString>>(&mut self, source: Option<S>) {
self.source = source.map(|s| s.into())
}
/// Get the statements.
#[cfg(not(feature = "internals"))]
#[inline(always)]
@ -253,6 +282,7 @@ impl AST {
let mut functions: Module = Default::default();
functions.merge_filtered(&self.functions, &mut filter);
Self {
source: self.source.clone(),
statements: Default::default(),
functions: functions.into(),
}
@ -262,6 +292,7 @@ impl AST {
#[inline(always)]
pub fn clone_statements_only(&self) -> Self {
Self {
source: self.source.clone(),
statements: self.statements.clone(),
functions: Default::default(),
}
@ -432,6 +463,7 @@ impl AST {
let Self {
statements,
functions,
..
} = self;
let ast = match (statements.is_empty(), other.statements.is_empty()) {
@ -445,10 +477,20 @@ impl AST {
(true, true) => vec![],
};
let source = if other.source.is_some() {
other.source.clone()
} else {
self.source.clone()
};
let mut functions = functions.as_ref().clone();
functions.merge_filtered(&other.functions, &mut filter);
Self::new(ast, functions)
if let Some(source) = source {
Self::new_with_source(ast, functions, source)
} else {
Self::new(ast, functions)
}
}
/// Combine one [`AST`] with another. The second [`AST`] is consumed.
///
@ -1191,12 +1233,16 @@ impl Expr {
| Self::Map(_, _) => match token {
#[cfg(not(feature = "no_index"))]
Token::LeftBracket => true,
#[cfg(not(feature = "no_object"))]
Token::Period => true,
_ => false,
},
Self::Variable(_) => match token {
#[cfg(not(feature = "no_index"))]
Token::LeftBracket => true,
#[cfg(not(feature = "no_object"))]
Token::Period => true,
Token::LeftParen => true,
Token::Bang => true,
Token::DoubleColon => true,
@ -1206,6 +1252,8 @@ impl Expr {
Self::Property(_) => match token {
#[cfg(not(feature = "no_index"))]
Token::LeftBracket => true,
#[cfg(not(feature = "no_object"))]
Token::Period => true,
Token::LeftParen => true,
_ => false,
},

View File

@ -24,7 +24,7 @@ use crate::stdlib::{
string::{String, ToString},
};
use crate::syntax::CustomSyntax;
use crate::utils::get_hasher;
use crate::utils::{get_hasher, StraightHasherBuilder};
use crate::{
calc_native_fn_hash, Dynamic, EvalAltResult, FnPtr, ImmutableString, Module, Position, Scope,
Shared, StaticVec,
@ -100,19 +100,21 @@ impl Imports {
}
/// Get an iterator to this stack of imported [modules][Module] in reverse order.
#[allow(dead_code)]
pub fn iter<'a>(&'a self) -> impl Iterator<Item = (ImmutableString, Shared<Module>)> + 'a {
pub fn iter<'a>(&'a self) -> impl Iterator<Item = (&'a str, &'a Module)> + 'a {
self.0.iter().flat_map(|lib| {
lib.iter()
.rev()
.map(|(name, module)| (name.clone(), module.clone()))
.map(|(name, module)| (name.as_str(), module.as_ref()))
})
}
/// Get an iterator to this stack of imported [modules][Module] in reverse order.
#[allow(dead_code)]
pub(crate) fn iter_raw<'a>(
&'a self,
) -> impl Iterator<Item = (ImmutableString, Shared<Module>)> + 'a {
self.0.iter().flat_map(|lib| lib.iter().rev().cloned())
) -> impl Iterator<Item = (&'a ImmutableString, &'a Shared<Module>)> + 'a {
self.0
.iter()
.flat_map(|lib| lib.iter().rev().map(|(n, m)| (n, m)))
}
/// Get a consuming iterator to this stack of imported [modules][Module] in reverse order.
pub fn into_iter(self) -> impl Iterator<Item = (ImmutableString, Shared<Module>)> {
@ -125,15 +127,23 @@ impl Imports {
/// Does the specified function hash key exist in this stack of imported [modules][Module]?
#[allow(dead_code)]
pub fn contains_fn(&self, hash: u64) -> bool {
self.0.as_ref().map_or(false, |x| {
x.iter().any(|(_, m)| m.contains_qualified_fn(hash))
})
if hash == 0 {
false
} else {
self.0.as_ref().map_or(false, |x| {
x.iter().any(|(_, m)| m.contains_qualified_fn(hash))
})
}
}
/// Get specified function via its hash key.
pub fn get_fn(&self, hash: u64) -> Option<&CallableFunction> {
self.0
.as_ref()
.and_then(|x| x.iter().rev().find_map(|(_, m)| m.get_qualified_fn(hash)))
if hash == 0 {
None
} else {
self.0
.as_ref()
.and_then(|x| x.iter().rev().find_map(|(_, m)| m.get_qualified_fn(hash)))
}
}
/// Does the specified [`TypeId`][std::any::TypeId] iterator exist in this stack of imported [modules][Module]?
#[allow(dead_code)]
@ -368,29 +378,29 @@ impl<'a> Target<'a> {
#[cfg(not(feature = "no_index"))]
Self::StringChar(_, _, ch) => {
let char_value = ch.clone();
self.set_value((char_value, Position::NONE)).unwrap();
self.set_value(char_value, Position::NONE).unwrap();
}
}
}
/// Update the value of the `Target`.
#[cfg(any(not(feature = "no_object"), not(feature = "no_index")))]
pub fn set_value(&mut self, new_val: (Dynamic, Position)) -> Result<(), Box<EvalAltResult>> {
pub fn set_value(&mut self, new_val: Dynamic, pos: Position) -> Result<(), Box<EvalAltResult>> {
match self {
Self::Ref(r) => **r = new_val.0,
Self::Ref(r) => **r = new_val,
#[cfg(not(feature = "no_closure"))]
#[cfg(not(feature = "no_object"))]
Self::LockGuard((r, _)) => **r = new_val.0,
Self::LockGuard((r, _)) => **r = new_val,
Self::Value(_) => unreachable!(),
#[cfg(not(feature = "no_index"))]
Self::StringChar(string, index, _) if string.is::<ImmutableString>() => {
let mut s = string.write_lock::<ImmutableString>().unwrap();
// Replace the character at the specified index position
let new_ch = new_val.0.as_char().map_err(|err| {
let new_ch = new_val.as_char().map_err(|err| {
Box::new(EvalAltResult::ErrorMismatchDataType(
err.to_string(),
"char".to_string(),
new_val.1,
pos,
))
})?;
@ -468,8 +478,10 @@ impl<T: Into<Dynamic>> From<T> for Target<'_> {
/// ## WARNING
///
/// This type is volatile and may change.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
#[derive(Debug, Clone, Default)]
pub struct State {
/// Source of the current context.
pub source: Option<ImmutableString>,
/// Normally, access to variables are parsed with a relative offset into the scope to avoid a lookup.
/// In some situation, e.g. after running an `eval` statement, subsequent offsets become mis-aligned.
/// When that happens, this flag is turned on to force a scope lookup by name.
@ -481,6 +493,8 @@ pub struct State {
pub operations: u64,
/// Number of modules loaded.
pub modules: usize,
/// Cached lookup values for function hashes.
pub functions_cache: HashMap<u64, Option<CallableFunction>, StraightHasherBuilder>,
}
impl State {
@ -644,6 +658,9 @@ pub struct Engine {
/// Max limits.
#[cfg(not(feature = "unchecked"))]
pub(crate) limits: Limits,
/// Disable doc-comments?
pub(crate) disable_doc_comments: bool,
}
impl fmt::Debug for Engine {
@ -695,10 +712,14 @@ fn default_print(_s: &str) {
/// Debug to stdout
#[inline(always)]
fn default_debug(_s: &str, _pos: Position) {
fn default_debug(_s: &str, _source: Option<&str>, _pos: Position) {
#[cfg(not(feature = "no_std"))]
#[cfg(not(target_arch = "wasm32"))]
println!("{:?} | {}", _pos, _s);
if let Some(source) = _source {
println!("{} @ {:?} | {}", source, _pos, _s);
} else {
println!("{:?} | {}", _pos, _s);
}
}
/// Search for a module within an imports stack.
@ -784,6 +805,8 @@ impl Engine {
#[cfg(not(feature = "no_object"))]
max_map_size: 0,
},
disable_doc_comments: false,
};
engine.load_package(StandardPackage::new().get());
@ -813,7 +836,7 @@ impl Engine {
resolve_var: None,
print: Box::new(|_| {}),
debug: Box::new(|_, _| {}),
debug: Box::new(|_, _, _| {}),
progress: None,
optimization_level: if cfg!(feature = "no_optimize") {
@ -837,6 +860,8 @@ impl Engine {
#[cfg(not(feature = "no_object"))]
max_map_size: 0,
},
disable_doc_comments: false,
}
}
@ -1014,7 +1039,8 @@ impl Engine {
) {
// Indexed value is a reference - update directly
Ok(ref mut obj_ptr) => {
obj_ptr.set_value(new_val.unwrap())?;
let (new_val, new_val_pos) = new_val.unwrap();
obj_ptr.set_value(new_val, new_val_pos)?;
None
}
Err(err) => match *err {
@ -1090,7 +1116,9 @@ impl Engine {
mods, state, lib, target_val, index, *pos, true, is_ref, false, level,
)?;
val.set_value(new_val.unwrap())?;
let (new_val, new_val_pos) = new_val.unwrap();
val.set_value(new_val, new_val_pos)?;
Ok((Default::default(), true))
}
// {xxx:map}.id
@ -1291,8 +1319,7 @@ impl Engine {
pos: var_pos,
} = &x.3;
self.inc_operations(state)
.map_err(|err| err.fill_position(*var_pos))?;
self.inc_operations(state, *var_pos)?;
let (target, _, pos) =
self.search_namespace(scope, mods, state, lib, this_ptr, lhs)?;
@ -1343,8 +1370,7 @@ impl Engine {
size: usize,
level: usize,
) -> Result<(), Box<EvalAltResult>> {
self.inc_operations(state)
.map_err(|err| err.fill_position(expr.position()))?;
self.inc_operations(state, expr.position())?;
match expr {
Expr::FnCall(x, _) if x.namespace.is_none() => {
@ -1418,7 +1444,7 @@ impl Engine {
_indexers: bool,
_level: usize,
) -> Result<Target<'t>, Box<EvalAltResult>> {
self.inc_operations(state)?;
self.inc_operations(state, Position::NONE)?;
match target {
#[cfg(not(feature = "no_index"))]
@ -1521,8 +1547,7 @@ impl Engine {
rhs: &Expr,
level: usize,
) -> Result<Dynamic, Box<EvalAltResult>> {
self.inc_operations(state)
.map_err(|err| err.fill_position(rhs.position()))?;
self.inc_operations(state, rhs.position())?;
let lhs_value = self.eval_expr(scope, mods, state, lib, this_ptr, lhs, level)?;
let rhs_value = self.eval_expr(scope, mods, state, lib, this_ptr, rhs, level)?;
@ -1695,8 +1720,7 @@ impl Engine {
expr: &Expr,
level: usize,
) -> Result<Dynamic, Box<EvalAltResult>> {
self.inc_operations(state)
.map_err(|err| err.fill_position(expr.position()))?;
self.inc_operations(state, expr.position())?;
let result = match expr {
Expr::Expr(x) => self.eval_expr(scope, mods, state, lib, this_ptr, x, level),
@ -1851,8 +1875,7 @@ impl Engine {
_ => unreachable!(),
};
self.check_data_size(result)
.map_err(|err| err.fill_position(expr.position()))
self.check_data_size(result, expr.position())
}
/// Evaluate a statements block.
@ -1878,6 +1901,10 @@ impl Engine {
});
scope.rewind(prev_scope_len);
if mods.len() != prev_mods_len {
// If imports list is modified, clear the functions lookup cache
state.functions_cache.clear();
}
mods.truncate(prev_mods_len);
state.scope_level -= 1;
@ -1904,8 +1931,7 @@ impl Engine {
stmt: &Stmt,
level: usize,
) -> Result<Dynamic, Box<EvalAltResult>> {
self.inc_operations(state)
.map_err(|err| err.fill_position(stmt.position()))?;
self.inc_operations(state, stmt.position())?;
let result = match stmt {
// No-op
@ -1927,8 +1953,7 @@ impl Engine {
return EvalAltResult::ErrorAssignmentToConstant(name.to_string(), pos).into();
}
self.inc_operations(state)
.map_err(|err| err.fill_position(pos))?;
self.inc_operations(state, pos)?;
if lhs_ptr.as_ref().is_read_only() {
// Assignment to constant variable
@ -2187,8 +2212,7 @@ impl Engine {
*loop_var = value;
}
self.inc_operations(state)
.map_err(|err| err.fill_position(stmt.position()))?;
self.inc_operations(state, stmt.position())?;
match self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level) {
Ok(_) => (),
@ -2357,6 +2381,8 @@ impl Engine {
} else {
mods.push(name_def.name.clone(), module);
}
// When imports list is modified, clear the functions lookup cache
state.functions_cache.clear();
}
state.modules += 1;
@ -2404,8 +2430,7 @@ impl Engine {
}
};
self.check_data_size(result)
.map_err(|err| err.fill_position(stmt.position()))
self.check_data_size(result, stmt.position())
}
/// Check a result to ensure that the data size is within allowable limit.
@ -2415,16 +2440,17 @@ impl Engine {
fn check_data_size(
&self,
result: Result<Dynamic, Box<EvalAltResult>>,
_pos: Position,
) -> Result<Dynamic, Box<EvalAltResult>> {
result
}
/// Check a result to ensure that the data size is within allowable limit.
/// [`Position`] in [`EvalAltResult`] may be None and should be set afterwards.
#[cfg(not(feature = "unchecked"))]
fn check_data_size(
&self,
result: Result<Dynamic, Box<EvalAltResult>>,
pos: Position,
) -> Result<Dynamic, Box<EvalAltResult>> {
// If no data size limits, just return
let mut total = 0;
@ -2513,47 +2539,41 @@ impl Engine {
let (_arr, _map, s) = calc_size(result.as_ref().unwrap());
if s > self.max_string_size() {
return EvalAltResult::ErrorDataTooLarge(
"Length of string".to_string(),
Position::NONE,
)
.into();
return EvalAltResult::ErrorDataTooLarge("Length of string".to_string(), pos).into();
}
#[cfg(not(feature = "no_index"))]
if _arr > self.max_array_size() {
return EvalAltResult::ErrorDataTooLarge("Size of array".to_string(), Position::NONE)
.into();
return EvalAltResult::ErrorDataTooLarge("Size of array".to_string(), pos).into();
}
#[cfg(not(feature = "no_object"))]
if _map > self.max_map_size() {
return EvalAltResult::ErrorDataTooLarge(
"Size of object map".to_string(),
Position::NONE,
)
.into();
return EvalAltResult::ErrorDataTooLarge("Size of object map".to_string(), pos).into();
}
result
}
/// Check if the number of operations stay within limit.
/// [`Position`] in [`EvalAltResult`] is [`None`][Position::None] and must be set afterwards.
pub(crate) fn inc_operations(&self, state: &mut State) -> Result<(), Box<EvalAltResult>> {
pub(crate) fn inc_operations(
&self,
state: &mut State,
pos: Position,
) -> Result<(), Box<EvalAltResult>> {
state.operations += 1;
#[cfg(not(feature = "unchecked"))]
// Guard against too many operations
if self.max_operations() > 0 && state.operations > self.max_operations() {
return EvalAltResult::ErrorTooManyOperations(Position::NONE).into();
return EvalAltResult::ErrorTooManyOperations(pos).into();
}
// Report progress - only in steps
if let Some(progress) = &self.progress {
if let Some(token) = progress(state.operations) {
// Terminate script if progress returns a termination token
return EvalAltResult::ErrorTerminated(token, Position::NONE).into();
return EvalAltResult::ErrorTerminated(token, pos).into();
}
}

View File

@ -1,7 +1,7 @@
//! Module that defines the extern API of [`Engine`].
use crate::dynamic::Variant;
use crate::engine::{EvalContext, Imports};
use crate::engine::{EvalContext, Imports, State};
use crate::fn_native::{FnCallArgs, SendSync};
use crate::optimize::OptimizationLevel;
use crate::stdlib::{
@ -1368,9 +1368,9 @@ impl Engine {
scope: &mut Scope,
ast: &AST,
) -> Result<T, Box<EvalAltResult>> {
let mut mods = self.global_sub_modules.clone();
let mods = &mut self.global_sub_modules.clone();
let (result, _) = self.eval_ast_with_scope_raw(scope, &mut mods, ast)?;
let result = self.eval_ast_with_scope_raw(scope, mods, ast)?;
let typ = self.map_type_name(result.type_name());
@ -1390,8 +1390,12 @@ impl Engine {
scope: &mut Scope,
mods: &mut Imports,
ast: &'a AST,
) -> Result<(Dynamic, u64), Box<EvalAltResult>> {
self.eval_statements_raw(scope, mods, ast.statements(), &[ast.lib()])
) -> Result<Dynamic, Box<EvalAltResult>> {
let state = &mut State {
source: ast.clone_source(),
..Default::default()
};
self.eval_statements_raw(scope, mods, state, ast.statements(), &[ast.lib()])
}
/// Evaluate a file, but throw away the result and only return error (if any).
/// Useful for when you don't need the result, but still need to keep track of possible errors.
@ -1450,10 +1454,13 @@ impl Engine {
scope: &mut Scope,
ast: &AST,
) -> Result<(), Box<EvalAltResult>> {
let mut mods = self.global_sub_modules.clone();
self.eval_statements_raw(scope, &mut mods, ast.statements(), &[ast.lib()])
.map(|_| ())
let mods = &mut self.global_sub_modules.clone();
let state = &mut State {
source: ast.clone_source(),
..Default::default()
};
self.eval_statements_raw(scope, mods, state, ast.statements(), &[ast.lib()])?;
Ok(())
}
/// Call a script function defined in an [`AST`] with multiple arguments.
/// Arguments are passed as a tuple.
@ -1808,20 +1815,22 @@ impl Engine {
///
/// // Override action of 'print' function
/// let logger = result.clone();
/// engine.on_debug(move |s, pos| logger.write().unwrap().push_str(
/// &format!("{:?} > {}", pos, s)
/// ));
/// engine.on_debug(move |s, src, pos| logger.write().unwrap().push_str(
/// &format!("{} @ {:?} > {}", src.unwrap_or("unknown"), pos, s)
/// ));
///
/// engine.consume(r#"let x = "hello"; debug(x);"#)?;
/// let mut ast = engine.compile(r#"let x = "hello"; debug(x);"#)?;
/// ast.set_source(Some("world"));
/// engine.consume_ast(&ast)?;
///
/// assert_eq!(*result.read().unwrap(), r#"1:18 > "hello""#);
/// assert_eq!(*result.read().unwrap(), r#"world @ 1:18 > "hello""#);
/// # Ok(())
/// # }
/// ```
#[inline(always)]
pub fn on_debug(
&mut self,
callback: impl Fn(&str, Position) + SendSync + 'static,
callback: impl Fn(&str, Option<&str>, Position) + SendSync + 'static,
) -> &mut Self {
self.debug = Box::new(callback);
self

View File

@ -40,6 +40,12 @@ impl Engine {
pub fn optimization_level(&self) -> crate::OptimizationLevel {
self.optimization_level
}
/// Enable/disable doc-comments.
#[inline(always)]
pub fn set_doc_comments(&mut self, enable: bool) -> &mut Self {
self.disable_doc_comments = !enable;
self
}
/// Set the maximum levels of function calls allowed for a script in order to avoid
/// infinite recursion and stack overflows.
#[cfg(not(feature = "unchecked"))]

View File

@ -168,16 +168,27 @@ impl Engine {
pos: Position,
def_val: Option<&Dynamic>,
) -> Result<(Dynamic, bool), Box<EvalAltResult>> {
self.inc_operations(state)?;
self.inc_operations(state, pos)?;
// Search for the native function
// First search registered functions (can override packages)
// Then search packages
let func = //lib.get_fn(hash_fn, pub_only)
self.global_namespace.get_fn(hash_fn, pub_only)
let func = state.functions_cache.get(&hash_fn).cloned();
let func = if let Some(ref f) = func {
f.as_ref()
} else {
// Search for the native function
// First search registered functions (can override packages)
// Then search packages
// lib.get_fn(hash_fn, pub_only)
let f = self
.global_namespace
.get_fn(hash_fn, pub_only)
.or_else(|| self.packages.get_fn(hash_fn))
.or_else(|| mods.get_fn(hash_fn));
state.functions_cache.insert(hash_fn, f.cloned());
f
};
if let Some(func) = func {
assert!(func.is_native());
@ -210,20 +221,17 @@ impl Engine {
.into(),
false,
),
KEYWORD_DEBUG => (
(self.debug)(
result.as_str().map_err(|typ| {
EvalAltResult::ErrorMismatchOutputType(
self.map_type_name(type_name::<ImmutableString>()).into(),
typ.into(),
pos,
)
})?,
pos,
)
.into(),
false,
),
KEYWORD_DEBUG => {
let text = result.as_str().map_err(|typ| {
EvalAltResult::ErrorMismatchOutputType(
self.map_type_name(type_name::<ImmutableString>()).into(),
typ.into(),
pos,
)
})?;
let source = state.source.as_ref().map(|s| s.as_str());
((self.debug)(text, source, pos).into(), false)
}
_ => (result, func.is_method()),
});
}
@ -337,7 +345,7 @@ impl Engine {
pos: Position,
level: usize,
) -> Result<Dynamic, Box<EvalAltResult>> {
self.inc_operations(state)?;
self.inc_operations(state, pos)?;
// Check for stack overflow
#[cfg(not(feature = "no_function"))]
@ -370,6 +378,9 @@ impl Engine {
let mut lib_merged: StaticVec<_>;
let unified_lib = if let Some(ref env_lib) = fn_def.lib {
// If the library is modified, clear the functions lookup cache
state.functions_cache.clear();
lib_merged = Default::default();
lib_merged.push(env_lib.as_ref());
lib_merged.extend(lib.iter().cloned());
@ -380,7 +391,7 @@ impl Engine {
#[cfg(not(feature = "no_module"))]
if !fn_def.mods.is_empty() {
mods.extend(fn_def.mods.iter_raw());
mods.extend(fn_def.mods.iter_raw().map(|(n, m)| (n.clone(), m.clone())));
}
// Evaluate the function at one higher level of call depth
@ -440,19 +451,18 @@ impl Engine {
hash_script: u64,
pub_only: bool,
) -> bool {
// NOTE: We skip script functions for global_namespace and packages, and native functions for lib
// First check script-defined functions
lib.iter().any(|&m| m.contains_fn(hash_script, pub_only))
(hash_script != 0 && lib.iter().any(|&m| m.contains_fn(hash_script, pub_only)))
//|| lib.iter().any(|&m| m.contains_fn(hash_fn, pub_only))
// Then check registered functions
//|| self.global_namespace.contains_fn(hash_script, pub_only)
//|| (hash_script != 0 && self.global_namespace.contains_fn(hash_script, pub_only))
|| self.global_namespace.contains_fn(hash_fn, false)
// Then check packages
|| self.packages.contains_fn(hash_script)
|| (hash_script != 0 && self.packages.contains_fn(hash_script))
|| self.packages.contains_fn(hash_fn)
// Then check imported modules
|| mods.map(|m| m.contains_fn(hash_script) || m.contains_fn(hash_fn)).unwrap_or(false)
|| (hash_script != 0 && mods.map(|m| m.contains_fn(hash_script)).unwrap_or(false))
|| mods.map(|m| m.contains_fn(hash_fn)).unwrap_or(false)
}
/// Perform an actual function call, native Rust or scripted, taking care of special functions.
@ -518,14 +528,16 @@ impl Engine {
// Script-like function found
#[cfg(not(feature = "no_function"))]
_ if self.has_override(Some(mods), lib, 0, hash_script, pub_only) => {
_ if hash_script != 0
&& self.has_override(Some(mods), lib, 0, hash_script, pub_only) =>
{
// Get function
let func = lib
let (func, mut source) = lib
.iter()
.find_map(|&m| m.get_fn(hash_script, pub_only))
.find_map(|&m| m.get_fn(hash_script, pub_only).map(|f| (f, m.clone_id())))
//.or_else(|| self.global_namespace.get_fn(hash_script, pub_only))
.or_else(|| self.packages.get_fn(hash_script))
//.or_else(|| mods.get_fn(hash_script))
.or_else(|| self.packages.get_fn(hash_script).map(|f| (f, None)))
//.or_else(|| mods.iter().find_map(|(_, m)| m.get_qualified_fn(hash_script).map(|f| (f, m.clone_id()))))
.unwrap();
if func.is_script() {
@ -550,7 +562,10 @@ impl Engine {
let result = if _is_method {
// Method call of script function - map first argument to `this`
let (first, rest) = args.split_first_mut().unwrap();
self.call_script_fn(
mem::swap(&mut state.source, &mut source);
let result = self.call_script_fn(
scope,
mods,
state,
@ -560,17 +575,27 @@ impl Engine {
rest,
pos,
_level,
)?
);
// Restore the original source
state.source = source;
result?
} else {
// Normal call of script function
// The first argument is a reference?
let mut backup: ArgBackup = Default::default();
backup.change_first_arg_to_copy(is_ref, args);
mem::swap(&mut state.source, &mut source);
let result = self.call_script_fn(
scope, mods, state, lib, &mut None, func, args, pos, _level,
);
// Restore the original source
state.source = source;
// Restore the original reference
backup.restore_first_arg(args);
@ -609,26 +634,24 @@ impl Engine {
&self,
scope: &mut Scope,
mods: &mut Imports,
state: &mut State,
statements: impl IntoIterator<Item = &'a Stmt>,
lib: &[&Module],
) -> Result<(Dynamic, u64), Box<EvalAltResult>> {
let mut state = Default::default();
) -> Result<Dynamic, Box<EvalAltResult>> {
statements
.into_iter()
.try_fold(().into(), |_, stmt| {
self.eval_stmt(scope, mods, &mut state, lib, &mut None, stmt, 0)
self.eval_stmt(scope, mods, state, lib, &mut None, stmt, 0)
})
.or_else(|err| match *err {
EvalAltResult::Return(out, _) => Ok(out),
EvalAltResult::LoopBreak(_, _) => unreachable!(),
_ => Err(err),
})
.map(|v| (v, state.operations))
}
/// Evaluate a text string as a script - used primarily for 'eval'.
fn eval_script_expr(
/// Evaluate a text script in place - used primarily for 'eval'.
fn eval_script_expr_in_place(
&self,
scope: &mut Scope,
mods: &mut Imports,
@ -638,7 +661,7 @@ impl Engine {
pos: Position,
_level: usize,
) -> Result<Dynamic, Box<EvalAltResult>> {
self.inc_operations(state)?;
self.inc_operations(state, pos)?;
let script = script.trim();
if script.is_empty() {
@ -666,12 +689,16 @@ impl Engine {
}
// Evaluate the AST
let (result, operations) = self.eval_statements_raw(scope, mods, ast.statements(), lib)?;
let mut new_state = State {
source: state.source.clone(),
operations: state.operations,
..Default::default()
};
state.operations += operations;
self.inc_operations(state)?;
let result = self.eval_statements_raw(scope, mods, &mut new_state, ast.statements(), lib);
return Ok(result);
state.operations = new_state.operations;
result
}
/// Call a dot method.
@ -963,7 +990,8 @@ impl Engine {
self.make_type_mismatch_err::<ImmutableString>(typ, args_expr[0].position())
})?;
let pos = args_expr[0].position();
let result = self.eval_script_expr(scope, mods, state, lib, script, pos, level + 1);
let result =
self.eval_script_expr_in_place(scope, mods, state, lib, script, pos, level + 1);
// IMPORTANT! If the eval defines new variables in the current scope,
// all variable offsets from this point on will be mis-aligned.
@ -1006,8 +1034,7 @@ impl Engine {
target = target.into_owned();
}
self.inc_operations(state)
.map_err(|err| err.fill_position(pos))?;
self.inc_operations(state, pos)?;
args = if target.is_shared() || target.is_value() {
arg_values.insert(0, target.take_or_clone().flatten());
@ -1089,8 +1116,7 @@ impl Engine {
let (target, _, pos) =
self.search_scope_only(scope, mods, state, lib, this_ptr, &args_expr[0])?;
self.inc_operations(state)
.map_err(|err| err.fill_position(pos))?;
self.inc_operations(state, pos)?;
if target.is_shared() || target.is_value() {
arg_values[0] = target.take_or_clone().flatten();
@ -1119,7 +1145,7 @@ impl Engine {
let func = match module.get_qualified_fn(hash_script) {
// Then search in Rust functions
None => {
self.inc_operations(state)?;
self.inc_operations(state, pos)?;
// Namespace-qualified Rust functions are indexed in two steps:
// 1) Calculate a hash in a similar manner to script-defined functions,
@ -1149,9 +1175,17 @@ impl Engine {
let args = args.as_mut();
let new_scope = &mut Default::default();
let fn_def = f.get_fn_def().clone();
self.call_script_fn(
let mut source = module.clone_id();
mem::swap(&mut state.source, &mut source);
let result = self.call_script_fn(
new_scope, mods, state, lib, &mut None, &fn_def, args, pos, level,
)
);
state.source = source;
result
}
Some(f) if f.is_plugin_fn() => f
.get_plugin_fn()

View File

@ -360,10 +360,10 @@ pub type OnPrintCallback = Box<dyn Fn(&str) + Send + Sync + 'static>;
/// A standard callback function for debugging.
#[cfg(not(feature = "sync"))]
pub type OnDebugCallback = Box<dyn Fn(&str, Position) + 'static>;
pub type OnDebugCallback = Box<dyn Fn(&str, Option<&str>, Position) + 'static>;
/// A standard callback function for debugging.
#[cfg(feature = "sync")]
pub type OnDebugCallback = Box<dyn Fn(&str, Position) + Send + Sync + 'static>;
pub type OnDebugCallback = Box<dyn Fn(&str, Option<&str>, Position) + Send + Sync + 'static>;
/// A standard callback function for variable access.
#[cfg(not(feature = "sync"))]

View File

@ -119,6 +119,8 @@ impl FuncInfo {
/// Not available under the `no_module` feature.
#[derive(Clone)]
pub struct Module {
/// ID identifying the module.
id: Option<ImmutableString>,
/// Sub-modules.
modules: HashMap<ImmutableString, Shared<Module>>,
/// Module variables.
@ -141,6 +143,7 @@ pub struct Module {
impl Default for Module {
fn default() -> Self {
Self {
id: None,
modules: Default::default(),
variables: Default::default(),
all_variables: Default::default(),
@ -157,7 +160,12 @@ impl fmt::Debug for Module {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Module(\n modules: {}\n vars: {}\n functions: {}\n)",
"Module({}\n modules: {}\n vars: {}\n functions: {}\n)",
if let Some(ref id) = self.id {
format!("id: {:?}", id)
} else {
"".to_string()
},
self.modules
.keys()
.map(|m| m.as_str())
@ -220,6 +228,41 @@ impl Module {
}
}
/// Get the ID of the module, if any.
///
/// # Example
///
/// ```
/// use rhai::Module;
///
/// let mut module = Module::new();
/// module.set_id(Some("hello"));
/// assert_eq!(module.id(), Some("hello"));
/// ```
pub fn id(&self) -> Option<&str> {
self.id.as_ref().map(|s| s.as_str())
}
/// Get the ID of the module, if any.
pub(crate) fn clone_id(&self) -> Option<ImmutableString> {
self.id.clone()
}
/// Set the ID of the module.
///
/// # Example
///
/// ```
/// use rhai::Module;
///
/// let mut module = Module::new();
/// module.set_id(Some("hello"));
/// assert_eq!(module.id(), Some("hello"));
/// ```
pub fn set_id<S: Into<ImmutableString>>(&mut self, id: Option<S>) {
self.id = id.map(|s| s.into());
}
/// Is the module empty?
///
/// # Example
@ -1338,7 +1381,11 @@ impl Module {
/// the hash calculated by [`build_index`][Module::build_index].
#[inline]
pub fn contains_qualified_fn(&self, hash_fn: u64) -> bool {
self.all_functions.contains_key(&hash_fn)
if hash_fn == 0 {
false
} else {
self.all_functions.contains_key(&hash_fn)
}
}
/// Get a namespace-qualified function.
@ -1348,7 +1395,11 @@ impl Module {
/// the hash calculated by [`build_index`][Module::build_index].
#[inline(always)]
pub(crate) fn get_qualified_fn(&self, hash_qualified_fn: u64) -> Option<&CallableFunction> {
self.all_functions.get(&hash_qualified_fn)
if hash_qualified_fn == 0 {
None
} else {
self.all_functions.get(&hash_qualified_fn)
}
}
/// Combine another module into this module.
@ -1679,6 +1730,7 @@ impl Module {
});
}
module.set_id(ast.clone_source());
module.build_index();
Ok(module)

View File

@ -163,10 +163,11 @@ impl ModuleResolver for FileModuleResolver {
_ => Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)),
})?;
let m = Module::eval_ast_as_new(scope, &ast, engine).map_err(|err| {
let mut m = Module::eval_ast_as_new(scope, &ast, engine).map_err(|err| {
Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos))
})?;
m.set_id(Some(path));
module = Some(m.into());
module_ref = module.clone();
};

View File

@ -143,6 +143,10 @@ macro_rules! gen_signed_functions {
Ok(Dynamic::from(-x))
}
}
#[rhai_fn(name = "+")]
pub fn plus(x: $arg_type) -> $arg_type {
x
}
#[rhai_fn(return_raw)]
pub fn abs(x: $arg_type) -> Result<Dynamic, Box<EvalAltResult>> {
if cfg!(not(feature = "unchecked")) {
@ -251,6 +255,10 @@ mod f32_functions {
pub fn neg(x: f32) -> f32 {
-x
}
#[rhai_fn(name = "+")]
pub fn plus(x: f32) -> f32 {
-x
}
pub fn abs(x: f32) -> f32 {
x.abs()
}
@ -310,6 +318,10 @@ mod f64_functions {
pub fn neg(x: f64) -> f64 {
-x
}
#[rhai_fn(name = "+")]
pub fn plus(x: f64) -> f64 {
-x
}
pub fn abs(x: f64) -> f64 {
x.abs()
}

View File

@ -128,8 +128,8 @@ fn collect_fn_metadata(ctx: NativeCallContext) -> Array {
.for_each(|(_, _, _, _, f)| list.push(make_metadata(&dict, None, f).into()));
if let Some(mods) = ctx.mods {
mods.iter()
.for_each(|(ns, m)| scan_module(&mut list, &dict, ns, m.as_ref()));
mods.iter_raw()
.for_each(|(ns, m)| scan_module(&mut list, &dict, ns.clone(), m.as_ref()));
}
list

View File

@ -64,15 +64,23 @@ impl PackagesCollection {
/// Does the specified function hash key exist in the [`PackagesCollection`]?
#[allow(dead_code)]
pub fn contains_fn(&self, hash: u64) -> bool {
self.0
.as_ref()
.map_or(false, |x| x.iter().any(|p| p.contains_fn(hash, false)))
if hash == 0 {
false
} else {
self.0
.as_ref()
.map_or(false, |x| x.iter().any(|p| p.contains_fn(hash, false)))
}
}
/// Get specified function via its hash key.
pub fn get_fn(&self, hash: u64) -> Option<&CallableFunction> {
self.0
.as_ref()
.and_then(|x| x.iter().find_map(|p| p.get_fn(hash, false)))
if hash == 0 {
None
} else {
self.0
.as_ref()
.and_then(|x| x.iter().find_map(|p| p.get_fn(hash, false)))
}
}
/// Does the specified [`TypeId`] iterator exist in the [`PackagesCollection`]?
#[allow(dead_code)]

View File

@ -357,6 +357,7 @@ fn parse_fn_call(
return Ok(Expr::FnCall(
Box::new(FnCallExpr {
name: id.to_string().into(),
native_only: !is_valid_identifier(id.chars()), // script functions can only be valid identifiers
capture,
namespace,
hash: hash_script,
@ -404,6 +405,7 @@ fn parse_fn_call(
return Ok(Expr::FnCall(
Box::new(FnCallExpr {
name: id.to_string().into(),
native_only: !is_valid_identifier(id.chars()), // script functions can only be valid identifiers
capture,
namespace,
hash: hash_script,
@ -1103,6 +1105,12 @@ fn parse_primary(
(expr, Token::LeftBracket) => {
parse_index_chain(input, state, lib, expr, settings.level_up())?
}
// Method access
#[cfg(not(feature = "no_object"))]
(expr, Token::Period) => {
let rhs = parse_unary(input, state, lib, settings.level_up())?;
make_dot_expr(state, expr, rhs, token_pos)?
}
// Unknown postfix operator
(expr, token) => unreachable!(
"unknown postfix operator '{}' for {:?}",
@ -1112,20 +1120,25 @@ fn parse_primary(
}
}
// Cache the hash key for namespace-qualified variables
match &mut root_expr {
// Cache the hash key for namespace-qualified variables
Expr::Variable(x) if x.1.is_some() => {
let (_, modules, hash, IdentX { name, .. }) = x.as_mut();
let namespace = modules.as_mut().unwrap();
// Qualifiers + variable name
*hash = calc_script_fn_hash(namespace.iter().map(|v| v.name.as_str()), name, 0);
#[cfg(not(feature = "no_module"))]
namespace.set_index(state.find_module(&namespace[0].name));
}
_ => (),
Expr::Variable(x) if x.1.is_some() => Some(x),
Expr::Index(x, _) | Expr::Dot(x, _) => match &mut x.lhs {
Expr::Variable(x) if x.1.is_some() => Some(x),
_ => None,
},
_ => None,
}
.map(|x| {
let (_, modules, hash, IdentX { name, .. }) = x.as_mut();
let namespace = modules.as_mut().unwrap();
// Qualifiers + variable name
*hash = calc_script_fn_hash(namespace.iter().map(|v| v.name.as_str()), name, 0);
#[cfg(not(feature = "no_module"))]
namespace.set_index(state.find_module(&namespace[0].name));
});
// Make sure identifiers are valid
Ok(root_expr)
@ -1199,8 +1212,33 @@ fn parse_unary(
}
// +expr
Token::UnaryPlus => {
eat_token(input, Token::UnaryPlus);
parse_unary(input, state, lib, settings.level_up())
let pos = eat_token(input, Token::UnaryPlus);
match parse_unary(input, state, lib, settings.level_up())? {
expr @ Expr::IntegerConstant(_, _) => Ok(expr),
#[cfg(not(feature = "no_float"))]
expr @ Expr::FloatConstant(_, _) => Ok(expr),
// Call plus function
expr => {
let op = "+";
let hash = calc_script_fn_hash(empty(), op, 1);
let mut args = StaticVec::new();
args.push(expr);
Ok(Expr::FnCall(
Box::new(FnCallExpr {
name: op.into(),
native_only: true,
namespace: None,
hash,
args,
..Default::default()
}),
pos,
))
}
}
}
// !expr
Token::Bang => {
@ -1768,6 +1806,7 @@ fn parse_binary_op(
make_in_expr(current_lhs, rhs, pos)?
}
// This is needed to parse closure followed by a dot.
#[cfg(not(feature = "no_object"))]
Token::Period => {
let rhs = args.pop().unwrap();

257
src/serde_impl/metadata.rs Normal file
View File

@ -0,0 +1,257 @@
use crate::stdlib::{
cmp::Ordering,
collections::BTreeMap,
string::{String, ToString},
vec,
vec::Vec,
};
use crate::{Engine, AST};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
enum FnType {
Script,
Native,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
enum FnNamespace {
Global,
Internal,
}
impl From<crate::FnNamespace> for FnNamespace {
fn from(value: crate::FnNamespace) -> Self {
match value {
crate::FnNamespace::Global => Self::Global,
crate::FnNamespace::Internal => Self::Internal,
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
enum FnAccess {
Public,
Private,
}
impl From<crate::FnAccess> for FnAccess {
fn from(value: crate::FnAccess) -> Self {
match value {
crate::FnAccess::Public => Self::Public,
crate::FnAccess::Private => Self::Private,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct FnParam {
pub name: String,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub typ: Option<String>,
}
impl PartialOrd for FnParam {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(match self.name.partial_cmp(&other.name).unwrap() {
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
Ordering::Equal => match (self.typ.is_none(), other.typ.is_none()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(false, false) => self
.typ
.as_ref()
.unwrap()
.partial_cmp(other.typ.as_ref().unwrap())
.unwrap(),
},
})
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct FnMetadata {
pub namespace: FnNamespace,
pub access: FnAccess,
pub name: String,
#[serde(rename = "type")]
pub typ: FnType,
pub num_params: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub params: Vec<FnParam>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub return_type: Option<String>,
pub signature: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc_comments: Option<Vec<String>>,
}
impl PartialOrd for FnMetadata {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(match self.name.partial_cmp(&other.name).unwrap() {
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
Ordering::Equal => match self.num_params.partial_cmp(&other.num_params).unwrap() {
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
Ordering::Equal => self.params.partial_cmp(&other.params).unwrap(),
},
})
}
}
impl Ord for FnMetadata {
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap()
}
}
impl From<&crate::module::FuncInfo> for FnMetadata {
fn from(info: &crate::module::FuncInfo) -> Self {
Self {
namespace: info.namespace.into(),
access: info.access.into(),
name: info.name.to_string(),
typ: if info.func.is_script() {
FnType::Script
} else {
FnType::Native
},
num_params: info.params,
params: if let Some(ref names) = info.param_names {
names
.iter()
.take(info.params)
.map(|s| {
let mut seg = s.splitn(2, ':');
let name = seg
.next()
.map(|s| s.trim().to_string())
.unwrap_or("_".to_string());
let typ = seg.next().map(|s| s.trim().to_string());
FnParam { name, typ }
})
.collect()
} else {
vec![]
},
return_type: if let Some(ref names) = info.param_names {
names
.last()
.map(|s| s.to_string())
.or_else(|| Some("()".to_string()))
} else {
None
},
signature: info.gen_signature(),
doc_comments: if info.func.is_script() {
Some(info.func.get_fn_def().comments.clone())
} else {
None
},
}
}
}
impl From<crate::ScriptFnMetadata<'_>> for FnMetadata {
fn from(info: crate::ScriptFnMetadata) -> Self {
Self {
namespace: FnNamespace::Global,
access: info.access.into(),
name: info.name.to_string(),
typ: FnType::Script,
num_params: info.params.len(),
params: info
.params
.iter()
.map(|s| FnParam {
name: s.to_string(),
typ: Some("Dynamic".to_string()),
})
.collect(),
return_type: Some("Dynamic".to_string()),
signature: info.to_string(),
doc_comments: if info.comments.is_empty() {
None
} else {
Some(info.comments.iter().map(|s| s.to_string()).collect())
},
}
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
struct ModuleMetadata {
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub modules: BTreeMap<String, Self>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub functions: Vec<FnMetadata>,
}
impl From<&crate::Module> for ModuleMetadata {
fn from(module: &crate::Module) -> Self {
let mut functions: Vec<_> = module.iter_fn().map(|f| f.into()).collect();
functions.sort();
Self {
modules: module
.iter_sub_modules()
.map(|(name, m)| (name.to_string(), m.as_ref().into()))
.collect(),
functions,
}
}
}
#[cfg(feature = "serde")]
impl Engine {
/// Generate a list of all functions (including those defined in an [`AST`][crate::AST], if provided)
/// in JSON format. Available only under the `metadata` feature.
///
/// Functions from the following sources are included:
/// 1) Functions defined in an [`AST`][crate::AST] (if provided)
/// 2) Functions registered into the global namespace
/// 3) Functions in registered sub-modules
/// 4) Functions in packages (optional)
pub fn gen_fn_metadata_to_json(
&self,
ast: Option<&AST>,
include_packages: bool,
) -> serde_json::Result<String> {
let mut global: ModuleMetadata = Default::default();
if include_packages {
self.packages
.iter()
.flat_map(|m| m.iter_fn().map(|f| f.into()))
.for_each(|info| global.functions.push(info));
}
self.global_sub_modules.iter().for_each(|(name, m)| {
global.modules.insert(name.to_string(), m.as_ref().into());
});
self.global_namespace
.iter_fn()
.map(|f| f.into())
.for_each(|info| global.functions.push(info));
if let Some(ast) = ast {
ast.iter_functions()
.map(|f| f.into())
.for_each(|info| global.functions.push(info));
}
global.functions.sort();
serde_json::to_string_pretty(&global)
}
}

View File

@ -3,3 +3,6 @@
pub mod de;
pub mod ser;
mod str;
#[cfg(feature = "metadata")]
pub mod metadata;

View File

@ -354,9 +354,7 @@ impl Token {
Reserved(s) => s.clone().into(),
Custom(s) => s.clone().into(),
LexError(err) => err.to_string().into(),
Comment(s) if is_doc_comment(s) => s[..3].to_string().into(),
Comment(s) => s[..2].to_string().into(),
Comment(s) => s.clone().into(),
token => match token {
LeftBrace => "{",
@ -759,6 +757,8 @@ pub struct TokenizeState {
pub end_with_none: bool,
/// Include comments?
pub include_comments: bool,
/// Disable doc-comments?
pub disable_doc_comments: bool,
}
/// _(INTERNALS)_ Trait that encapsulates a peekable character input stream.
@ -1020,7 +1020,8 @@ fn is_binary_char(c: char) -> bool {
/// Test if the comment block is a doc-comment.
#[inline(always)]
pub fn is_doc_comment(comment: &str) -> bool {
comment.starts_with("///") || comment.starts_with("/**")
(comment.starts_with("///") && !comment.starts_with("////"))
|| (comment.starts_with("/**") && !comment.starts_with("/***"))
}
/// Get the next token.
@ -1040,7 +1041,9 @@ fn get_next_token_inner(
state.comment_level = scan_block_comment(stream, state.comment_level, pos, &mut comment);
if state.include_comments || is_doc_comment(comment.as_ref().unwrap()) {
if state.include_comments
|| (!state.disable_doc_comments && is_doc_comment(comment.as_ref().unwrap()))
{
return Some((Token::Comment(comment.unwrap()), start_pos));
}
}
@ -1288,9 +1291,14 @@ fn get_next_token_inner(
eat_next(stream, pos);
let mut comment = match stream.peek_next() {
Some('/') => {
Some('/') if !state.disable_doc_comments => {
eat_next(stream, pos);
Some("///".to_string())
// Long streams of `///...` are not doc-comments
match stream.peek_next() {
Some('/') => None,
_ => Some("///".to_string()),
}
}
_ if state.include_comments => Some("//".to_string()),
_ => None,
@ -1316,9 +1324,14 @@ fn get_next_token_inner(
eat_next(stream, pos);
let mut comment = match stream.peek_next() {
Some('*') => {
Some('*') if !state.disable_doc_comments => {
eat_next(stream, pos);
Some("/**".to_string())
// Long streams of `/****...` are not doc-comments
match stream.peek_next() {
Some('*') => None,
_ => Some("/**".to_string()),
}
}
_ if state.include_comments => Some("/*".to_string()),
_ => None,
@ -1785,6 +1798,7 @@ impl Engine {
comment_level: 0,
end_with_none: false,
include_comments: false,
disable_doc_comments: self.disable_doc_comments,
},
pos: Position::new(1, 0),
stream: MultiInputsStream {

View File

@ -29,7 +29,7 @@ fn test_comments() -> Result<(), Box<EvalAltResult>> {
#[cfg(not(feature = "no_function"))]
#[test]
fn test_comments_doc() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
let mut engine = Engine::new();
let ast = engine.compile(
r"
@ -54,6 +54,16 @@ fn test_comments_doc() -> Result<(), Box<EvalAltResult>> {
)
.is_err());
engine.compile(
r"
///////////////
let x = 42;
/***************/
let x = 42;
",
)?;
let ast = engine.compile(
r"
/** Hello world
@ -78,5 +88,17 @@ fn test_comments_doc() -> Result<(), Box<EvalAltResult>> {
)
.is_err());
engine.set_doc_comments(false);
engine.compile(
r"
/// Hello world!
let x = 42;
/** Hello world! */
let x = 42;
",
)?;
Ok(())
}

View File

@ -48,6 +48,7 @@ fn test_module_sub_module() -> Result<(), Box<EvalAltResult>> {
assert_eq!(engine.eval::<INT>("question::MYSTIC_NUMBER")?, 42);
assert!(engine.eval::<INT>("MYSTIC_NUMBER").is_err());
assert_eq!(engine.eval::<INT>("question::life::universe::answer")?, 41);
assert_eq!(
engine.eval::<INT>("question::life::universe::answer + 1")?,
42
@ -60,6 +61,8 @@ fn test_module_sub_module() -> Result<(), Box<EvalAltResult>> {
.eval::<INT>("inc(question::life::universe::answer)")
.is_err());
#[cfg(not(feature = "no_object"))]
assert_eq!(engine.eval::<INT>("question::MYSTIC_NUMBER.doubled")?, 84);
#[cfg(not(feature = "no_object"))]
assert_eq!(
engine.eval::<INT>("question::life::universe::answer.doubled")?,
82

View File

@ -55,17 +55,20 @@ fn test_optimizer_parse() -> Result<(), Box<EvalAltResult>> {
let ast = engine.compile("{ const DECISION = false; if DECISION { 42 } else { 123 } }")?;
assert!(format!("{:?}", ast).starts_with(r#"AST { statements: [Block([Const(IdentX { name: "DECISION", pos: 1:9 }, Some(Unit(0:0)), false, 1:3), Expr(IntegerConstant(123, 1:53))], 1:1)]"#));
assert!(format!("{:?}", ast).starts_with(r#"AST { source: None, statements: [Block([Const(IdentX { name: "DECISION", pos: 1:9 }, Some(Unit(0:0)), false, 1:3), Expr(IntegerConstant(123, 1:53))], 1:1)]"#));
let ast = engine.compile("if 1 == 2 { 42 }")?;
assert!(format!("{:?}", ast).starts_with("AST { statements: [], functions: Module("));
assert!(
format!("{:?}", ast).starts_with("AST { source: None, statements: [], functions: Module(")
);
engine.set_optimization_level(OptimizationLevel::Full);
let ast = engine.compile("abs(-42)")?;
assert!(format!("{:?}", ast).starts_with(r"AST { statements: [Expr(IntegerConstant(42, 1:1))]"));
assert!(format!("{:?}", ast)
.starts_with(r"AST { source: None, statements: [Expr(IntegerConstant(42, 1:1))]"));
Ok(())
}

View File

@ -13,20 +13,28 @@ fn test_print_debug() -> Result<(), Box<EvalAltResult>> {
engine
.on_print(move |s| log1.write().unwrap().push(format!("entry: {}", s)))
.on_debug(move |s, pos| {
log2.write()
.unwrap()
.push(format!("DEBUG at {:?}: {}", pos, s))
.on_debug(move |s, src, pos| {
log2.write().unwrap().push(format!(
"DEBUG of {} at {:?}: {}",
src.unwrap_or("unknown"),
pos,
s
))
});
// Evaluate script
engine.eval::<()>("print(40 + 2)")?;
engine.eval::<()>(r#"let x = "hello!"; debug(x)"#)?;
engine.consume("print(40 + 2)")?;
let mut ast = engine.compile(r#"let x = "hello!"; debug(x)"#)?;
ast.set_source(Some("world"));
engine.consume_ast(&ast)?;
// 'logbook' captures all the 'print' and 'debug' output
assert_eq!(logbook.read().unwrap().len(), 2);
assert_eq!(logbook.read().unwrap()[0], "entry: 42");
assert_eq!(logbook.read().unwrap()[1], r#"DEBUG at 1:19: "hello!""#);
assert_eq!(
logbook.read().unwrap()[1],
r#"DEBUG of world at 1:19: "hello!""#
);
for entry in logbook.read().unwrap().iter() {
println!("{}", entry);