Merge pull request #223 from schungx/master

Fix bugs.
This commit is contained in:
Stephen Chung 2020-08-20 17:28:37 +08:00 committed by GitHub
commit 87685f653b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 402 additions and 85 deletions

View File

@ -4,10 +4,18 @@ Rhai Release Notes
Version 0.19.0
==============
Bug fixes
---------
* Fixes bug that prevents calling functions in closures.
* Fixes bug that erroneously consumes the first argument to a module-qualified function call.
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

@ -3,11 +3,14 @@ Parse an Object Map from JSON
{{#include ../links.md}}
The syntax for an [object map] is extremely similar to JSON, with the exception of `null` values which can
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!
The syntax for an [object map] is extremely similar to the JSON representation of a object hash,
with the exception of `null` values which can technically be mapped to [`()`].
Use the `Engine::parse_json` method to parse a piece of JSON into an object map:
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.
The JSON text must represent a single object hash (i.e. must be wrapped within "`{ .. }`")
otherwise it returns a syntax error.
```rust
// JSON string - notice that JSON property names are always quoted
@ -26,7 +29,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 +37,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 +48,30 @@ 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 "`#{`".
A JSON object hash starting with `#{` is handled transparently by `Engine::parse_json`.
```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

@ -63,16 +63,5 @@ For example, the `len` method of an [array] has the signature: `Fn(&mut Array) -
The array itself is not modified in any way, but using a `&mut` parameter avoids a cloning that would
otherwise have happened if the signature were `Fn(Array) -> INT`.
For primary types that are cheap to clone (e.g. those that implement `Copy`),
including `ImmutableString`, this is not necessary.
Avoid `&mut ImmutableString`
---------------------------
`ImmutableString`, Rhai's internal [string] type, is an exception.
`ImmutableString` is cheap to clone, but expensive to take a mutable reference (because the underlying
string must be cloned to make a private copy).
Therefore, avoid using `&mut ImmutableString` unless the intention is to mutate it.
For primary types that are cheap to clone (e.g. those that implement `Copy`), including `ImmutableString`,
this is not necessary.

View File

@ -46,7 +46,7 @@ struct Config {
### Make Shared Object
```rust
let config: Rc<RefCell<Config>> = Rc::new(RefCell::(Default::default()));
let config: Rc<RefCell<Config>> = Rc::new(RefCell::new(Default::default()));
```
### Register Config API
@ -76,7 +76,17 @@ engine.register_fn("config_add", move |values: &mut Array|
let cfg = config.clone();
engine.register_fn("config_add", move |key: String, value: bool|
cfg.borrow_mut().som_map.insert(key, value)
cfg.borrow_mut().some_map.insert(key, value)
);
let cfg = config.clone();
engine.register_fn("config_contains", move |value: String|
cfg.borrow().some_list.contains(&value)
);
let cfg = config.clone();
engine.register_fn("config_is_set", move |value: String|
cfg.borrow().some_map.get(&value).cloned().unwrap_or(false)
);
```
@ -91,7 +101,10 @@ config_set_id("hello");
config_add("foo"); // add to list
config_add("bar", true); // add to map
config_add("baz", false); // add to map
if config_contains("hey") || config_is_set("hey") {
config_add("baz", false); // add to map
}
```
### Load the Configuration
@ -103,3 +116,35 @@ let id = config_get_id();
id == "hello";
```
Consider a Custom Syntax
------------------------
This is probably one of the few scenarios where a [custom syntax] can be recommended.
A properly-designed [custom syntax] can make the configuration file clean, simple to write,
easy to understand and quick to modify.
For example, the above configuration example may be expressed by this custom syntax:
```rust
------------------
| my_config.rhai |
------------------
// Configure ID
id "hello";
// Add to list
list +"foo"
// Add to map
map "bar" => true;
if config contains "hey" || config is_set "hey" {
map "baz" => false;
}
```
Notice that `contains` and `is_set` may be implemented as a [custom operator].

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

@ -33,7 +33,7 @@ engine.register_raw_fn(
// But remember this is Rust, so you can keep only one mutable reference at any one time!
// Therefore, get a '&mut' reference to the first argument _last_.
// Alternatively, use `args.split_at_mut(1)` etc. to split the slice first.
// Alternatively, use `args.split_first_mut()` etc. to split the slice first.
let y: i64 = *args[1].read_lock::<i64>() // get a reference to the second argument
.unwrap(); // then copying it because it is a primary type
@ -163,17 +163,17 @@ Hold Multiple References
------------------------
In order to access a value argument that is expensive to clone _while_ holding a mutable reference
to the first argument, either _consume_ that argument via `mem::take` as above, or use `args.split_at`
to the first argument, either _consume_ that argument via `mem::take` as above, or use `args.split_first`
to partition the slice:
```rust
// Partition the slice
let (first, rest) = args.split_at_mut(1);
let (first, rest) = args.split_first_mut().unwrap();
// Mutable reference to the first parameter
let this_ptr: &mut Dynamic = &mut *first[0].write_lock::<A>().unwrap();
let this_ptr: &mut A = &mut *first.write_lock::<A>().unwrap();
// Immutable reference to the second value parameter
// This can be mutable but there is no point because the parameter is passed by value
let value_ref: &Dynamic = &*rest[0].read_lock::<B>().unwrap();
let value_ref: &B = &*rest[0].read_lock::<B>().unwrap();
```

View File

@ -41,25 +41,3 @@ let len = engine.eval::<i64>("x.len1()")?; // 'x' is cloned, ve
let len = engine.eval::<i64>("x.len2()")?; // 'x' is shared
let len = engine.eval::<i64>("x.len3()")?; // 'x' is shared
```
Avoid `&mut ImmutableString`
---------------------------
Rhai functions can take a first `&mut` parameter. Usually this is a good idea because it avoids
cloning of the argument (except for primary types where cloning is cheap), so its use is encouraged
even though there is no intention to ever mutate that argument.
[`ImmutableString`][string] is an exception to this rule.
While `ImmutableString` is cheap to clone (only incrementing a reference count), taking a mutable
reference to it involves making a private clone of the underlying string because Rhai has no way
to find out whether that parameter will be mutated.
If the `ImmutableString` is not shared by any other variables, then Rhai just returns a mutable
reference to it since nobody else is watching! Otherwise a private copy is made first,
because other reference holders will not expect the `ImmutableString` to ever change
(it is supposed to be _immutable_).
Therefore, avoid using `&mut ImmutableString` as the first parameter of a function unless you really
intend to mutate that string. Use `ImmutableString` instead.

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

@ -2,7 +2,7 @@
use crate::any::{Dynamic, Variant};
use crate::engine::{Engine, Imports, State};
use crate::error::ParseError;
use crate::error::{ParseError, ParseErrorType};
use crate::fn_native::{IteratorFn, SendSync};
use crate::module::{FuncReturn, Module};
use crate::optimize::OptimizationLevel;
@ -34,6 +34,7 @@ use crate::optimize::optimize_into_ast;
use crate::stdlib::{
any::{type_name, TypeId},
boxed::Box,
string::ToString,
};
#[cfg(not(feature = "no_optimize"))]
@ -895,24 +896,39 @@ impl Engine {
/// Parse a JSON string into a map.
///
/// The JSON string must be an object hash. It cannot be a simple JavaScript primitive.
///
/// 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 +937,20 @@ impl Engine {
let mut scope = Scope::new();
// Trims the JSON string and add a '#' in front
let scripts = ["#", json.trim()];
let json_text = json.trim_start();
let scripts = if json_text.starts_with(Token::MapStart.syntax().as_ref()) {
[json_text, ""]
} else if json_text.starts_with(Token::LeftBrace.syntax().as_ref()) {
["#", json_text]
} else {
return Err(ParseErrorType::MissingToken(
Token::LeftBrace.syntax().to_string(),
"to start a JSON object hash".to_string(),
)
.into_err(Position::new(1, (json.len() - json_text.len() + 1) as u16))
.into());
};
let stream = lex(
&scripts,
if has_null {

View File

@ -521,13 +521,13 @@ impl Engine {
let result = if _is_method {
// Method call of script function - map first argument to `this`
let (first, rest) = args.split_at_mut(1);
let (first, rest) = args.split_first_mut().unwrap();
self.call_script_fn(
scope,
mods,
state,
lib,
&mut Some(first[0]),
&mut Some(*first),
fn_name,
func,
rest,
@ -974,6 +974,7 @@ impl Engine {
) -> Result<Dynamic, Box<EvalAltResult>> {
let modules = modules.as_ref().unwrap();
let mut arg_values: StaticVec<_>;
let mut first_arg_value = None;
let mut args: StaticVec<_>;
if args_expr.is_empty() {
@ -988,17 +989,28 @@ impl Engine {
Expr::Variable(x) if x.1.is_none() => {
arg_values = args_expr
.iter()
.skip(1)
.map(|expr| self.eval_expr(scope, mods, state, lib, this_ptr, expr, level))
.enumerate()
.map(|(i, expr)| {
// Skip the first argument
if i == 0 {
Ok(Default::default())
} else {
self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)
}
})
.collect::<Result<_, _>>()?;
// Get target reference to first argument
let (target, _, _, pos) =
search_scope_only(scope, state, this_ptr, args_expr.get(0).unwrap())?;
self.inc_operations(state)
.map_err(|err| err.new_position(pos))?;
args = once(target).chain(arg_values.iter_mut()).collect();
let (first, rest) = arg_values.split_first_mut().unwrap();
first_arg_value = Some(first);
args = once(target).chain(rest.iter_mut()).collect();
}
// func(..., ...) or func(mod::x, ...)
_ => {
@ -1037,6 +1049,13 @@ impl Engine {
match func {
#[cfg(not(feature = "no_function"))]
Some(f) if f.is_script() => {
// Clone first argument
if let Some(first) = first_arg_value {
let first_val = args[0].clone();
args[0] = first;
*args[0] = first_val;
}
let args = args.as_mut();
let func = f.get_fn_def();
@ -1055,7 +1074,19 @@ impl Engine {
self.call_script_fn(scope, mods, state, lib, &mut None, name, func, args, level)
}
Some(f) => f.get_native_fn()(self, lib, args.as_mut()),
Some(f) if f.is_native() => {
if !f.is_method() {
// Clone first argument
if let Some(first) = first_arg_value {
let first_val = args[0].clone();
args[0] = first;
*args[0] = first_val;
}
}
f.get_native_fn()(self, lib, args.as_mut())
}
Some(_) => unreachable!(),
None if def_val.is_some() => Ok(def_val.unwrap().into()),
None => EvalAltResult::ErrorFunctionNotFound(
format!(

View File

@ -611,9 +611,9 @@ impl Module {
) -> u64 {
let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| {
let b = mem::take(args[1]).cast::<B>();
let mut a = args[0].write_lock::<A>().unwrap();
let a = &mut args[0].write_lock::<A>().unwrap();
func(&mut a, b).map(Dynamic::from)
func(a, b).map(Dynamic::from)
};
let arg_types = [TypeId::of::<A>(), TypeId::of::<B>()];
self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f)))
@ -735,9 +735,9 @@ impl Module {
let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| {
let b = mem::take(args[1]).cast::<B>();
let c = mem::take(args[2]).cast::<C>();
let mut a = args[0].write_lock::<A>().unwrap();
let a = &mut args[0].write_lock::<A>().unwrap();
func(&mut a, b, c).map(Dynamic::from)
func(a, b, c).map(Dynamic::from)
};
let arg_types = [TypeId::of::<A>(), TypeId::of::<B>(), TypeId::of::<C>()];
self.set_fn(name, Public, &arg_types, Func::from_method(Box::new(f)))
@ -769,9 +769,9 @@ impl Module {
let f = move |_: &Engine, _: &Module, args: &mut FnCallArgs| {
let b = mem::take(args[1]).cast::<B>();
let c = mem::take(args[2]).cast::<C>();
let mut a = args[0].write_lock::<A>().unwrap();
let a = &mut args[0].write_lock::<A>().unwrap();
func(&mut a, b, c).map(Dynamic::from)
func(a, b, c).map(Dynamic::from)
};
let arg_types = [TypeId::of::<A>(), TypeId::of::<B>(), TypeId::of::<C>()];
self.set_fn(
@ -892,9 +892,9 @@ impl Module {
let b = mem::take(args[1]).cast::<B>();
let c = mem::take(args[2]).cast::<C>();
let d = mem::take(args[3]).cast::<D>();
let mut a = args[0].write_lock::<A>().unwrap();
let a = &mut args[0].write_lock::<A>().unwrap();
func(&mut a, b, c, d).map(Dynamic::from)
func(a, b, c, d).map(Dynamic::from)
};
let arg_types = [
TypeId::of::<A>(),
@ -928,6 +928,18 @@ impl Module {
self.all_functions.get(&hash_qualified_fn)
}
/// Combine another module into this module.
/// The other module is consumed to merge into this module.
pub fn combine(&mut self, other: Self) -> &mut Self {
self.variables.extend(other.variables.into_iter());
self.functions.extend(other.functions.into_iter());
self.type_iterators.extend(other.type_iterators.into_iter());
self.all_functions.clear();
self.all_variables.clear();
self.indexed = false;
self
}
/// Merge another module into this module.
pub fn merge(&mut self, other: &Self) -> &mut Self {
self.merge_filtered(other, |_, _, _| true)

View File

@ -414,7 +414,8 @@ struct ParseState<'e> {
/// Tracks a list of external variables (variables that are not explicitly declared in the scope).
#[cfg(not(feature = "no_closure"))]
externals: HashMap<String, Position>,
/// An indicator that disables variable capturing into externals one single time.
/// An indicator that disables variable capturing into externals one single time
/// up until the nearest consumed Identifier token.
/// If set to false the next call to `access_var` will not capture the variable.
/// All consequent calls to `access_var` will not be affected
#[cfg(not(feature = "no_closure"))]
@ -1637,6 +1638,11 @@ fn parse_primary(
// Function call
Token::Identifier(s) if *next_token == Token::LeftParen || *next_token == Token::Bang => {
// Once the identifier consumed we must enable next variables capturing
#[cfg(not(feature = "no_closure"))]
{
state.allow_capture = true;
}
Expr::Variable(Box::new(((s, settings.pos), None, 0, None)))
}
// Normal variable access

View File

@ -1,6 +1,7 @@
#![cfg(not(feature = "no_function"))]
use rhai::{Dynamic, Engine, EvalAltResult, FnPtr, Module, RegisterFn, INT};
use std::any::TypeId;
use std::mem::take;
#[test]
fn test_fn_ptr_curry_call() -> Result<(), Box<EvalAltResult>> {
@ -88,6 +89,59 @@ fn test_closures() -> Result<(), Box<EvalAltResult>> {
42
);
assert_eq!(
engine.eval::<INT>(
r#"
let a = 40;
let f = |x| {
let f = |x| {
let f = |x| plus_one(a) + x;
f.call(x)
};
f.call(x)
};
f.call(1)
"#
)?,
42
);
assert_eq!(
engine.eval::<INT>(
r#"
let a = 21;
let f = |x| a += x;
f.call(a);
a
"#
)?,
42
);
#[allow(deprecated)]
engine.register_raw_fn(
"custom_call",
&[TypeId::of::<INT>(), TypeId::of::<FnPtr>()],
|engine: &Engine, module: &Module, args: &mut [&mut Dynamic]| {
let func = take(args[1]).cast::<FnPtr>();
func.call_dynamic(engine, module, None, [])
},
);
assert_eq!(
engine.eval::<INT>(
r#"
let a = 41;
let b = 0;
let f = || b.custom_call(|| a + 1);
f.call()
"#
)?,
42
);
Ok(())
}
@ -101,12 +155,12 @@ fn test_closures_data_race() -> Result<(), Box<EvalAltResult>> {
assert_eq!(
engine.eval::<INT>(
r#"
let a = 1;
let b = 40;
let foo = |x| { this += a + x };
b.call(foo, 1);
b
"#
let a = 1;
let b = 40;
let foo = |x| { this += a + x };
b.call(foo, 1);
b
"#
)?,
42
);
@ -115,11 +169,11 @@ fn test_closures_data_race() -> Result<(), Box<EvalAltResult>> {
*engine
.eval::<INT>(
r#"
let a = 20;
let foo = |x| { this += a + x };
a.call(foo, 1);
a
"#
let a = 20;
let foo = |x| { this += a + x };
a.call(foo, 1);
a
"#
)
.expect_err("should error"),
EvalAltResult::ErrorDataRace(_, _)

View File

@ -1,6 +1,6 @@
#![cfg(not(feature = "no_object"))]
use rhai::{Engine, EvalAltResult, Map, Scope, INT};
use rhai::{Engine, EvalAltResult, Map, ParseErrorType, Scope, INT};
#[test]
fn test_map_indexing() -> Result<(), Box<EvalAltResult>> {
@ -182,6 +182,14 @@ fn test_map_json() -> Result<(), Box<EvalAltResult>> {
);
}
engine.parse_json(&format!("#{}", json), true)?;
assert!(matches!(
*engine.parse_json(" 123", true).expect_err("should error"),
EvalAltResult::ErrorParsing(ParseErrorType::MissingToken(token, _), pos)
if token == "{" && pos.position() == Some(4)
));
Ok(())
}

View File

@ -94,6 +94,31 @@ fn test_module_resolver() -> Result<(), Box<EvalAltResult>> {
42
);
assert_eq!(
engine.eval::<INT>(
r#"
import "hello" as h1;
import "hello" as h2;
let x = 42;
h1::sum(x, -10, 3, 7)
"#
)?,
42
);
assert_eq!(
engine.eval::<INT>(
r#"
import "hello" as h1;
import "hello" as h2;
let x = 42;
h1::sum(x, 0, 0, 0);
x
"#
)?,
42
);
assert_eq!(
engine.eval::<INT>(
r#"