Merge branch 'master' into plugins

This commit is contained in:
Stephen Chung 2020-08-18 22:01:57 +08:00
commit 096a009418
6 changed files with 157 additions and 11 deletions

View File

@ -9,6 +9,7 @@ New features
* Adds `Engine::register_get_result`, `Engine::register_set_result`, `Engine::register_indexer_get_result`, `Engine::register_indexer_set_result` API.
* Adds `Module::combine` to combine two modules.
* `Engine::parse_json` now also accepts a JSON object starting with `#{`.
Version 0.18.1

View File

@ -88,6 +88,7 @@ The Rhai Scripting Language
4. [Create from AST](language/modules/ast.md)
5. [Module Resolvers](rust/modules/resolvers.md)
1. [Custom Implementation](rust/modules/imp-resolver.md)
18. [Eval Statement](language/eval.md)
6. [Safety and Protection](safety/index.md)
1. [Checked Arithmetic](safety/checked.md)
2. [Sand-Boxing](safety/sandbox.md)
@ -119,7 +120,7 @@ The Rhai Scripting Language
1. [Disable Keywords and/or Operators](engine/disable.md)
2. [Custom Operators](engine/custom-op.md)
3. [Extending with Custom Syntax](engine/custom-syntax.md)
7. [Eval Statement](language/eval.md)
7. [Multiple Instantiation](patterns/multiple.md)
8. [Appendix](appendix/index.md)
1. [Keywords](appendix/keywords.md)
2. [Operators and Symbols](appendix/operators.md)

View File

@ -7,7 +7,7 @@ The syntax for an [object map] is extremely similar to JSON, with the exception
technically be mapped to [`()`]. A valid JSON string does not start with a hash character `#` while a
Rhai [object map] does - that's the major difference!
Use the `Engine::parse_json` method to parse a piece of JSON into an object map:
Use the `Engine::parse_json` method to parse a piece of _simple_ JSON into an object map:
```rust
// JSON string - notice that JSON property names are always quoted
@ -26,7 +26,7 @@ let json = r#"{
// Set the second boolean parameter to true in order to map 'null' to '()'
let map = engine.parse_json(json, true)?;
map.len() == 6; // 'map' contains all properties in the JSON string
map.len() == 6; // 'map' contains all properties in the JSON string
// Put the object map into a 'Scope'
let mut scope = Scope::new();
@ -34,7 +34,7 @@ scope.push("map", map);
let result = engine.eval_with_scope::<INT>(r#"map["^^^!!!"].len()"#)?;
result == 3; // the object map is successfully used in the script
result == 3; // the object map is successfully used in the script
```
Representation of Numbers
@ -45,3 +45,28 @@ the [`no_float`] feature is not used. Most common generators of JSON data disti
integer and floating-point values by always serializing a floating-point number with a decimal point
(i.e. `123.0` instead of `123` which is assumed to be an integer). This style can be used successfully
with Rhai [object maps].
Parse JSON with Sub-Objects
--------------------------
`Engine::parse_json` depends on the fact that the [object map] literal syntax in Rhai is _almost_
the same as a JSON object. However, it is _almost_ because the syntax for a sub-object in JSON
(i.e. "`{ ... }`") is different from a Rhai [object map] literal (i.e. "`#{ ... }`").
When `Engine::parse_json` encounters JSON with sub-objects, it fails with a syntax error.
If it is certain that no text string in the JSON will ever contain the character '`{`',
then it is possible to parse it by first replacing all occupance of '`{`' with "`#{`".
```rust
// JSON with sub-object 'b'.
let json = r#"{"a":1, "b":{"x":true, "y":false}}"#;
let new_json = json.replace("{" "#{");
// The leading '{' will also be replaced to '#{', but parse_json can handle this.
let map = engine.parse_json(&new_json, false)?;
map.len() == 2; // 'map' contains two properties: 'a' and 'b'
```

View File

@ -0,0 +1,89 @@
Multiple Instantiation
======================
{{#include ../links.md}}
Background
----------
Rhai's [features] are not strictly additive. This is easily deduced from the [`no_std`] feature
which prepares the crate for `no-std` builds. Obviously, turning on this feature has a material
impact on how Rhai behaves.
Many crates resolve this by going the opposite direction: build for `no-std` in default,
but add a `std` feature, included by default, which builds for the `stdlib`.
Rhai Language Features Are Not Additive
--------------------------------------
Rhai, however, is more complex. Language features cannot be easily made _additive_.
That is because the _lack_ of a language feature is a feature by itself.
For example, by including [`no_float`], a project sets the Rhai language to ignore floating-point math.
Floating-point numbers do not even parse under this case and will generate syntax errors.
Assume that the project expects this behavior (why? perhaps integers are all that make sense
within the project domain).
Now, assume that a dependent crate also depends on Rhai. Under such circumstances,
unless _exact_ versioning is used and the dependent crate depends on a _different_ version
of Rhai, Cargo automatically _merges_ both dependencies, with the [`no_float`] feature turned on
because Cargo features are _additive_.
This will break the dependent crate, which does not by itself specify [`no_float`]
and expects floating-point numbers and math to work normally.
There is no way out of this dilemma. Reversing the [features] set with a `float` feature
causes the project to break because floating-point numbers are not rejected as expected.
Multiple Instantiations of Rhai Within The Same Project
------------------------------------------------------
The trick is to differentiate between multiple identical copies of Rhai, each having
a different [features] set, by their _sources_:
* Different versions from [`crates.io`](https://crates.io/crates/rhai/) - The official crate.
* Different releases from [`GitHub`](https://github.com/jonathandturner/rhai) - Crate source on GitHub.
* Forked copy of [https://github.com/jonathandturner/rhai](https://github.com/jonathandturner/rhai) on GitHub.
* Local copy of [https://github.com/jonathandturner/rhai](https://github.com/jonathandturner/rhai) downloaded form GitHub.
Use the following configuration in `Cargo.toml` to pull in multiple copies of Rhai within the same project:
```toml
[dependencies]
rhai = { version = "{{version}}", features = [ "no_float" ] }
rhai_github = { git = "https://github.com/jonathandturner/rhai", features = [ "unchecked" ] }
rhai_my_github = { git = "https://github.com/my_github/rhai", branch = "variation1", features = [ "serde", "no_closure" ] }
rhai_local = { path = "../rhai_copy" }
```
The example above creates four different modules: `rhai`, `rhai_github`, `rhai_my_github` and
`rhai_local`, each referring to a different Rhai copy with the appropriate [features] set.
Only one crate of any particular version can be used from each source, because Cargo merges
all candidate cases within the same source, adding all [features] together.
If more than four different instantiations of Rhai is necessary (why?), create more local repositories
or GitHub forks or branches.
Caveat - No Way To Avoid Dependency Conflicts
--------------------------------------------
Unfortunately, pulling in Rhai from different sources do not resolve the problem of
[features] conflict between dependencies. Even overriding `crates.io` via the `[patch]` manifest
section doesn't work - all dependencies will eventually find the only one copy.
What is necessary - multiple copies of Rhai, one for each dependent crate that requires it,
together with their _unique_ [features] set intact. In other words, turning off Cargo's
crate merging feature _just for Rhai_.
Unfortunately, as of this writing, there is no known method to achieve it.
Therefore, moral of the story: avoid pulling in multiple crates that depend on Rhai.

View File

@ -52,3 +52,15 @@ no floating-point, is `Send + Sync` (so it can be safely used across threads), a
nor loading external [modules].
This configuration is perfect for an expression parser in a 32-bit embedded system without floating-point hardware.
Caveat - Features Are Not Additive
---------------------------------
Rhai features are not strictly _additive_ - i.e. they do not only add optional functionalities.
In fact, most features are _subtractive_ - i.e. they _remove_ functionalities.
There is a reason for this design, because the _lack_ of a language feature by itself is a feature.
See [here]({{rootUrl}}/patterns/multiple.md) for more details.

View File

@ -898,21 +898,34 @@ impl Engine {
/// Set `has_null` to `true` in order to map `null` values to `()`.
/// Setting it to `false` will cause a _variable not found_ error during parsing.
///
/// # JSON With Sub-Objects
///
/// This method assumes no sub-objects in the JSON string. That is because the syntax
/// of a JSON sub-object (or object hash), `{ .. }`, is different from Rhai's syntax, `#{ .. }`.
/// Parsing a JSON string with sub-objects will cause a syntax error.
///
/// If it is certain that the character `{` never appears in any text string within the JSON object,
/// then globally replace `{` with `#{` before calling this method.
///
/// # Example
///
/// ```
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
/// use rhai::Engine;
/// use rhai::{Engine, Map};
///
/// let engine = Engine::new();
///
/// let map = engine.parse_json(r#"{"a":123, "b":42, "c":false, "d":null}"#, true)?;
/// let map = engine.parse_json(
/// r#"{"a":123, "b":42, "c":{"x":false, "y":true}, "d":null}"#
/// .replace("{", "#{").as_str(), true)?;
///
/// assert_eq!(map.len(), 4);
/// assert_eq!(map.get("a").cloned().unwrap().cast::<i64>(), 123);
/// assert_eq!(map.get("b").cloned().unwrap().cast::<i64>(), 42);
/// assert_eq!(map.get("c").cloned().unwrap().cast::<bool>(), false);
/// assert_eq!(map.get("d").cloned().unwrap().cast::<()>(), ());
/// assert_eq!(map["a"].as_int().unwrap(), 123);
/// assert_eq!(map["b"].as_int().unwrap(), 42);
/// assert!(map["d"].is::<()>());
///
/// let c = map["c"].read_lock::<Map>().unwrap();
/// assert_eq!(c["x"].as_bool().unwrap(), false);
/// # Ok(())
/// # }
/// ```
@ -921,7 +934,12 @@ impl Engine {
let mut scope = Scope::new();
// Trims the JSON string and add a '#' in front
let scripts = ["#", json.trim()];
let json = json.trim();
let scripts = if json.starts_with(Token::MapStart.syntax().as_ref()) {
[json, ""]
} else {
["#", json]
};
let stream = lex(
&scripts,
if has_null {