diff --git a/Cargo.toml b/Cargo.toml index 81cbe637..b785865d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,8 @@ no_function = [ "no_closure" ] # no script-defined functions (meaning no closur no_closure = [] # no automatic sharing and capture of anonymous functions to external variables no_module = [] # no modules internals = [] # expose internal data structures -unicode-xid-ident = ["unicode-xid"] # allow Unicode Standard Annex #31 for identifiers. +unicode-xid-ident = ["unicode-xid"] # allow Unicode Standard Annex #31 for identifiers. +metadata = [ "serde", "serde_json"] # enables exporting functions metadata to JSON # compiling for no-std no_std = [ "smallvec/union", "num-traits/libm", "hashbrown", "core-error", "libm", "ahash" ] @@ -86,6 +87,12 @@ default_features = false features = ["derive", "alloc"] optional = true +[dependencies.serde_json] +version = "1.0.60" +default_features = false +features = ["alloc"] +optional = true + [dependencies.unicode-xid] version = "0.2.1" default_features = false diff --git a/RELEASES.md b/RELEASES.md index bc08d9dc..012706d0 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -10,9 +10,14 @@ Each function defined in an `AST` can optionally attach _doc-comments_ (which, a are comments prefixed by either `///` or `/**`). Doc-comments allow third-party tools to automatically generate documentation for functions defined in a Rhai script. +A new API, `Engine::gen_fn_metadata_to_json`, paired with the new `metadata` feature, +exports the full list of functions metadata (including those in an `AST`) as a JSON document. + Bug fixes --------- +* Unary prefix operators `-`, `+` and `!` now bind correctly when applied to an expression. Previously, `-x.len` is parsed as `(-x).len` which is obviously counter-intuitive. +* Indexing of namespace-qualified variables now work properly, such as `path::to::var[x]`. * Constants are no longer propagated by the optimizer if shadowed by a non-constant variable. * Constants passed as the `this` parameter to Rhai functions now throws an error if assigned to. * Generic type parameter of `Engine::register_iterator` is `IntoIterator` instead of `Iterator`. @@ -22,7 +27,7 @@ Breaking changes ---------------- * `Engine::on_progress` now takes `u64` instead of `&u64`. -* The closure for `Engine::on_debug` now takes an additional `Position` parameter. +* The closure for `Engine::on_debug` now takes two additional parameters: `source: Option<&str>` and `pos: Position`. * `AST::iter_functions` now returns `ScriptFnMetadata`. * The parser function passed to `Engine::register_custom_syntax_raw` now takes an additional parameter containing the _look-ahead_ symbol. @@ -30,10 +35,13 @@ New features ------------ * `AST::iter_functions` now returns `ScriptFnMetadata` which includes, among others, _doc-comments_ for functions prefixed by `///` or `/**`. +* _Doc-comments_ can be enabled/disabled with the new `Engine::set_doc_comments` method. +* A new feature `metadata` is added that pulls in `serde_json` and enables `Engine::gen_fn_metadata_to_json` which exports the full list of functions metadata (including those inside an `AST`) in JSON format. Enhancements ------------ +* A functions lookup cache is added to make function call resolution faster. * Capturing a constant variable in a closure is now supported, with no cloning. * Provides position info for `debug` statements. * A _look-ahead_ symbol is provided to custom syntax parsers, which can be used to parse variable-length symbol streams. diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 556a60fb..a0a76d3c 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -55,6 +55,7 @@ The Rhai Scripting Language 2. [Export a Rust Function](plugins/function.md) 5. [Rhai Language Reference](language/index.md) 1. [Comments](language/comments.md) + 1. [Doc-Comments](language/doc-comments.md) 2. [Values and Types](language/values-and-types.md) 1. [Dynamic Values](language/dynamic.md) 2. [Serialization/Deserialization with `serde`](rust/serde.md) @@ -79,14 +80,14 @@ The Rhai Scripting Language 9. [If Statement](language/if.md) 10. [Switch Expression](language/switch.md) 11. [While Loop](language/while.md) - 11. [Do Loop](language/do.md) - 12. [Loop Statement](language/loop.md) - 13. [For Loop](language/for.md) + 12. [Do Loop](language/do.md) + 13. [Loop Statement](language/loop.md) + 14. [For Loop](language/for.md) 1. [Iterators for Custom Types](language/iterator.md) - 14. [Return Values](language/return.md) - 15. [Throw Exception on Error](language/throw.md) - 16. [Catch Exceptions](language/try-catch.md) - 17. [Functions](language/functions.md) + 15. [Return Values](language/return.md) + 16. [Throw Exception on Error](language/throw.md) + 17. [Catch Exceptions](language/try-catch.md) + 18. [Functions](language/functions.md) 1. [Call Method as Function](language/method.md) 2. [Overloading](language/overload.md) 3. [Namespaces](language/fn-namespaces.md) @@ -94,11 +95,11 @@ The Rhai Scripting Language 5. [Currying](language/fn-curry.md) 6. [Anonymous Functions](language/fn-anon.md) 7. [Closures](language/fn-closure.md) - 18. [Print and Debug](language/print-debug.md) - 19. [Modules](language/modules/index.md) + 19. [Print and Debug](language/print-debug.md) + 20. [Modules](language/modules/index.md) 1. [Export Variables, Functions and Sub-Modules](language/modules/export.md) 2. [Import Modules](language/modules/import.md) - 20. [Eval Function](language/eval.md) + 21. [Eval Function](language/eval.md) 6. [Safety and Protection](safety/index.md) 1. [Checked Arithmetic](safety/checked.md) 2. [Sand-Boxing](safety/sandbox.md) @@ -136,7 +137,9 @@ The Rhai Scripting Language 2. [Custom Operators](engine/custom-op.md) 3. [Extending with Custom Syntax](engine/custom-syntax.md) 5. [Multiple Instantiation](patterns/multiple.md) - 6. [Get Function Signatures](engine/get_fn_sig.md) + 6. [Functions Metadata](engine/metadata/index.md) + 1. [Generate Function Signatures](engine/metadata/gen_fn_sig.md) + 2. [Export Metadata to JSON](engine/metadata/export_to_json.md) 10. [Appendix](appendix/index.md) 1. [Keywords](appendix/keywords.md) 2. [Operators and Symbols](appendix/operators.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 1f5ca2ef..82bfc65a 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -27,7 +27,6 @@ Fast * Fairly low compile-time overhead. * Fairly efficient evaluation (1 million iterations in 0.3 sec on a single core, 2.3 GHz Linux VM). - An unofficial Fibonacci benchmark puts Rhai somewhere between Wren and Python. * Scripts are [optimized][script optimization] (useful for template-based machine-generated scripts) for repeated evaluations. diff --git a/doc/src/about/non-design.md b/doc/src/about/non-design.md index 4f01b14a..b0c0cbe9 100644 --- a/doc/src/about/non-design.md +++ b/doc/src/about/non-design.md @@ -27,8 +27,8 @@ It doesn't attempt to be a new language. For example: There is, however, support for simulated [closures] via [currying] a [function pointer] with captured shared variables. -* **No byte-codes/JIT** - Rhai has an optimized AST-walking interpreter which is fast enough for most usage scenarios. - Essential AST data structures are packed and kept together to maximize cache friendliness. +* **No byte-codes/JIT** - Rhai has an optimized AST-walking interpreter which is fast enough for most casual + usage scenarios. Essential AST data structures are packed and kept together to maximize cache friendliness. Functions are dispatched based on pre-calculated hashes and accessing variables are mostly through pre-calculated offsets to the variables file (a [`Scope`]), so it is seldom necessary to look something up by text name. diff --git a/doc/src/engine/metadata/export_to_json.md b/doc/src/engine/metadata/export_to_json.md new file mode 100644 index 00000000..c6476df3 --- /dev/null +++ b/doc/src/engine/metadata/export_to_json.md @@ -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 */", + ... + ] +} +``` diff --git a/doc/src/engine/get_fn_sig.md b/doc/src/engine/metadata/gen_fn_sig.md similarity index 58% rename from doc/src/engine/get_fn_sig.md rename to doc/src/engine/metadata/gen_fn_sig.md index 69894633..ee8390fc 100644 --- a/doc/src/engine/get_fn_sig.md +++ b/doc/src/engine/metadata/gen_fn_sig.md @@ -1,26 +1,29 @@ -Get Function Signatures -======================= +Generate Function Signatures +=========================== -{{#include ../links.md}} +{{#include ../../links.md}} `Engine::gen_fn_signatures` -------------------------- -As part of a _reflections_ API, `Engine::gen_fn_signatures` returns a list of function signatures -(`Vec`), each corresponding to a particular function available to that [`Engine`] instance. +As part of a _reflections_ API, `Engine::gen_fn_signatures` returns a list of function _signatures_ +(as `Vec`), each corresponding to a particular function available to that [`Engine`] instance. + +> `fn_name ( param_1: type_1, param_2: type_2, ... , param_n : type_n ) -> return_type` + +### Sources Functions from the following sources are included, in order: -1) Functions registered into the global namespace via the `Engine::register_XXX` API, -2) Functions in global sub-modules registered via [`Engine::register_module`]({{rootUrl}}/rust/modules/create.md), -3) Functions in registered [packages] (optional) - -Included are both native Rust as well as script-defined functions (except [`private`] ones). +1) Native Rust functions registered into the global namespace via the `Engine::register_XXX` API +2) _Public_ (i.e. non-[`private`]) functions (native Rust or Rhai scripted) in global sub-modules registered via + [`Engine::register_module`]({{rootUrl}}/rust/modules/create.md) +3) Native Rust functions in registered [packages] (optional) -Function Metadata ------------------ +Functions Metadata +------------------ Beware, however, that not all function signatures contain parameters and return value information. @@ -30,7 +33,7 @@ For instance, functions registered via `Engine::register_XXX` contain no informa the names of parameter and their actual types because Rust simply does not make such metadata available natively. The return type is also undetermined. -A function registered under the name 'foo' with three parameters and unknown return type: +A function registered under the name `foo` with three parameters and unknown return type: > `foo(_, _, _)` @@ -41,18 +44,18 @@ Notice that function names do not need to be valid identifiers. A [property setter][getters/setters] - again, unknown parameters and return type. Notice that function names do not need to be valid identifiers. -In this case, the first parameter should be '&mut T' of the custom type and the return value is '()': +In this case, the first parameter should be `&mut T` of the custom type and the return value is `()`: > `set$prop(_, _, _)` -### Script-defined functions +### Script-Defined Functions Script-defined [function] signatures contain parameter names. Since all parameters, as well as the return value, are [`Dynamic`] the types are simply not shown. A script-defined function always takes dynamic arguments, and the return type is also dynamic: -> `foo(x, y, z)` +> `foo(x, y, z) -> Dynamic` probably defined as: @@ -66,22 +69,22 @@ is the same as: > `foo(x: Dynamic, y: Dynamic, z: Dynamic) -> Result>` -### Plugin functions +### Plugin Functions Functions defined in [plugin modules] are the best. They contain all the metadata describing the functions. -A plugin function `merge`: +For example, a plugin function `merge`: > `merge(list: &mut MyStruct, num: usize, name: &str) -> Option` Notice that function names do not need to be valid identifiers. -An operator defined as a [fallible function] in a [plugin module] via `#[rhai_fn(name="+=", return_raw)]` -returns `Result>`: +For example, an operator defined as a [fallible function] in a [plugin module] via +`#[rhai_fn(name="+=", return_raw)]` returns `Result>`: > `+=(list: &mut MyStruct, num: usize, name: &str) -> Result>` -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) -> String` diff --git a/doc/src/engine/metadata/index.md b/doc/src/engine/metadata/index.md new file mode 100644 index 00000000..227b1fcf --- /dev/null +++ b/doc/src/engine/metadata/index.md @@ -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` diff --git a/doc/src/engine/options.md b/doc/src/engine/options.md index 46f55236..345a4b4c 100644 --- a/doc/src/engine/options.md +++ b/doc/src/engine/options.md @@ -5,14 +5,15 @@ Engine Configuration Options A number of other configuration options are available from the `Engine` to fine-tune behavior and safeguards. -| Method | Not available under | Description | -| ------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -| `set_optimization_level` | [`no_optimize`] | sets the amount of script _optimizations_ performed. See [script optimization]. | -| `set_max_expr_depths` | [`unchecked`] | sets the maximum nesting levels of an expression/statement. See [maximum statement depth]. | -| `set_max_call_levels` | [`unchecked`] | sets the maximum number of function call levels (default 50) to avoid infinite recursion. See [maximum call stack depth]. | -| `set_max_operations` | [`unchecked`] | sets the maximum number of _operations_ that a script is allowed to consume. See [maximum number of operations]. | -| `set_max_modules` | [`unchecked`] | sets the maximum number of [modules] that a script is allowed to load. See [maximum number of modules]. | -| `set_max_string_size` | [`unchecked`] | sets the maximum length (in UTF-8 bytes) for [strings]. See [maximum length of strings]. | -| `set_max_array_size` | [`unchecked`], [`no_index`] | sets the maximum size for [arrays]. See [maximum size of arrays]. | -| `set_max_map_size` | [`unchecked`], [`no_object`] | sets the maximum number of properties for [object maps]. See [maximum size of object maps]. | -| `disable_symbol` | | disables a certain keyword or operator. See [disable keywords and operators]. | +| Method | Not available under | Description | +| ------------------------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `set_doc_comments` | | enables/disables [doc-comments] | +| `set_optimization_level` | [`no_optimize`] | sets the amount of script _optimizations_ performedSee [script optimization] | +| `set_max_expr_depths` | [`unchecked`] | sets the maximum nesting levels of an expression/statementSee [maximum statement depth] | +| `set_max_call_levels` | [`unchecked`] | sets the maximum number of function call levels (default 50) to avoid infinite recursionSee [maximum call stack depth] | +| `set_max_operations` | [`unchecked`] | sets the maximum number of _operations_ that a script is allowed to consumeSee [maximum number of operations] | +| `set_max_modules` | [`unchecked`] | sets the maximum number of [modules] that a script is allowed to loadSee [maximum number of modules] | +| `set_max_string_size` | [`unchecked`] | sets the maximum length (in UTF-8 bytes) for [strings]See [maximum length of strings] | +| `set_max_array_size` | [`unchecked`], [`no_index`] | sets the maximum size for [arrays]See [maximum size of arrays] | +| `set_max_map_size` | [`unchecked`], [`no_object`] | sets the maximum number of properties for [object maps]See [maximum size of object maps] | +| `disable_symbol` | | disables a certain keyword or operatorSee [disable keywords and operators] | diff --git a/doc/src/language/comments.md b/doc/src/language/comments.md index eab5273c..111e370c 100644 --- a/doc/src/language/comments.md +++ b/doc/src/language/comments.md @@ -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. diff --git a/doc/src/language/doc-comments.md b/doc/src/language/doc-comments.md new file mode 100644 index 00000000..747413d3 --- /dev/null +++ b/doc/src/language/doc-comments.md @@ -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. diff --git a/doc/src/language/print-debug.md b/doc/src/language/print-debug.md index 606452d6..996e6aac 100644 --- a/doc/src/language/print-debug.md +++ b/doc/src/language/print-debug.md @@ -27,7 +27,7 @@ engine.on_print(|x| println!("hello: {}", x)); // Any function or closure that takes a '&str' and a 'Position' argument can be used to // override 'debug'. -engine.on_debug(|x, pos| println!("DEBUG at {:?}: {}", pos, x)); +engine.on_debug(|x, src, pos| println!("DEBUG of {} at {:?}: {}", src.unwrap_or("unknown"), pos, x)); // Example: quick-'n-dirty logging let logbook = Arc::new(RwLock::new(Vec::::new())); @@ -37,9 +37,9 @@ let log = logbook.clone(); engine.on_print(move |s| log.write().unwrap().push(format!("entry: {}", s))); let log = logbook.clone(); -engine.on_debug(move |s, pos| log.write().unwrap().push( - format!("DEBUG at {:?}: {}", pos, s) - )); +engine.on_debug(move |s, src, pos| log.write().unwrap().push( + format!("DEBUG of {} at {:?}: {}", src.unwrap_or("unknown"), pos, s) + )); // Evaluate script engine.eval::<()>(script)?; @@ -49,3 +49,24 @@ for entry in logbook.read().unwrap().iter() { println!("{}", entry); } ``` + + +`on_debug` Callback Signature +----------------------------- + +The function signature passed to `Engine::on_debug` takes the following form: + +> `Fn(text: &str, source: Option<&str>, pos: Position) + 'static` + +where: + +| Parameter | Type | Description | +| --------- | :------------: | --------------------------------------------------------------- | +| `text` | `&str` | text to display | +| `source` | `Option<&str>` | source of the current evaluation, if any | +| `pos` | `Position` | position (line number and character offset) of the `debug` call | + +The _source_ of a script evaluation is any text string provided to an [`AST`] via the `AST::set_source` method. + +If a [module] is loaded via an [`import`] statement, then the _source_ of functions defined +within the module will be the module's _path_. diff --git a/doc/src/links.md b/doc/src/links.md index 115c8890..c2681d79 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -13,6 +13,7 @@ [`no_closure`]: {{rootUrl}}/start/features.md [`no_std`]: {{rootUrl}}/start/features.md [`no-std`]: {{rootUrl}}/start/features.md +[`metadata`]: {{rootUrl}}/start/features.md [`internals`]: {{rootUrl}}/start/features.md [`unicode-xid-ident`]: {{rootUrl}}/start/features.md @@ -40,6 +41,7 @@ [plugin modules]: {{rootUrl}}/plugins/module.md [plugin function]: {{rootUrl}}/plugins/function.md [plugin functions]: {{rootUrl}}/plugins/function.md +[functions metadata]: {{rootUrl}}/engine/metadata/index.md [`Scope`]: {{rootUrl}}/engine/scope.md [`serde`]: {{rootUrl}}/rust/serde.md @@ -89,6 +91,7 @@ [timestamp]: {{rootUrl}}/language/timestamps.md [timestamps]: {{rootUrl}}/language/timestamps.md +[doc-comments]: {{rootUrl}}/language/doc-comments.md [function]: {{rootUrl}}/language/functions.md [functions]: {{rootUrl}}/language/functions.md [function overloading]: {{rootUrl}}/rust/functions.md#function-overloading diff --git a/doc/src/patterns/enums.md b/doc/src/patterns/enums.md index a4c9bc0b..c061bab9 100644 --- a/doc/src/patterns/enums.md +++ b/doc/src/patterns/enums.md @@ -178,22 +178,21 @@ Use `switch` Through Arrays --------------------------- Another way to work with Rust enums in a `switch` expression is through exposing the internal data -of each enum variant as a variable-length [array], usually with the name of the variant as -the first item for convenience: +(or at least those that act as effective _discriminants_) of each enum variant as a variable-length +[array], usually with the name of the variant as the first item for convenience: ```rust use rhai::Array; engine.register_get("enum_data", |x: &mut Enum| { match x { - Enum::Foo => vec![ - "Foo".into() - ] as Array, + Enum::Foo => vec![ "Foo".into() ] as Array, - Enum::Bar(value) => vec![ - "Bar".into(), (*value).into() - ] as Array, + // Say, skip the data field because it is not + // used as a discriminant + Enum::Bar(value) => vec![ "Bar".into() ] as Array, + // Say, all fields act as discriminants Enum::Baz(val1, val2) => vec![ "Baz".into(), val1.clone().into(), (*val2).into() ] as Array @@ -208,8 +207,7 @@ Then it is a simple matter to match an enum via the `switch` expression: // 'enum_data' creates a variable-length array with 'MyEnum' data let x = switch value.enum_data { ["Foo"] => 1, - ["Bar", 42] => 2, - ["Bar", 123] => 3, + ["Bar"] => value.field_1, ["Baz", "hello", false] => 4, ["Baz", "hello", true] => 5, _ => 9 @@ -220,10 +218,20 @@ x == 5; // Which is essentially the same as: let x = switch [value.type, value.field_0, value.field_1] { ["Foo", (), ()] => 1, - ["Bar", 42, ()] => 2, - ["Bar", 123, ()] => 3, + ["Bar", 42, ()] => 42, + ["Bar", 123, ()] => 123, + : ["Baz", "hello", false] => 4, ["Baz", "hello", true] => 5, _ => 9 } ``` + +Usually, a helper method returns an array of values that can uniquely determine +the switch case based on actual usage requirements - which means that it probably +skips fields that contain data instead of discriminants. + +Then `switch` is used to very quickly match through a large number of array shapes +and jump to the appropriate case implementation. + +Data fields can then be extracted from the enum independently. diff --git a/doc/src/patterns/oop.md b/doc/src/patterns/oop.md index b0e454c6..7b225c8c 100644 --- a/doc/src/patterns/oop.md +++ b/doc/src/patterns/oop.md @@ -26,10 +26,10 @@ Use Closures to Define Methods ----------------------------- [Anonymous functions] or [closures] defined as values for [object map] properties take on -a syntactic shape that resembles very closely that of class methods in an OOP language. +a syntactic shape which resembles very closely that of class methods in an OOP language. Closures also _[capture][automatic currying]_ variables from the defining environment, which is a very -common OOP pattern. Capturing is accomplished via a feature called _[automatic currying]_ and +common language feature. Capturing is accomplished via a feature called _[automatic currying]_ and can be turned off via the [`no_closure`] feature. @@ -59,3 +59,51 @@ factor = 2; obj.update(42); obj.action(); // prints 84 ``` + + +Simulating Inheritance With Mixin +-------------------------------- + +The `fill_with` method of [object maps] can be conveniently used to _polyfill_ default +method implementations from a _base class_, as per OOP lingo. + +Do not use the `mixin` method because it _overwrites_ existing fields. + +```rust +// Define base class +let BaseClass = #{ + factor: 1, + data: 42, + + get_data: || this.data * 2, + update: |x| this.data += x * this.factor +}; + +let obj = #{ + // Override base class field + factor: 100, + + // Override base class method + // Notice that the base class can also be accessed, if in scope + get_data: || this.call(BaseClass.get_data) * 999, +} + +// Polyfill missing fields/methods +obj.fill_with(BaseClass); + +// By this point, 'obj' has the following: +// +// #{ +// factor: 100 +// data: 42, +// get_data: || this.call(BaseClass.get_data) * 999, +// update: |x| this.data += x * this.factor +// } + +// obj.get_data() => (this.data (42) * 2) * 999 +obj.get_data() == 83916; + +obj.update(1); + +obj.data == 142 +``` diff --git a/doc/src/start/features.md b/doc/src/start/features.md index 85a13168..06e868db 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -11,24 +11,25 @@ Notice that this deviates from Rust norm where features are _additive_. Excluding unneeded functionalities can result in smaller, faster builds as well as more control over what a script can (or cannot) do. -| Feature | Additive? | Description | -| ------------------- | :-------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `unchecked` | no | disables arithmetic checking (such as over-flows and division by zero), call stack depth limit, operations count limit and modules loading limit.
Beware that a bad script may panic the entire system! | -| `sync` | no | restricts all values types to those that are `Send + Sync`. Under this feature, all Rhai types, including [`Engine`], [`Scope`] and [`AST`], are all `Send + Sync` | -| `no_optimize` | no | disables [script optimization] | -| `no_float` | no | disables floating-point numbers and math | -| `f32_float` | no | sets the system floating-point type to `f32` instead of `f64`. `FLOAT` is set to `f32` | -| `only_i32` | no | sets the system integer type to `i32` and disable all other integer types. `INT` is set to `i32` | -| `only_i64` | no | sets the system integer type to `i64` and disable all other integer types. `INT` is set to `i64` | -| `no_index` | no | disables [arrays] and indexing features | -| `no_object` | no | disables support for [custom types] and [object maps] | -| `no_function` | no | disables script-defined [functions] | -| `no_module` | no | disables loading external [modules] | -| `no_closure` | no | disables [capturing][automatic currying] external variables in [anonymous functions] to simulate _closures_, or [capturing the calling scope]({{rootUrl}}/language/fn-capture.md) in function calls | -| `no_std` | no | builds for `no-std` (implies `no_closure`). Notice that additional dependencies will be pulled in to replace `std` features | -| `serde` | yes | enables serialization/deserialization via `serde`. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies | -| `unicode-xid-ident` | no | allows [Unicode Standard Annex #31](http://www.unicode.org/reports/tr31/) as identifiers | -| `internals` | yes | exposes internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version | +| Feature | Additive? | Description | +| ------------------- | :-------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `unchecked` | no | disables arithmetic checking (such as over-flows and division by zero), call stack depth limit, operations count limit and modules loading limit.
Beware that a bad script may panic the entire system! | +| `sync` | no | restricts all values types to those that are `Send + Sync`. Under this feature, all Rhai types, including [`Engine`], [`Scope`] and [`AST`], are all `Send + Sync` | +| `no_optimize` | no | disables [script optimization] | +| `no_float` | no | disables floating-point numbers and math | +| `f32_float` | no | sets the system floating-point type to `f32` instead of `f64`. `FLOAT` is set to `f32` | +| `only_i32` | no | sets the system integer type to `i32` and disable all other integer types. `INT` is set to `i32` | +| `only_i64` | no | sets the system integer type to `i64` and disable all other integer types. `INT` is set to `i64` | +| `no_index` | no | disables [arrays] and indexing features | +| `no_object` | no | disables support for [custom types] and [object maps] | +| `no_function` | no | disables script-defined [functions] (implies `no_closure`) | +| `no_module` | no | disables loading external [modules] | +| `no_closure` | no | disables [capturing][automatic currying] external variables in [anonymous functions] to simulate _closures_, or [capturing the calling scope]({{rootUrl}}/language/fn-capture.md) in function calls | +| `no_std` | no | builds for `no-std` (implies `no_closure`). Notice that additional dependencies will be pulled in to replace `std` features | +| `serde` | yes | enables serialization/deserialization via `serde` (requires the [`serde`](https://crates.io/crates/serde) crate) | +| `unicode-xid-ident` | no | allows [Unicode Standard Annex #31](http://www.unicode.org/reports/tr31/) as identifiers | +| `metadata` | yes | enables exporting [functions metadata] to [JSON format]({{rootUrl}}/engine/metadata/export_to_json.md) (implies `serde` and additionally requires the [`serde_json`](https://crates.io/crates/serde_json) crate) | +| `internals` | yes | exposes internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version | Example diff --git a/examples/repl.rs b/examples/repl.rs index 7d42cfac..92e04a71 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -147,6 +147,15 @@ fn main() { println!(); continue; } + // "json" => { + // println!( + // "{}", + // engine + // .gen_fn_metadata_to_json(Some(&main_ast), false) + // .unwrap() + // ); + // continue; + // } _ => (), } diff --git a/src/ast.rs b/src/ast.rs index 75435825..5b0262a1 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -163,6 +163,8 @@ impl<'a> Into> for &'a ScriptFnDef { /// Currently, [`AST`] is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. #[derive(Debug, Clone)] pub struct AST { + /// Source of the [`AST`]. + source: Option, /// Global statements. statements: Vec, /// Script-defined functions. @@ -172,6 +174,7 @@ pub struct AST { impl Default for AST { fn default() -> Self { Self { + source: None, statements: Vec::with_capacity(16), functions: Default::default(), } @@ -186,10 +189,36 @@ impl AST { functions: impl Into>, ) -> Self { Self { + source: None, statements: statements.into_iter().collect(), functions: functions.into(), } } + /// Create a new [`AST`] with a source name. + #[inline(always)] + pub fn new_with_source( + statements: impl IntoIterator, + functions: impl Into>, + source: impl Into, + ) -> 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 { + self.source.clone() + } + /// Set the source. + pub fn set_source>(&mut self, source: Option) { + self.source = source.map(|s| s.into()) + } /// Get the statements. #[cfg(not(feature = "internals"))] #[inline(always)] @@ -253,6 +282,7 @@ impl AST { let mut functions: Module = Default::default(); functions.merge_filtered(&self.functions, &mut filter); Self { + source: self.source.clone(), statements: Default::default(), functions: functions.into(), } @@ -262,6 +292,7 @@ impl AST { #[inline(always)] pub fn clone_statements_only(&self) -> Self { Self { + source: self.source.clone(), statements: self.statements.clone(), functions: Default::default(), } @@ -432,6 +463,7 @@ impl AST { let Self { statements, functions, + .. } = self; let ast = match (statements.is_empty(), other.statements.is_empty()) { @@ -445,10 +477,20 @@ impl AST { (true, true) => vec![], }; + let source = if other.source.is_some() { + other.source.clone() + } else { + self.source.clone() + }; + let mut functions = functions.as_ref().clone(); functions.merge_filtered(&other.functions, &mut filter); - Self::new(ast, functions) + if let Some(source) = source { + Self::new_with_source(ast, functions, source) + } else { + Self::new(ast, functions) + } } /// Combine one [`AST`] with another. The second [`AST`] is consumed. /// @@ -1191,12 +1233,16 @@ impl Expr { | Self::Map(_, _) => match token { #[cfg(not(feature = "no_index"))] Token::LeftBracket => true, + #[cfg(not(feature = "no_object"))] + Token::Period => true, _ => false, }, Self::Variable(_) => match token { #[cfg(not(feature = "no_index"))] Token::LeftBracket => true, + #[cfg(not(feature = "no_object"))] + Token::Period => true, Token::LeftParen => true, Token::Bang => true, Token::DoubleColon => true, @@ -1206,6 +1252,8 @@ impl Expr { Self::Property(_) => match token { #[cfg(not(feature = "no_index"))] Token::LeftBracket => true, + #[cfg(not(feature = "no_object"))] + Token::Period => true, Token::LeftParen => true, _ => false, }, diff --git a/src/engine.rs b/src/engine.rs index 0fa2ee9f..17937537 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -24,7 +24,7 @@ use crate::stdlib::{ string::{String, ToString}, }; use crate::syntax::CustomSyntax; -use crate::utils::get_hasher; +use crate::utils::{get_hasher, StraightHasherBuilder}; use crate::{ calc_native_fn_hash, Dynamic, EvalAltResult, FnPtr, ImmutableString, Module, Position, Scope, Shared, StaticVec, @@ -100,19 +100,21 @@ impl Imports { } /// Get an iterator to this stack of imported [modules][Module] in reverse order. #[allow(dead_code)] - pub fn iter<'a>(&'a self) -> impl Iterator)> + 'a { + pub fn iter<'a>(&'a self) -> impl Iterator + 'a { self.0.iter().flat_map(|lib| { lib.iter() .rev() - .map(|(name, module)| (name.clone(), module.clone())) + .map(|(name, module)| (name.as_str(), module.as_ref())) }) } /// Get an iterator to this stack of imported [modules][Module] in reverse order. #[allow(dead_code)] pub(crate) fn iter_raw<'a>( &'a self, - ) -> impl Iterator)> + 'a { - self.0.iter().flat_map(|lib| lib.iter().rev().cloned()) + ) -> impl Iterator)> + 'a { + self.0 + .iter() + .flat_map(|lib| lib.iter().rev().map(|(n, m)| (n, m))) } /// Get a consuming iterator to this stack of imported [modules][Module] in reverse order. pub fn into_iter(self) -> impl Iterator)> { @@ -125,15 +127,23 @@ impl Imports { /// Does the specified function hash key exist in this stack of imported [modules][Module]? #[allow(dead_code)] pub fn contains_fn(&self, hash: u64) -> bool { - self.0.as_ref().map_or(false, |x| { - x.iter().any(|(_, m)| m.contains_qualified_fn(hash)) - }) + if hash == 0 { + false + } else { + self.0.as_ref().map_or(false, |x| { + x.iter().any(|(_, m)| m.contains_qualified_fn(hash)) + }) + } } /// Get specified function via its hash key. pub fn get_fn(&self, hash: u64) -> Option<&CallableFunction> { - self.0 - .as_ref() - .and_then(|x| x.iter().rev().find_map(|(_, m)| m.get_qualified_fn(hash))) + if hash == 0 { + None + } else { + self.0 + .as_ref() + .and_then(|x| x.iter().rev().find_map(|(_, m)| m.get_qualified_fn(hash))) + } } /// Does the specified [`TypeId`][std::any::TypeId] iterator exist in this stack of imported [modules][Module]? #[allow(dead_code)] @@ -368,29 +378,29 @@ impl<'a> Target<'a> { #[cfg(not(feature = "no_index"))] Self::StringChar(_, _, ch) => { let char_value = ch.clone(); - self.set_value((char_value, Position::NONE)).unwrap(); + self.set_value(char_value, Position::NONE).unwrap(); } } } /// Update the value of the `Target`. #[cfg(any(not(feature = "no_object"), not(feature = "no_index")))] - pub fn set_value(&mut self, new_val: (Dynamic, Position)) -> Result<(), Box> { + pub fn set_value(&mut self, new_val: Dynamic, pos: Position) -> Result<(), Box> { match self { - Self::Ref(r) => **r = new_val.0, + Self::Ref(r) => **r = new_val, #[cfg(not(feature = "no_closure"))] #[cfg(not(feature = "no_object"))] - Self::LockGuard((r, _)) => **r = new_val.0, + Self::LockGuard((r, _)) => **r = new_val, Self::Value(_) => unreachable!(), #[cfg(not(feature = "no_index"))] Self::StringChar(string, index, _) if string.is::() => { let mut s = string.write_lock::().unwrap(); // Replace the character at the specified index position - let new_ch = new_val.0.as_char().map_err(|err| { + let new_ch = new_val.as_char().map_err(|err| { Box::new(EvalAltResult::ErrorMismatchDataType( err.to_string(), "char".to_string(), - new_val.1, + pos, )) })?; @@ -468,8 +478,10 @@ impl> From for Target<'_> { /// ## WARNING /// /// This type is volatile and may change. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] +#[derive(Debug, Clone, Default)] pub struct State { + /// Source of the current context. + pub source: Option, /// Normally, access to variables are parsed with a relative offset into the scope to avoid a lookup. /// In some situation, e.g. after running an `eval` statement, subsequent offsets become mis-aligned. /// When that happens, this flag is turned on to force a scope lookup by name. @@ -481,6 +493,8 @@ pub struct State { pub operations: u64, /// Number of modules loaded. pub modules: usize, + /// Cached lookup values for function hashes. + pub functions_cache: HashMap, StraightHasherBuilder>, } impl State { @@ -644,6 +658,9 @@ pub struct Engine { /// Max limits. #[cfg(not(feature = "unchecked"))] pub(crate) limits: Limits, + + /// Disable doc-comments? + pub(crate) disable_doc_comments: bool, } impl fmt::Debug for Engine { @@ -695,10 +712,14 @@ fn default_print(_s: &str) { /// Debug to stdout #[inline(always)] -fn default_debug(_s: &str, _pos: Position) { +fn default_debug(_s: &str, _source: Option<&str>, _pos: Position) { #[cfg(not(feature = "no_std"))] #[cfg(not(target_arch = "wasm32"))] - println!("{:?} | {}", _pos, _s); + if let Some(source) = _source { + println!("{} @ {:?} | {}", source, _pos, _s); + } else { + println!("{:?} | {}", _pos, _s); + } } /// Search for a module within an imports stack. @@ -784,6 +805,8 @@ impl Engine { #[cfg(not(feature = "no_object"))] max_map_size: 0, }, + + disable_doc_comments: false, }; engine.load_package(StandardPackage::new().get()); @@ -813,7 +836,7 @@ impl Engine { resolve_var: None, print: Box::new(|_| {}), - debug: Box::new(|_, _| {}), + debug: Box::new(|_, _, _| {}), progress: None, optimization_level: if cfg!(feature = "no_optimize") { @@ -837,6 +860,8 @@ impl Engine { #[cfg(not(feature = "no_object"))] max_map_size: 0, }, + + disable_doc_comments: false, } } @@ -1014,7 +1039,8 @@ impl Engine { ) { // Indexed value is a reference - update directly Ok(ref mut obj_ptr) => { - obj_ptr.set_value(new_val.unwrap())?; + let (new_val, new_val_pos) = new_val.unwrap(); + obj_ptr.set_value(new_val, new_val_pos)?; None } Err(err) => match *err { @@ -1090,7 +1116,9 @@ impl Engine { mods, state, lib, target_val, index, *pos, true, is_ref, false, level, )?; - val.set_value(new_val.unwrap())?; + let (new_val, new_val_pos) = new_val.unwrap(); + val.set_value(new_val, new_val_pos)?; + Ok((Default::default(), true)) } // {xxx:map}.id @@ -1291,8 +1319,7 @@ impl Engine { pos: var_pos, } = &x.3; - self.inc_operations(state) - .map_err(|err| err.fill_position(*var_pos))?; + self.inc_operations(state, *var_pos)?; let (target, _, pos) = self.search_namespace(scope, mods, state, lib, this_ptr, lhs)?; @@ -1343,8 +1370,7 @@ impl Engine { size: usize, level: usize, ) -> Result<(), Box> { - self.inc_operations(state) - .map_err(|err| err.fill_position(expr.position()))?; + self.inc_operations(state, expr.position())?; match expr { Expr::FnCall(x, _) if x.namespace.is_none() => { @@ -1418,7 +1444,7 @@ impl Engine { _indexers: bool, _level: usize, ) -> Result, Box> { - self.inc_operations(state)?; + self.inc_operations(state, Position::NONE)?; match target { #[cfg(not(feature = "no_index"))] @@ -1521,8 +1547,7 @@ impl Engine { rhs: &Expr, level: usize, ) -> Result> { - self.inc_operations(state) - .map_err(|err| err.fill_position(rhs.position()))?; + self.inc_operations(state, rhs.position())?; let lhs_value = self.eval_expr(scope, mods, state, lib, this_ptr, lhs, level)?; let rhs_value = self.eval_expr(scope, mods, state, lib, this_ptr, rhs, level)?; @@ -1695,8 +1720,7 @@ impl Engine { expr: &Expr, level: usize, ) -> Result> { - self.inc_operations(state) - .map_err(|err| err.fill_position(expr.position()))?; + self.inc_operations(state, expr.position())?; let result = match expr { Expr::Expr(x) => self.eval_expr(scope, mods, state, lib, this_ptr, x, level), @@ -1851,8 +1875,7 @@ impl Engine { _ => unreachable!(), }; - self.check_data_size(result) - .map_err(|err| err.fill_position(expr.position())) + self.check_data_size(result, expr.position()) } /// Evaluate a statements block. @@ -1878,6 +1901,10 @@ impl Engine { }); scope.rewind(prev_scope_len); + if mods.len() != prev_mods_len { + // If imports list is modified, clear the functions lookup cache + state.functions_cache.clear(); + } mods.truncate(prev_mods_len); state.scope_level -= 1; @@ -1904,8 +1931,7 @@ impl Engine { stmt: &Stmt, level: usize, ) -> Result> { - self.inc_operations(state) - .map_err(|err| err.fill_position(stmt.position()))?; + self.inc_operations(state, stmt.position())?; let result = match stmt { // No-op @@ -1927,8 +1953,7 @@ impl Engine { return EvalAltResult::ErrorAssignmentToConstant(name.to_string(), pos).into(); } - self.inc_operations(state) - .map_err(|err| err.fill_position(pos))?; + self.inc_operations(state, pos)?; if lhs_ptr.as_ref().is_read_only() { // Assignment to constant variable @@ -2187,8 +2212,7 @@ impl Engine { *loop_var = value; } - self.inc_operations(state) - .map_err(|err| err.fill_position(stmt.position()))?; + self.inc_operations(state, stmt.position())?; match self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level) { Ok(_) => (), @@ -2357,6 +2381,8 @@ impl Engine { } else { mods.push(name_def.name.clone(), module); } + // When imports list is modified, clear the functions lookup cache + state.functions_cache.clear(); } state.modules += 1; @@ -2404,8 +2430,7 @@ impl Engine { } }; - self.check_data_size(result) - .map_err(|err| err.fill_position(stmt.position())) + self.check_data_size(result, stmt.position()) } /// Check a result to ensure that the data size is within allowable limit. @@ -2415,16 +2440,17 @@ impl Engine { fn check_data_size( &self, result: Result>, + _pos: Position, ) -> Result> { result } /// Check a result to ensure that the data size is within allowable limit. - /// [`Position`] in [`EvalAltResult`] may be None and should be set afterwards. #[cfg(not(feature = "unchecked"))] fn check_data_size( &self, result: Result>, + pos: Position, ) -> Result> { // If no data size limits, just return let mut total = 0; @@ -2513,47 +2539,41 @@ impl Engine { let (_arr, _map, s) = calc_size(result.as_ref().unwrap()); if s > self.max_string_size() { - return EvalAltResult::ErrorDataTooLarge( - "Length of string".to_string(), - Position::NONE, - ) - .into(); + return EvalAltResult::ErrorDataTooLarge("Length of string".to_string(), pos).into(); } #[cfg(not(feature = "no_index"))] if _arr > self.max_array_size() { - return EvalAltResult::ErrorDataTooLarge("Size of array".to_string(), Position::NONE) - .into(); + return EvalAltResult::ErrorDataTooLarge("Size of array".to_string(), pos).into(); } #[cfg(not(feature = "no_object"))] if _map > self.max_map_size() { - return EvalAltResult::ErrorDataTooLarge( - "Size of object map".to_string(), - Position::NONE, - ) - .into(); + return EvalAltResult::ErrorDataTooLarge("Size of object map".to_string(), pos).into(); } result } /// Check if the number of operations stay within limit. - /// [`Position`] in [`EvalAltResult`] is [`None`][Position::None] and must be set afterwards. - pub(crate) fn inc_operations(&self, state: &mut State) -> Result<(), Box> { + pub(crate) fn inc_operations( + &self, + state: &mut State, + pos: Position, + ) -> Result<(), Box> { state.operations += 1; #[cfg(not(feature = "unchecked"))] // Guard against too many operations if self.max_operations() > 0 && state.operations > self.max_operations() { - return EvalAltResult::ErrorTooManyOperations(Position::NONE).into(); + return EvalAltResult::ErrorTooManyOperations(pos).into(); } // Report progress - only in steps if let Some(progress) = &self.progress { if let Some(token) = progress(state.operations) { // Terminate script if progress returns a termination token - return EvalAltResult::ErrorTerminated(token, Position::NONE).into(); + return EvalAltResult::ErrorTerminated(token, pos).into(); } } diff --git a/src/engine_api.rs b/src/engine_api.rs index fe6a9a48..9f72a5df 100644 --- a/src/engine_api.rs +++ b/src/engine_api.rs @@ -1,7 +1,7 @@ //! Module that defines the extern API of [`Engine`]. use crate::dynamic::Variant; -use crate::engine::{EvalContext, Imports}; +use crate::engine::{EvalContext, Imports, State}; use crate::fn_native::{FnCallArgs, SendSync}; use crate::optimize::OptimizationLevel; use crate::stdlib::{ @@ -1368,9 +1368,9 @@ impl Engine { scope: &mut Scope, ast: &AST, ) -> Result> { - let mut mods = self.global_sub_modules.clone(); + let mods = &mut self.global_sub_modules.clone(); - let (result, _) = self.eval_ast_with_scope_raw(scope, &mut mods, ast)?; + let result = self.eval_ast_with_scope_raw(scope, mods, ast)?; let typ = self.map_type_name(result.type_name()); @@ -1390,8 +1390,12 @@ impl Engine { scope: &mut Scope, mods: &mut Imports, ast: &'a AST, - ) -> Result<(Dynamic, u64), Box> { - self.eval_statements_raw(scope, mods, ast.statements(), &[ast.lib()]) + ) -> Result> { + let state = &mut State { + source: ast.clone_source(), + ..Default::default() + }; + self.eval_statements_raw(scope, mods, state, ast.statements(), &[ast.lib()]) } /// Evaluate a file, but throw away the result and only return error (if any). /// Useful for when you don't need the result, but still need to keep track of possible errors. @@ -1450,10 +1454,13 @@ impl Engine { scope: &mut Scope, ast: &AST, ) -> Result<(), Box> { - let mut mods = self.global_sub_modules.clone(); - - self.eval_statements_raw(scope, &mut mods, ast.statements(), &[ast.lib()]) - .map(|_| ()) + let mods = &mut self.global_sub_modules.clone(); + let state = &mut State { + source: ast.clone_source(), + ..Default::default() + }; + self.eval_statements_raw(scope, mods, state, ast.statements(), &[ast.lib()])?; + Ok(()) } /// Call a script function defined in an [`AST`] with multiple arguments. /// Arguments are passed as a tuple. @@ -1808,20 +1815,22 @@ impl Engine { /// /// // Override action of 'print' function /// let logger = result.clone(); - /// engine.on_debug(move |s, pos| logger.write().unwrap().push_str( - /// &format!("{:?} > {}", pos, s) - /// )); + /// engine.on_debug(move |s, src, pos| logger.write().unwrap().push_str( + /// &format!("{} @ {:?} > {}", src.unwrap_or("unknown"), pos, s) + /// )); /// - /// engine.consume(r#"let x = "hello"; debug(x);"#)?; + /// let mut ast = engine.compile(r#"let x = "hello"; debug(x);"#)?; + /// ast.set_source(Some("world")); + /// engine.consume_ast(&ast)?; /// - /// assert_eq!(*result.read().unwrap(), r#"1:18 > "hello""#); + /// assert_eq!(*result.read().unwrap(), r#"world @ 1:18 > "hello""#); /// # Ok(()) /// # } /// ``` #[inline(always)] pub fn on_debug( &mut self, - callback: impl Fn(&str, Position) + SendSync + 'static, + callback: impl Fn(&str, Option<&str>, Position) + SendSync + 'static, ) -> &mut Self { self.debug = Box::new(callback); self diff --git a/src/engine_settings.rs b/src/engine_settings.rs index 6b6f8ba7..ca220cb1 100644 --- a/src/engine_settings.rs +++ b/src/engine_settings.rs @@ -40,6 +40,12 @@ impl Engine { pub fn optimization_level(&self) -> crate::OptimizationLevel { self.optimization_level } + /// Enable/disable doc-comments. + #[inline(always)] + pub fn set_doc_comments(&mut self, enable: bool) -> &mut Self { + self.disable_doc_comments = !enable; + self + } /// Set the maximum levels of function calls allowed for a script in order to avoid /// infinite recursion and stack overflows. #[cfg(not(feature = "unchecked"))] diff --git a/src/fn_call.rs b/src/fn_call.rs index 82673b20..70258f03 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -168,16 +168,27 @@ impl Engine { pos: Position, def_val: Option<&Dynamic>, ) -> Result<(Dynamic, bool), Box> { - self.inc_operations(state)?; + self.inc_operations(state, pos)?; - // Search for the native function - // First search registered functions (can override packages) - // Then search packages - let func = //lib.get_fn(hash_fn, pub_only) - self.global_namespace.get_fn(hash_fn, pub_only) + let func = state.functions_cache.get(&hash_fn).cloned(); + + let func = if let Some(ref f) = func { + f.as_ref() + } else { + // Search for the native function + // First search registered functions (can override packages) + // Then search packages + // lib.get_fn(hash_fn, pub_only) + let f = self + .global_namespace + .get_fn(hash_fn, pub_only) .or_else(|| self.packages.get_fn(hash_fn)) .or_else(|| mods.get_fn(hash_fn)); + state.functions_cache.insert(hash_fn, f.cloned()); + f + }; + if let Some(func) = func { assert!(func.is_native()); @@ -210,20 +221,17 @@ impl Engine { .into(), false, ), - KEYWORD_DEBUG => ( - (self.debug)( - result.as_str().map_err(|typ| { - EvalAltResult::ErrorMismatchOutputType( - self.map_type_name(type_name::()).into(), - typ.into(), - pos, - ) - })?, - pos, - ) - .into(), - false, - ), + KEYWORD_DEBUG => { + let text = result.as_str().map_err(|typ| { + EvalAltResult::ErrorMismatchOutputType( + self.map_type_name(type_name::()).into(), + typ.into(), + pos, + ) + })?; + let source = state.source.as_ref().map(|s| s.as_str()); + ((self.debug)(text, source, pos).into(), false) + } _ => (result, func.is_method()), }); } @@ -337,7 +345,7 @@ impl Engine { pos: Position, level: usize, ) -> Result> { - self.inc_operations(state)?; + self.inc_operations(state, pos)?; // Check for stack overflow #[cfg(not(feature = "no_function"))] @@ -370,6 +378,9 @@ impl Engine { let mut lib_merged: StaticVec<_>; let unified_lib = if let Some(ref env_lib) = fn_def.lib { + // If the library is modified, clear the functions lookup cache + state.functions_cache.clear(); + lib_merged = Default::default(); lib_merged.push(env_lib.as_ref()); lib_merged.extend(lib.iter().cloned()); @@ -380,7 +391,7 @@ impl Engine { #[cfg(not(feature = "no_module"))] if !fn_def.mods.is_empty() { - mods.extend(fn_def.mods.iter_raw()); + mods.extend(fn_def.mods.iter_raw().map(|(n, m)| (n.clone(), m.clone()))); } // Evaluate the function at one higher level of call depth @@ -440,19 +451,18 @@ impl Engine { hash_script: u64, pub_only: bool, ) -> bool { - // NOTE: We skip script functions for global_namespace and packages, and native functions for lib - // First check script-defined functions - lib.iter().any(|&m| m.contains_fn(hash_script, pub_only)) + (hash_script != 0 && lib.iter().any(|&m| m.contains_fn(hash_script, pub_only))) //|| lib.iter().any(|&m| m.contains_fn(hash_fn, pub_only)) // Then check registered functions - //|| self.global_namespace.contains_fn(hash_script, pub_only) + //|| (hash_script != 0 && self.global_namespace.contains_fn(hash_script, pub_only)) || self.global_namespace.contains_fn(hash_fn, false) // Then check packages - || self.packages.contains_fn(hash_script) + || (hash_script != 0 && self.packages.contains_fn(hash_script)) || self.packages.contains_fn(hash_fn) // Then check imported modules - || mods.map(|m| m.contains_fn(hash_script) || m.contains_fn(hash_fn)).unwrap_or(false) + || (hash_script != 0 && mods.map(|m| m.contains_fn(hash_script)).unwrap_or(false)) + || mods.map(|m| m.contains_fn(hash_fn)).unwrap_or(false) } /// Perform an actual function call, native Rust or scripted, taking care of special functions. @@ -518,14 +528,16 @@ impl Engine { // Script-like function found #[cfg(not(feature = "no_function"))] - _ if self.has_override(Some(mods), lib, 0, hash_script, pub_only) => { + _ if hash_script != 0 + && self.has_override(Some(mods), lib, 0, hash_script, pub_only) => + { // Get function - let func = lib + let (func, mut source) = lib .iter() - .find_map(|&m| m.get_fn(hash_script, pub_only)) + .find_map(|&m| m.get_fn(hash_script, pub_only).map(|f| (f, m.clone_id()))) //.or_else(|| self.global_namespace.get_fn(hash_script, pub_only)) - .or_else(|| self.packages.get_fn(hash_script)) - //.or_else(|| mods.get_fn(hash_script)) + .or_else(|| self.packages.get_fn(hash_script).map(|f| (f, None))) + //.or_else(|| mods.iter().find_map(|(_, m)| m.get_qualified_fn(hash_script).map(|f| (f, m.clone_id())))) .unwrap(); if func.is_script() { @@ -550,7 +562,10 @@ impl Engine { let result = if _is_method { // Method call of script function - map first argument to `this` let (first, rest) = args.split_first_mut().unwrap(); - self.call_script_fn( + + mem::swap(&mut state.source, &mut source); + + let result = self.call_script_fn( scope, mods, state, @@ -560,17 +575,27 @@ impl Engine { rest, pos, _level, - )? + ); + + // Restore the original source + state.source = source; + + result? } else { // Normal call of script function // The first argument is a reference? let mut backup: ArgBackup = Default::default(); backup.change_first_arg_to_copy(is_ref, args); + mem::swap(&mut state.source, &mut source); + let result = self.call_script_fn( scope, mods, state, lib, &mut None, func, args, pos, _level, ); + // Restore the original source + state.source = source; + // Restore the original reference backup.restore_first_arg(args); @@ -609,26 +634,24 @@ impl Engine { &self, scope: &mut Scope, mods: &mut Imports, + state: &mut State, statements: impl IntoIterator, lib: &[&Module], - ) -> Result<(Dynamic, u64), Box> { - let mut state = Default::default(); - + ) -> Result> { statements .into_iter() .try_fold(().into(), |_, stmt| { - self.eval_stmt(scope, mods, &mut state, lib, &mut None, stmt, 0) + self.eval_stmt(scope, mods, state, lib, &mut None, stmt, 0) }) .or_else(|err| match *err { EvalAltResult::Return(out, _) => Ok(out), EvalAltResult::LoopBreak(_, _) => unreachable!(), _ => Err(err), }) - .map(|v| (v, state.operations)) } - /// Evaluate a text string as a script - used primarily for 'eval'. - fn eval_script_expr( + /// Evaluate a text script in place - used primarily for 'eval'. + fn eval_script_expr_in_place( &self, scope: &mut Scope, mods: &mut Imports, @@ -638,7 +661,7 @@ impl Engine { pos: Position, _level: usize, ) -> Result> { - self.inc_operations(state)?; + self.inc_operations(state, pos)?; let script = script.trim(); if script.is_empty() { @@ -666,12 +689,16 @@ impl Engine { } // Evaluate the AST - let (result, operations) = self.eval_statements_raw(scope, mods, ast.statements(), lib)?; + let mut new_state = State { + source: state.source.clone(), + operations: state.operations, + ..Default::default() + }; - state.operations += operations; - self.inc_operations(state)?; + let result = self.eval_statements_raw(scope, mods, &mut new_state, ast.statements(), lib); - return Ok(result); + state.operations = new_state.operations; + result } /// Call a dot method. @@ -963,7 +990,8 @@ impl Engine { self.make_type_mismatch_err::(typ, args_expr[0].position()) })?; let pos = args_expr[0].position(); - let result = self.eval_script_expr(scope, mods, state, lib, script, pos, level + 1); + let result = + self.eval_script_expr_in_place(scope, mods, state, lib, script, pos, level + 1); // IMPORTANT! If the eval defines new variables in the current scope, // all variable offsets from this point on will be mis-aligned. @@ -1006,8 +1034,7 @@ impl Engine { target = target.into_owned(); } - self.inc_operations(state) - .map_err(|err| err.fill_position(pos))?; + self.inc_operations(state, pos)?; args = if target.is_shared() || target.is_value() { arg_values.insert(0, target.take_or_clone().flatten()); @@ -1089,8 +1116,7 @@ impl Engine { let (target, _, pos) = self.search_scope_only(scope, mods, state, lib, this_ptr, &args_expr[0])?; - self.inc_operations(state) - .map_err(|err| err.fill_position(pos))?; + self.inc_operations(state, pos)?; if target.is_shared() || target.is_value() { arg_values[0] = target.take_or_clone().flatten(); @@ -1119,7 +1145,7 @@ impl Engine { let func = match module.get_qualified_fn(hash_script) { // Then search in Rust functions None => { - self.inc_operations(state)?; + self.inc_operations(state, pos)?; // Namespace-qualified Rust functions are indexed in two steps: // 1) Calculate a hash in a similar manner to script-defined functions, @@ -1149,9 +1175,17 @@ impl Engine { let args = args.as_mut(); let new_scope = &mut Default::default(); let fn_def = f.get_fn_def().clone(); - self.call_script_fn( + + let mut source = module.clone_id(); + mem::swap(&mut state.source, &mut source); + + let result = self.call_script_fn( new_scope, mods, state, lib, &mut None, &fn_def, args, pos, level, - ) + ); + + state.source = source; + + result } Some(f) if f.is_plugin_fn() => f .get_plugin_fn() diff --git a/src/fn_native.rs b/src/fn_native.rs index c9c82300..6cbe7542 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -360,10 +360,10 @@ pub type OnPrintCallback = Box; /// A standard callback function for debugging. #[cfg(not(feature = "sync"))] -pub type OnDebugCallback = Box; +pub type OnDebugCallback = Box, Position) + 'static>; /// A standard callback function for debugging. #[cfg(feature = "sync")] -pub type OnDebugCallback = Box; +pub type OnDebugCallback = Box, Position) + Send + Sync + 'static>; /// A standard callback function for variable access. #[cfg(not(feature = "sync"))] diff --git a/src/module/mod.rs b/src/module/mod.rs index 6db9889d..e94d3297 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -119,6 +119,8 @@ impl FuncInfo { /// Not available under the `no_module` feature. #[derive(Clone)] pub struct Module { + /// ID identifying the module. + id: Option, /// Sub-modules. modules: HashMap>, /// Module variables. @@ -141,6 +143,7 @@ pub struct Module { impl Default for Module { fn default() -> Self { Self { + id: None, modules: Default::default(), variables: Default::default(), all_variables: Default::default(), @@ -157,7 +160,12 @@ impl fmt::Debug for Module { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "Module(\n modules: {}\n vars: {}\n functions: {}\n)", + "Module({}\n modules: {}\n vars: {}\n functions: {}\n)", + if let Some(ref id) = self.id { + format!("id: {:?}", id) + } else { + "".to_string() + }, self.modules .keys() .map(|m| m.as_str()) @@ -220,6 +228,41 @@ impl Module { } } + /// Get the ID of the module, if any. + /// + /// # Example + /// + /// ``` + /// use rhai::Module; + /// + /// let mut module = Module::new(); + /// module.set_id(Some("hello")); + /// assert_eq!(module.id(), Some("hello")); + /// ``` + pub fn id(&self) -> Option<&str> { + self.id.as_ref().map(|s| s.as_str()) + } + + /// Get the ID of the module, if any. + pub(crate) fn clone_id(&self) -> Option { + 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>(&mut self, id: Option) { + self.id = id.map(|s| s.into()); + } + /// Is the module empty? /// /// # Example @@ -1338,7 +1381,11 @@ impl Module { /// the hash calculated by [`build_index`][Module::build_index]. #[inline] pub fn contains_qualified_fn(&self, hash_fn: u64) -> bool { - self.all_functions.contains_key(&hash_fn) + if hash_fn == 0 { + false + } else { + self.all_functions.contains_key(&hash_fn) + } } /// Get a namespace-qualified function. @@ -1348,7 +1395,11 @@ impl Module { /// the hash calculated by [`build_index`][Module::build_index]. #[inline(always)] pub(crate) fn get_qualified_fn(&self, hash_qualified_fn: u64) -> Option<&CallableFunction> { - self.all_functions.get(&hash_qualified_fn) + if hash_qualified_fn == 0 { + None + } else { + self.all_functions.get(&hash_qualified_fn) + } } /// Combine another module into this module. @@ -1679,6 +1730,7 @@ impl Module { }); } + module.set_id(ast.clone_source()); module.build_index(); Ok(module) diff --git a/src/module/resolvers/file.rs b/src/module/resolvers/file.rs index 3c509e96..0831ff25 100644 --- a/src/module/resolvers/file.rs +++ b/src/module/resolvers/file.rs @@ -163,10 +163,11 @@ impl ModuleResolver for FileModuleResolver { _ => Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)), })?; - let m = Module::eval_ast_as_new(scope, &ast, engine).map_err(|err| { + let mut m = Module::eval_ast_as_new(scope, &ast, engine).map_err(|err| { Box::new(EvalAltResult::ErrorInModule(path.to_string(), err, pos)) })?; + m.set_id(Some(path)); module = Some(m.into()); module_ref = module.clone(); }; diff --git a/src/packages/arithmetic.rs b/src/packages/arithmetic.rs index 9bc13cb8..6ed344e3 100644 --- a/src/packages/arithmetic.rs +++ b/src/packages/arithmetic.rs @@ -143,6 +143,10 @@ macro_rules! gen_signed_functions { Ok(Dynamic::from(-x)) } } + #[rhai_fn(name = "+")] + pub fn plus(x: $arg_type) -> $arg_type { + x + } #[rhai_fn(return_raw)] pub fn abs(x: $arg_type) -> Result> { if cfg!(not(feature = "unchecked")) { @@ -251,6 +255,10 @@ mod f32_functions { pub fn neg(x: f32) -> f32 { -x } + #[rhai_fn(name = "+")] + pub fn plus(x: f32) -> f32 { + -x + } pub fn abs(x: f32) -> f32 { x.abs() } @@ -310,6 +318,10 @@ mod f64_functions { pub fn neg(x: f64) -> f64 { -x } + #[rhai_fn(name = "+")] + pub fn plus(x: f64) -> f64 { + -x + } pub fn abs(x: f64) -> f64 { x.abs() } diff --git a/src/packages/fn_basic.rs b/src/packages/fn_basic.rs index e4592c33..4b241f55 100644 --- a/src/packages/fn_basic.rs +++ b/src/packages/fn_basic.rs @@ -128,8 +128,8 @@ fn collect_fn_metadata(ctx: NativeCallContext) -> Array { .for_each(|(_, _, _, _, f)| list.push(make_metadata(&dict, None, f).into())); if let Some(mods) = ctx.mods { - mods.iter() - .for_each(|(ns, m)| scan_module(&mut list, &dict, ns, m.as_ref())); + mods.iter_raw() + .for_each(|(ns, m)| scan_module(&mut list, &dict, ns.clone(), m.as_ref())); } list diff --git a/src/packages/mod.rs b/src/packages/mod.rs index bc764554..ca42cced 100644 --- a/src/packages/mod.rs +++ b/src/packages/mod.rs @@ -64,15 +64,23 @@ impl PackagesCollection { /// Does the specified function hash key exist in the [`PackagesCollection`]? #[allow(dead_code)] pub fn contains_fn(&self, hash: u64) -> bool { - self.0 - .as_ref() - .map_or(false, |x| x.iter().any(|p| p.contains_fn(hash, false))) + if hash == 0 { + false + } else { + self.0 + .as_ref() + .map_or(false, |x| x.iter().any(|p| p.contains_fn(hash, false))) + } } /// Get specified function via its hash key. pub fn get_fn(&self, hash: u64) -> Option<&CallableFunction> { - self.0 - .as_ref() - .and_then(|x| x.iter().find_map(|p| p.get_fn(hash, false))) + if hash == 0 { + None + } else { + self.0 + .as_ref() + .and_then(|x| x.iter().find_map(|p| p.get_fn(hash, false))) + } } /// Does the specified [`TypeId`] iterator exist in the [`PackagesCollection`]? #[allow(dead_code)] diff --git a/src/parser.rs b/src/parser.rs index f0003abd..6dfe48c3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -357,6 +357,7 @@ fn parse_fn_call( return Ok(Expr::FnCall( Box::new(FnCallExpr { name: id.to_string().into(), + native_only: !is_valid_identifier(id.chars()), // script functions can only be valid identifiers capture, namespace, hash: hash_script, @@ -404,6 +405,7 @@ fn parse_fn_call( return Ok(Expr::FnCall( Box::new(FnCallExpr { name: id.to_string().into(), + native_only: !is_valid_identifier(id.chars()), // script functions can only be valid identifiers capture, namespace, hash: hash_script, @@ -1103,6 +1105,12 @@ fn parse_primary( (expr, Token::LeftBracket) => { parse_index_chain(input, state, lib, expr, settings.level_up())? } + // Method access + #[cfg(not(feature = "no_object"))] + (expr, Token::Period) => { + let rhs = parse_unary(input, state, lib, settings.level_up())?; + make_dot_expr(state, expr, rhs, token_pos)? + } // Unknown postfix operator (expr, token) => unreachable!( "unknown postfix operator '{}' for {:?}", @@ -1112,20 +1120,25 @@ fn parse_primary( } } + // Cache the hash key for namespace-qualified variables match &mut root_expr { - // Cache the hash key for namespace-qualified variables - Expr::Variable(x) if x.1.is_some() => { - let (_, modules, hash, IdentX { name, .. }) = x.as_mut(); - let namespace = modules.as_mut().unwrap(); - - // Qualifiers + variable name - *hash = calc_script_fn_hash(namespace.iter().map(|v| v.name.as_str()), name, 0); - - #[cfg(not(feature = "no_module"))] - namespace.set_index(state.find_module(&namespace[0].name)); - } - _ => (), + Expr::Variable(x) if x.1.is_some() => Some(x), + Expr::Index(x, _) | Expr::Dot(x, _) => match &mut x.lhs { + Expr::Variable(x) if x.1.is_some() => Some(x), + _ => None, + }, + _ => None, } + .map(|x| { + let (_, modules, hash, IdentX { name, .. }) = x.as_mut(); + let namespace = modules.as_mut().unwrap(); + + // Qualifiers + variable name + *hash = calc_script_fn_hash(namespace.iter().map(|v| v.name.as_str()), name, 0); + + #[cfg(not(feature = "no_module"))] + namespace.set_index(state.find_module(&namespace[0].name)); + }); // Make sure identifiers are valid Ok(root_expr) @@ -1199,8 +1212,33 @@ fn parse_unary( } // +expr Token::UnaryPlus => { - eat_token(input, Token::UnaryPlus); - parse_unary(input, state, lib, settings.level_up()) + let pos = eat_token(input, Token::UnaryPlus); + + match parse_unary(input, state, lib, settings.level_up())? { + expr @ Expr::IntegerConstant(_, _) => Ok(expr), + #[cfg(not(feature = "no_float"))] + expr @ Expr::FloatConstant(_, _) => Ok(expr), + + // Call plus function + expr => { + let op = "+"; + let hash = calc_script_fn_hash(empty(), op, 1); + let mut args = StaticVec::new(); + args.push(expr); + + Ok(Expr::FnCall( + Box::new(FnCallExpr { + name: op.into(), + native_only: true, + namespace: None, + hash, + args, + ..Default::default() + }), + pos, + )) + } + } } // !expr Token::Bang => { @@ -1768,6 +1806,7 @@ fn parse_binary_op( make_in_expr(current_lhs, rhs, pos)? } + // This is needed to parse closure followed by a dot. #[cfg(not(feature = "no_object"))] Token::Period => { let rhs = args.pop().unwrap(); diff --git a/src/serde_impl/metadata.rs b/src/serde_impl/metadata.rs new file mode 100644 index 00000000..ef291705 --- /dev/null +++ b/src/serde_impl/metadata.rs @@ -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 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 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, +} + +impl PartialOrd for FnParam { + fn partial_cmp(&self, other: &Self) -> Option { + 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, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub return_type: Option, + pub signature: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub doc_comments: Option>, +} + +impl PartialOrd for FnMetadata { + fn partial_cmp(&self, other: &Self) -> Option { + 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> 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, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub functions: Vec, +} + +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 { + 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) + } +} diff --git a/src/serde_impl/mod.rs b/src/serde_impl/mod.rs index da853425..d38105a1 100644 --- a/src/serde_impl/mod.rs +++ b/src/serde_impl/mod.rs @@ -3,3 +3,6 @@ pub mod de; pub mod ser; mod str; + +#[cfg(feature = "metadata")] +pub mod metadata; diff --git a/src/token.rs b/src/token.rs index e6f3588a..ee3febb0 100644 --- a/src/token.rs +++ b/src/token.rs @@ -354,9 +354,7 @@ impl Token { Reserved(s) => s.clone().into(), Custom(s) => s.clone().into(), LexError(err) => err.to_string().into(), - - Comment(s) if is_doc_comment(s) => s[..3].to_string().into(), - Comment(s) => s[..2].to_string().into(), + Comment(s) => s.clone().into(), token => match token { LeftBrace => "{", @@ -759,6 +757,8 @@ pub struct TokenizeState { pub end_with_none: bool, /// Include comments? pub include_comments: bool, + /// Disable doc-comments? + pub disable_doc_comments: bool, } /// _(INTERNALS)_ Trait that encapsulates a peekable character input stream. @@ -1020,7 +1020,8 @@ fn is_binary_char(c: char) -> bool { /// Test if the comment block is a doc-comment. #[inline(always)] pub fn is_doc_comment(comment: &str) -> bool { - comment.starts_with("///") || comment.starts_with("/**") + (comment.starts_with("///") && !comment.starts_with("////")) + || (comment.starts_with("/**") && !comment.starts_with("/***")) } /// Get the next token. @@ -1040,7 +1041,9 @@ fn get_next_token_inner( state.comment_level = scan_block_comment(stream, state.comment_level, pos, &mut comment); - if state.include_comments || is_doc_comment(comment.as_ref().unwrap()) { + if state.include_comments + || (!state.disable_doc_comments && is_doc_comment(comment.as_ref().unwrap())) + { return Some((Token::Comment(comment.unwrap()), start_pos)); } } @@ -1288,9 +1291,14 @@ fn get_next_token_inner( eat_next(stream, pos); let mut comment = match stream.peek_next() { - Some('/') => { + Some('/') if !state.disable_doc_comments => { eat_next(stream, pos); - Some("///".to_string()) + + // Long streams of `///...` are not doc-comments + match stream.peek_next() { + Some('/') => None, + _ => Some("///".to_string()), + } } _ if state.include_comments => Some("//".to_string()), _ => None, @@ -1316,9 +1324,14 @@ fn get_next_token_inner( eat_next(stream, pos); let mut comment = match stream.peek_next() { - Some('*') => { + Some('*') if !state.disable_doc_comments => { eat_next(stream, pos); - Some("/**".to_string()) + + // Long streams of `/****...` are not doc-comments + match stream.peek_next() { + Some('*') => None, + _ => Some("/**".to_string()), + } } _ if state.include_comments => Some("/*".to_string()), _ => None, @@ -1785,6 +1798,7 @@ impl Engine { comment_level: 0, end_with_none: false, include_comments: false, + disable_doc_comments: self.disable_doc_comments, }, pos: Position::new(1, 0), stream: MultiInputsStream { diff --git a/tests/comments.rs b/tests/comments.rs index 17746a94..81aa2369 100644 --- a/tests/comments.rs +++ b/tests/comments.rs @@ -29,7 +29,7 @@ fn test_comments() -> Result<(), Box> { #[cfg(not(feature = "no_function"))] #[test] fn test_comments_doc() -> Result<(), Box> { - let engine = Engine::new(); + let mut engine = Engine::new(); let ast = engine.compile( r" @@ -54,6 +54,16 @@ fn test_comments_doc() -> Result<(), Box> { ) .is_err()); + engine.compile( + r" + /////////////// + let x = 42; + + /***************/ + let x = 42; + ", + )?; + let ast = engine.compile( r" /** Hello world @@ -78,5 +88,17 @@ fn test_comments_doc() -> Result<(), Box> { ) .is_err()); + engine.set_doc_comments(false); + + engine.compile( + r" + /// Hello world! + let x = 42; + + /** Hello world! */ + let x = 42; + ", + )?; + Ok(()) } diff --git a/tests/modules.rs b/tests/modules.rs index ef148bc9..656dc728 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -48,6 +48,7 @@ fn test_module_sub_module() -> Result<(), Box> { assert_eq!(engine.eval::("question::MYSTIC_NUMBER")?, 42); assert!(engine.eval::("MYSTIC_NUMBER").is_err()); + assert_eq!(engine.eval::("question::life::universe::answer")?, 41); assert_eq!( engine.eval::("question::life::universe::answer + 1")?, 42 @@ -60,6 +61,8 @@ fn test_module_sub_module() -> Result<(), Box> { .eval::("inc(question::life::universe::answer)") .is_err()); #[cfg(not(feature = "no_object"))] + assert_eq!(engine.eval::("question::MYSTIC_NUMBER.doubled")?, 84); + #[cfg(not(feature = "no_object"))] assert_eq!( engine.eval::("question::life::universe::answer.doubled")?, 82 diff --git a/tests/optimizer.rs b/tests/optimizer.rs index bffc2633..b6468d03 100644 --- a/tests/optimizer.rs +++ b/tests/optimizer.rs @@ -55,17 +55,20 @@ fn test_optimizer_parse() -> Result<(), Box> { let ast = engine.compile("{ const DECISION = false; if DECISION { 42 } else { 123 } }")?; - assert!(format!("{:?}", ast).starts_with(r#"AST { statements: [Block([Const(IdentX { name: "DECISION", pos: 1:9 }, Some(Unit(0:0)), false, 1:3), Expr(IntegerConstant(123, 1:53))], 1:1)]"#)); + assert!(format!("{:?}", ast).starts_with(r#"AST { source: None, statements: [Block([Const(IdentX { name: "DECISION", pos: 1:9 }, Some(Unit(0:0)), false, 1:3), Expr(IntegerConstant(123, 1:53))], 1:1)]"#)); let ast = engine.compile("if 1 == 2 { 42 }")?; - assert!(format!("{:?}", ast).starts_with("AST { statements: [], functions: Module(")); + assert!( + format!("{:?}", ast).starts_with("AST { source: None, statements: [], functions: Module(") + ); engine.set_optimization_level(OptimizationLevel::Full); let ast = engine.compile("abs(-42)")?; - assert!(format!("{:?}", ast).starts_with(r"AST { statements: [Expr(IntegerConstant(42, 1:1))]")); + assert!(format!("{:?}", ast) + .starts_with(r"AST { source: None, statements: [Expr(IntegerConstant(42, 1:1))]")); Ok(()) } diff --git a/tests/print.rs b/tests/print.rs index 57471323..c61cae49 100644 --- a/tests/print.rs +++ b/tests/print.rs @@ -13,20 +13,28 @@ fn test_print_debug() -> Result<(), Box> { engine .on_print(move |s| log1.write().unwrap().push(format!("entry: {}", s))) - .on_debug(move |s, pos| { - log2.write() - .unwrap() - .push(format!("DEBUG at {:?}: {}", pos, s)) + .on_debug(move |s, src, pos| { + log2.write().unwrap().push(format!( + "DEBUG of {} at {:?}: {}", + src.unwrap_or("unknown"), + pos, + s + )) }); // Evaluate script - engine.eval::<()>("print(40 + 2)")?; - engine.eval::<()>(r#"let x = "hello!"; debug(x)"#)?; + engine.consume("print(40 + 2)")?; + let mut ast = engine.compile(r#"let x = "hello!"; debug(x)"#)?; + ast.set_source(Some("world")); + engine.consume_ast(&ast)?; // 'logbook' captures all the 'print' and 'debug' output assert_eq!(logbook.read().unwrap().len(), 2); assert_eq!(logbook.read().unwrap()[0], "entry: 42"); - assert_eq!(logbook.read().unwrap()[1], r#"DEBUG at 1:19: "hello!""#); + assert_eq!( + logbook.read().unwrap()[1], + r#"DEBUG of world at 1:19: "hello!""# + ); for entry in logbook.read().unwrap().iter() { println!("{}", entry);