Merge pull request #311 from schungx/master
A bunch of new features and bug fixes.
This commit is contained in:
commit
b1e8f52135
@ -42,6 +42,7 @@ no_closure = [] # no automatic sharing and capture of anonymous functions to
|
||||
no_module = [] # no modules
|
||||
internals = [] # expose internal data structures
|
||||
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
|
||||
|
10
RELEASES.md
10
RELEASES.md
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
106
doc/src/engine/metadata/export_to_json.md
Normal file
106
doc/src/engine/metadata/export_to_json.md
Normal 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 */",
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
@ -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`
|
28
doc/src/engine/metadata/index.md
Normal file
28
doc/src/engine/metadata/index.md
Normal 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`
|
@ -6,13 +6,14 @@ 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]. |
|
||||
| ------------------------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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] |
|
||||
|
@ -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.
|
||||
|
76
doc/src/language/doc-comments.md
Normal file
76
doc/src/language/doc-comments.md
Normal 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.
|
@ -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,8 +37,8 @@ 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
|
||||
@ -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_.
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
```
|
||||
|
@ -12,7 +12,7 @@ Excluding unneeded functionalities can result in smaller, faster builds as well
|
||||
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] |
|
||||
@ -22,12 +22,13 @@ more control over what a script can (or cannot) do.
|
||||
| `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_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`. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies |
|
||||
| `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 |
|
||||
|
||||
|
||||
|
@ -147,6 +147,15 @@ fn main() {
|
||||
println!();
|
||||
continue;
|
||||
}
|
||||
// "json" => {
|
||||
// println!(
|
||||
// "{}",
|
||||
// engine
|
||||
// .gen_fn_metadata_to_json(Some(&main_ast), false)
|
||||
// .unwrap()
|
||||
// );
|
||||
// continue;
|
||||
// }
|
||||
_ => (),
|
||||
}
|
||||
|
||||
|
48
src/ast.rs
48
src/ast.rs
@ -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,11 +477,21 @@ 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);
|
||||
|
||||
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.
|
||||
///
|
||||
/// Statements in the second [`AST`] are simply appended to the end of the first _without any processing_.
|
||||
@ -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,
|
||||
},
|
||||
|
124
src/engine.rs
124
src/engine.rs
@ -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,16 +127,24 @@ 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 {
|
||||
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> {
|
||||
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)]
|
||||
pub fn contains_iter(&self, id: TypeId) -> bool {
|
||||
@ -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"))]
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"))]
|
||||
|
128
src/fn_call.rs
128
src/fn_call.rs
@ -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)?;
|
||||
|
||||
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
|
||||
let func = //lib.get_fn(hash_fn, pub_only)
|
||||
self.global_namespace.get_fn(hash_fn, pub_only)
|
||||
// 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| {
|
||||
KEYWORD_DEBUG => {
|
||||
let text = result.as_str().map_err(|typ| {
|
||||
EvalAltResult::ErrorMismatchOutputType(
|
||||
self.map_type_name(type_name::<ImmutableString>()).into(),
|
||||
typ.into(),
|
||||
pos,
|
||||
)
|
||||
})?,
|
||||
pos,
|
||||
)
|
||||
.into(),
|
||||
false,
|
||||
),
|
||||
})?;
|
||||
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()
|
||||
|
@ -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"))]
|
||||
|
@ -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,8 +1381,12 @@ impl Module {
|
||||
/// the hash calculated by [`build_index`][Module::build_index].
|
||||
#[inline]
|
||||
pub fn contains_qualified_fn(&self, hash_fn: u64) -> bool {
|
||||
if hash_fn == 0 {
|
||||
false
|
||||
} else {
|
||||
self.all_functions.contains_key(&hash_fn)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a namespace-qualified function.
|
||||
/// Name and Position in `EvalAltResult` are None and must be set afterwards.
|
||||
@ -1348,8 +1395,12 @@ 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> {
|
||||
if hash_qualified_fn == 0 {
|
||||
None
|
||||
} else {
|
||||
self.all_functions.get(&hash_qualified_fn)
|
||||
}
|
||||
}
|
||||
|
||||
/// Combine another module into this module.
|
||||
/// The other module is consumed to merge into this module.
|
||||
@ -1679,6 +1730,7 @@ impl Module {
|
||||
});
|
||||
}
|
||||
|
||||
module.set_id(ast.clone_source());
|
||||
module.build_index();
|
||||
|
||||
Ok(module)
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -64,16 +64,24 @@ impl PackagesCollection {
|
||||
/// Does the specified function hash key exist in the [`PackagesCollection`]?
|
||||
#[allow(dead_code)]
|
||||
pub fn contains_fn(&self, hash: u64) -> bool {
|
||||
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> {
|
||||
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)]
|
||||
pub fn contains_iter(&self, id: TypeId) -> bool {
|
||||
|
@ -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,9 +1120,16 @@ fn parse_primary(
|
||||
}
|
||||
}
|
||||
|
||||
match &mut root_expr {
|
||||
// Cache the hash key for namespace-qualified variables
|
||||
Expr::Variable(x) if x.1.is_some() => {
|
||||
match &mut root_expr {
|
||||
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();
|
||||
|
||||
@ -1123,9 +1138,7 @@ fn parse_primary(
|
||||
|
||||
#[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
257
src/serde_impl/metadata.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -3,3 +3,6 @@
|
||||
pub mod de;
|
||||
pub mod ser;
|
||||
mod str;
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
pub mod metadata;
|
||||
|
32
src/token.rs
32
src/token.rs
@ -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 {
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user