Merge pull request #311 from schungx/master

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

View File

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

View File

@ -10,9 +10,14 @@ Each function defined in an `AST` can optionally attach _doc-comments_ (which, a
are comments prefixed by either `///` or `/**`). Doc-comments allow third-party tools to are comments prefixed by either `///` or `/**`). Doc-comments allow third-party tools to
automatically generate documentation for functions defined in a Rhai script. 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 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 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. * 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`. * 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`. * `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`. * `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. * 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 `/**`. * `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 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. * Capturing a constant variable in a closure is now supported, with no cloning.
* Provides position info for `debug` statements. * 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. * A _look-ahead_ symbol is provided to custom syntax parsers, which can be used to parse variable-length symbol streams.

View File

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

View File

@ -27,7 +27,6 @@ Fast
* Fairly low compile-time overhead. * Fairly low compile-time overhead.
* Fairly efficient evaluation (1 million iterations in 0.3 sec on a single core, 2.3 GHz Linux VM). * 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. * Scripts are [optimized][script optimization] (useful for template-based machine-generated scripts) for repeated evaluations.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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. more control over what a script can (or cannot) do.
| Feature | Additive? | Description | | 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! | | `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` | | `sync` | no | restricts all values types to those that are `Send + Sync`. Under this feature, all Rhai types, including [`Engine`], [`Scope`] and [`AST`], are all `Send + Sync` |
| `no_optimize` | no | disables [script optimization] | | `no_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` | | `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_index` | no | disables [arrays] and indexing features |
| `no_object` | no | disables support for [custom types] and [object maps] | | `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_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_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 | | `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 | | `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 | | `internals` | yes | exposes internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version |

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
//! Module that defines the extern API of [`Engine`]. //! Module that defines the extern API of [`Engine`].
use crate::dynamic::Variant; use crate::dynamic::Variant;
use crate::engine::{EvalContext, Imports}; use crate::engine::{EvalContext, Imports, State};
use crate::fn_native::{FnCallArgs, SendSync}; use crate::fn_native::{FnCallArgs, SendSync};
use crate::optimize::OptimizationLevel; use crate::optimize::OptimizationLevel;
use crate::stdlib::{ use crate::stdlib::{
@ -1368,9 +1368,9 @@ impl Engine {
scope: &mut Scope, scope: &mut Scope,
ast: &AST, ast: &AST,
) -> Result<T, Box<EvalAltResult>> { ) -> 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()); let typ = self.map_type_name(result.type_name());
@ -1390,8 +1390,12 @@ impl Engine {
scope: &mut Scope, scope: &mut Scope,
mods: &mut Imports, mods: &mut Imports,
ast: &'a AST, ast: &'a AST,
) -> Result<(Dynamic, u64), Box<EvalAltResult>> { ) -> Result<Dynamic, Box<EvalAltResult>> {
self.eval_statements_raw(scope, mods, ast.statements(), &[ast.lib()]) 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). /// 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. /// 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, scope: &mut Scope,
ast: &AST, ast: &AST,
) -> Result<(), Box<EvalAltResult>> { ) -> Result<(), Box<EvalAltResult>> {
let mut mods = self.global_sub_modules.clone(); let mods = &mut self.global_sub_modules.clone();
let state = &mut State {
self.eval_statements_raw(scope, &mut mods, ast.statements(), &[ast.lib()]) source: ast.clone_source(),
.map(|_| ()) ..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. /// Call a script function defined in an [`AST`] with multiple arguments.
/// Arguments are passed as a tuple. /// Arguments are passed as a tuple.
@ -1808,20 +1815,22 @@ impl Engine {
/// ///
/// // Override action of 'print' function /// // Override action of 'print' function
/// let logger = result.clone(); /// let logger = result.clone();
/// engine.on_debug(move |s, pos| logger.write().unwrap().push_str( /// engine.on_debug(move |s, src, pos| logger.write().unwrap().push_str(
/// &format!("{:?} > {}", pos, s) /// &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(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
#[inline(always)] #[inline(always)]
pub fn on_debug( pub fn on_debug(
&mut self, &mut self,
callback: impl Fn(&str, Position) + SendSync + 'static, callback: impl Fn(&str, Option<&str>, Position) + SendSync + 'static,
) -> &mut Self { ) -> &mut Self {
self.debug = Box::new(callback); self.debug = Box::new(callback);
self self

View File

@ -40,6 +40,12 @@ impl Engine {
pub fn optimization_level(&self) -> crate::OptimizationLevel { pub fn optimization_level(&self) -> crate::OptimizationLevel {
self.optimization_level 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 /// Set the maximum levels of function calls allowed for a script in order to avoid
/// infinite recursion and stack overflows. /// infinite recursion and stack overflows.
#[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "unchecked"))]

View File

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

View File

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

View File

@ -119,6 +119,8 @@ impl FuncInfo {
/// Not available under the `no_module` feature. /// Not available under the `no_module` feature.
#[derive(Clone)] #[derive(Clone)]
pub struct Module { pub struct Module {
/// ID identifying the module.
id: Option<ImmutableString>,
/// Sub-modules. /// Sub-modules.
modules: HashMap<ImmutableString, Shared<Module>>, modules: HashMap<ImmutableString, Shared<Module>>,
/// Module variables. /// Module variables.
@ -141,6 +143,7 @@ pub struct Module {
impl Default for Module { impl Default for Module {
fn default() -> Self { fn default() -> Self {
Self { Self {
id: None,
modules: Default::default(), modules: Default::default(),
variables: Default::default(), variables: Default::default(),
all_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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!( write!(
f, 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 self.modules
.keys() .keys()
.map(|m| m.as_str()) .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? /// Is the module empty?
/// ///
/// # Example /// # Example
@ -1338,8 +1381,12 @@ impl Module {
/// the hash calculated by [`build_index`][Module::build_index]. /// the hash calculated by [`build_index`][Module::build_index].
#[inline] #[inline]
pub fn contains_qualified_fn(&self, hash_fn: u64) -> bool { pub fn contains_qualified_fn(&self, hash_fn: u64) -> bool {
if hash_fn == 0 {
false
} else {
self.all_functions.contains_key(&hash_fn) self.all_functions.contains_key(&hash_fn)
} }
}
/// Get a namespace-qualified function. /// Get a namespace-qualified function.
/// Name and Position in `EvalAltResult` are None and must be set afterwards. /// 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]. /// the hash calculated by [`build_index`][Module::build_index].
#[inline(always)] #[inline(always)]
pub(crate) fn get_qualified_fn(&self, hash_qualified_fn: u64) -> Option<&CallableFunction> { 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) self.all_functions.get(&hash_qualified_fn)
} }
}
/// Combine another module into this module. /// Combine another module into this module.
/// The other module is consumed to merge 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(); module.build_index();
Ok(module) Ok(module)

View File

@ -163,10 +163,11 @@ impl ModuleResolver for FileModuleResolver {
_ => Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)), _ => 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)) Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos))
})?; })?;
m.set_id(Some(path));
module = Some(m.into()); module = Some(m.into());
module_ref = module.clone(); module_ref = module.clone();
}; };

View File

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

View File

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

View File

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

View File

@ -357,6 +357,7 @@ fn parse_fn_call(
return Ok(Expr::FnCall( return Ok(Expr::FnCall(
Box::new(FnCallExpr { Box::new(FnCallExpr {
name: id.to_string().into(), name: id.to_string().into(),
native_only: !is_valid_identifier(id.chars()), // script functions can only be valid identifiers
capture, capture,
namespace, namespace,
hash: hash_script, hash: hash_script,
@ -404,6 +405,7 @@ fn parse_fn_call(
return Ok(Expr::FnCall( return Ok(Expr::FnCall(
Box::new(FnCallExpr { Box::new(FnCallExpr {
name: id.to_string().into(), name: id.to_string().into(),
native_only: !is_valid_identifier(id.chars()), // script functions can only be valid identifiers
capture, capture,
namespace, namespace,
hash: hash_script, hash: hash_script,
@ -1103,6 +1105,12 @@ fn parse_primary(
(expr, Token::LeftBracket) => { (expr, Token::LeftBracket) => {
parse_index_chain(input, state, lib, expr, settings.level_up())? 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 // Unknown postfix operator
(expr, token) => unreachable!( (expr, token) => unreachable!(
"unknown postfix operator '{}' for {:?}", "unknown postfix operator '{}' for {:?}",
@ -1112,9 +1120,16 @@ fn parse_primary(
} }
} }
match &mut root_expr {
// Cache the hash key for namespace-qualified variables // 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 (_, modules, hash, IdentX { name, .. }) = x.as_mut();
let namespace = modules.as_mut().unwrap(); let namespace = modules.as_mut().unwrap();
@ -1123,9 +1138,7 @@ fn parse_primary(
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
namespace.set_index(state.find_module(&namespace[0].name)); namespace.set_index(state.find_module(&namespace[0].name));
} });
_ => (),
}
// Make sure identifiers are valid // Make sure identifiers are valid
Ok(root_expr) Ok(root_expr)
@ -1199,8 +1212,33 @@ fn parse_unary(
} }
// +expr // +expr
Token::UnaryPlus => { Token::UnaryPlus => {
eat_token(input, Token::UnaryPlus); let pos = eat_token(input, Token::UnaryPlus);
parse_unary(input, state, lib, settings.level_up())
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 // !expr
Token::Bang => { Token::Bang => {
@ -1768,6 +1806,7 @@ fn parse_binary_op(
make_in_expr(current_lhs, rhs, pos)? make_in_expr(current_lhs, rhs, pos)?
} }
// This is needed to parse closure followed by a dot.
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
Token::Period => { Token::Period => {
let rhs = args.pop().unwrap(); let rhs = args.pop().unwrap();

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,17 +55,20 @@ fn test_optimizer_parse() -> Result<(), Box<EvalAltResult>> {
let ast = engine.compile("{ const DECISION = false; if DECISION { 42 } else { 123 } }")?; 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 }")?; 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); engine.set_optimization_level(OptimizationLevel::Full);
let ast = engine.compile("abs(-42)")?; 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(()) Ok(())
} }

View File

@ -13,20 +13,28 @@ fn test_print_debug() -> Result<(), Box<EvalAltResult>> {
engine engine
.on_print(move |s| log1.write().unwrap().push(format!("entry: {}", s))) .on_print(move |s| log1.write().unwrap().push(format!("entry: {}", s)))
.on_debug(move |s, pos| { .on_debug(move |s, src, pos| {
log2.write() log2.write().unwrap().push(format!(
.unwrap() "DEBUG of {} at {:?}: {}",
.push(format!("DEBUG at {:?}: {}", pos, s)) src.unwrap_or("unknown"),
pos,
s
))
}); });
// Evaluate script // Evaluate script
engine.eval::<()>("print(40 + 2)")?; engine.consume("print(40 + 2)")?;
engine.eval::<()>(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)?;
// 'logbook' captures all the 'print' and 'debug' output // 'logbook' captures all the 'print' and 'debug' output
assert_eq!(logbook.read().unwrap().len(), 2); assert_eq!(logbook.read().unwrap().len(), 2);
assert_eq!(logbook.read().unwrap()[0], "entry: 42"); 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() { for entry in logbook.read().unwrap().iter() {
println!("{}", entry); println!("{}", entry);