From f36b4a69ae4ad6be03674c25d6297de3bc8058c3 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 9 Jul 2020 22:21:07 +0800 Subject: [PATCH] FIXED - method calls inside dot chain. --- RELEASES.md | 5 + doc/src/SUMMARY.md | 8 +- doc/src/about/features.md | 2 +- doc/src/appendix/operators.md | 1 + doc/src/engine/custom-syntax.md | 5 + doc/src/engine/dsl.md | 80 +++++++++ doc/src/links.md | 2 + doc/src/start/features.md | 2 +- src/engine.rs | 287 +++++++++++++++++++------------- 9 files changed, 275 insertions(+), 117 deletions(-) create mode 100644 doc/src/engine/custom-syntax.md create mode 100644 doc/src/engine/dsl.md diff --git a/RELEASES.md b/RELEASES.md index 8a022390..69c0186d 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -11,6 +11,11 @@ This version adds: * Ability to define custom operators (which must be valid identifiers). * Low-level API to register functions. +Bug fixes +--------- + +* Fixed method calls in the middle of a dot chain. + Breaking changes ---------------- diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 438b4590..b436efb9 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -105,9 +105,11 @@ The Rhai Scripting Language 5. [Volatility Considerations](engine/optimize/volatility.md) 6. [Subtle Semantic Changes](engine/optimize/semantics.md) 4. [Low-Level API](rust/register-raw.md) - 5. [Disable Keywords and/or Operators](engine/disable.md) - 6. [Custom Operators](engine/custom-op.md) - 7. [Eval Statement](language/eval.md) + 5. [Use as DSL](engine/dsl.md) + 1. [Disable Keywords and/or Operators](engine/disable.md) + 2. [Custom Operators](engine/custom-op.md) + 3. [Custom Syntax](engine/custom-syntax.md) + 6. [Eval Statement](language/eval.md) 9. [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 5a5739cb..88f585f0 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -67,4 +67,4 @@ Flexible * Surgically [disable keywords and operators] to restrict the language. -* [Custom operators]. +* Use as a [DSL] by [disabling keywords/operators][disable keywords and operators], [custom operators] and defining [custom syntax]. diff --git a/doc/src/appendix/operators.md b/doc/src/appendix/operators.md index 3e895da9..b4303d79 100644 --- a/doc/src/appendix/operators.md +++ b/doc/src/appendix/operators.md @@ -41,6 +41,7 @@ Symbols | ------------ | ------------------------ | | `:` | Property value separator | | `::` | Module path separator | +| `#` | _Reserved_ | | `=>` | _Reserved_ | | `->` | _Reserved_ | | `<-` | _Reserved_ | diff --git a/doc/src/engine/custom-syntax.md b/doc/src/engine/custom-syntax.md new file mode 100644 index 00000000..fe3f399c --- /dev/null +++ b/doc/src/engine/custom-syntax.md @@ -0,0 +1,5 @@ +Custom Syntax +============= + +{{#include ../links.md}} + diff --git a/doc/src/engine/dsl.md b/doc/src/engine/dsl.md new file mode 100644 index 00000000..adc04e44 --- /dev/null +++ b/doc/src/engine/dsl.md @@ -0,0 +1,80 @@ +Use Rhai as a Domain-Specific Language (DSL) +=========================================== + +{{#include ../links.md}} + +Rhai can be successfully used as a domain-specific language (DSL). + + +Expressions Only +---------------- + +In many DSL scenarios, only evaluation of expressions is needed. + +The `Engine::eval_expression_XXX`[`eval_expression`] API can be used to restrict +a script to expressions only. + + +Disable Keywords and/or Operators +-------------------------------- + +In some DSL scenarios, it is necessary to further restrict the language to exclude certain +language features that are not necessary or dangerous to the application. + +For example, a DSL may disable the `while` loop altogether while keeping all other statement +types intact. + +It is possible, in Rhai, to surgically [disable keywords and operators]. + + +Custom Operators +---------------- + +On the other hand, some DSL scenarios require special operators that make sense only for +that specific environment. In such cases, it is possible to define [custom operators] in Rhai. + +For example: + +```rust +let animal = "rabbit"; +let food = "carrot"; + +animal eats food // custom operator - 'eats' + +eats(animal, food) // <- the above really de-sugars to this +``` + +Although a [custom operator] always de-sugars to a simple function call, +nevertheless it makes the DSL syntax much simpler and expressive. + + +Custom Syntax +------------- + +For advanced DSL scenarios, it is possible to define entire expression [_syntax_][custom syntax] - +essentially custom statement types. + +The [`internals`] feature is needed to be able to define [custom syntax] in Rhai. + +For example: + +```rust +let table = [..., ..., ..., ...]; + +// Syntax = "select" $ident$ $ident$ "from" $expr$ "->" $ident$ ":" $expr$ +let total = select sum price from table -> row : row.weight > 50; +``` + +After registering this custom syntax with Rhai, it can be used anywhere inside a script as +a normal expression. + +For its evaluation, the callback function will receive the following list of parameters: + +`exprs[0] = "sum"` - math operator +`exprs[1] = "price"` - field name +`exprs[2] = Expr(table)` - data source +`exprs[3] = "row"` - loop variable name +`exprs[4] = Expr(row.wright > 50)` - expression + +The other identified, such as `"select"`, `"from"`, as as as symbols `->` and `:` are +parsed in the order defined within the custom syntax. diff --git a/doc/src/links.md b/doc/src/links.md index 85561acc..82ceab3b 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -89,6 +89,7 @@ [`eval`]: {{rootUrl}}/language/eval.md [OOP]: {{rootUrl}}/language/oop.md +[DSL]: {{rootUrl}}/engine/dsl.md [maximum statement depth]: {{rootUrl}}/safety/max-stmt-depth.md [maximum call stack depth]: {{rootUrl}}/safety/max-call-stack.md @@ -107,3 +108,4 @@ [disable keywords and operators]: {{rootUrl}}/engine/disable.md [custom operator]: {{rootUrl}}/engine/custom-op.md [custom operators]: {{rootUrl}}/engine/custom-op.md +[custom syntax]: {{rootUrl}}/engine/custom-syntax.md diff --git a/doc/src/start/features.md b/doc/src/start/features.md index 77a17e10..023834fd 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -25,7 +25,7 @@ more control over what a script can (or cannot) do. | `no_module` | Disable loading external [modules]. | | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | | `serde` | Enable serialization/deserialization via [`serde`]. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies. | -| `internals` | Expose internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version. | +| `internals` | Expose internal data structures (e.g. [`AST`] nodes) and enable defining [custom syntax]. Beware that Rhai internals are volatile and may change from version to version. | Example diff --git a/src/engine.rs b/src/engine.rs index ee963eb2..1fcae581 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1012,6 +1012,81 @@ impl Engine { return Ok(result); } + /// Call a dot method. + fn call_method( + &self, + state: &mut State, + lib: &Module, + target: &mut Target, + expr: &Expr, + mut idx_val: Dynamic, + level: usize, + ) -> Result<(Dynamic, bool), Box> { + let ((name, native, pos), _, hash, _, def_val) = match expr { + Expr::FnCall(x) => x.as_ref(), + _ => unreachable!(), + }; + + let is_ref = target.is_ref(); + let is_value = target.is_value(); + let def_val = def_val.as_ref(); + + // Get a reference to the mutation target Dynamic + let obj = target.as_mut(); + let idx = idx_val.downcast_mut::>().unwrap(); + let mut fn_name = name.as_ref(); + + // Check if it is a FnPtr call + let (result, updated) = if fn_name == KEYWORD_FN_PTR_CALL && obj.is::() { + // Redirect function name + fn_name = obj.as_str().unwrap(); + // Recalculate hash + let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); + // Arguments are passed as-is + let mut arg_values = idx.iter_mut().collect::>(); + let args = arg_values.as_mut(); + + // Map it to name(args) in function-call style + self.exec_fn_call( + state, lib, fn_name, *native, hash, args, false, false, def_val, level, + ) + } else { + let redirected: Option; + let mut hash = *hash; + + // Check if it is a map method call in OOP style + if let Some(map) = obj.downcast_ref::() { + if let Some(val) = map.get(fn_name) { + if let Some(f) = val.downcast_ref::() { + // Remap the function name + redirected = Some(f.get_fn_name().clone()); + fn_name = redirected.as_ref().unwrap(); + + // Recalculate the hash based on the new function name + hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); + } + } + }; + + // Attached object pointer in front of the arguments + let mut arg_values = once(obj).chain(idx.iter_mut()).collect::>(); + let args = arg_values.as_mut(); + + self.exec_fn_call( + state, lib, fn_name, *native, hash, args, is_ref, true, def_val, level, + ) + } + .map_err(|err| err.new_position(*pos))?; + + // Feed the changed temp value back + if updated && !is_ref && !is_value { + let new_val = target.as_mut().clone(); + target.set_value(new_val)?; + } + + Ok((result, updated)) + } + /// Chain-evaluate a dot/index chain. /// Position in `EvalAltResult` is None and must be set afterwards. fn eval_dot_index_chain_helper( @@ -1031,7 +1106,6 @@ impl Engine { } let is_ref = target.is_ref(); - let is_value = target.is_value(); let next_chain = match rhs { Expr::Index(_) => ChainType::Index, @@ -1040,7 +1114,7 @@ impl Engine { }; // Pop the last index value - let mut idx_val = idx_values.pop(); + let idx_val = idx_values.pop(); match chain_type { #[cfg(not(feature = "no_index"))] @@ -1124,69 +1198,7 @@ impl Engine { match rhs { // xxx.fn_name(arg_expr_list) Expr::FnCall(x) if x.1.is_none() => { - let ((name, native, pos), _, hash, _, def_val) = x.as_ref(); - let def_val = def_val.as_ref(); - - // Get a reference to the mutation target Dynamic - let (result, updated) = { - let obj = target.as_mut(); - let idx = idx_val.downcast_mut::>().unwrap(); - let mut fn_name = name.as_ref(); - - // Check if it is a FnPtr call - if fn_name == KEYWORD_FN_PTR_CALL && obj.is::() { - // Redirect function name - fn_name = obj.as_str().unwrap(); - // Recalculate hash - let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); - // Arguments are passed as-is - let mut arg_values = idx.iter_mut().collect::>(); - let args = arg_values.as_mut(); - - // Map it to name(args) in function-call style - self.exec_fn_call( - state, lib, fn_name, *native, hash, args, false, false, - def_val, level, - ) - } else { - let redirected: Option; - let mut hash = *hash; - - // Check if it is a map method call in OOP style - if let Some(map) = obj.downcast_ref::() { - if let Some(val) = map.get(fn_name) { - if let Some(f) = val.downcast_ref::() { - // Remap the function name - redirected = Some(f.get_fn_name().clone()); - fn_name = redirected.as_ref().unwrap(); - - // Recalculate the hash based on the new function name - hash = - calc_fn_hash(empty(), fn_name, idx.len(), empty()); - } - } - }; - - // Attached object pointer in front of the arguments - let mut arg_values = - once(obj).chain(idx.iter_mut()).collect::>(); - let args = arg_values.as_mut(); - - self.exec_fn_call( - state, lib, fn_name, *native, hash, args, is_ref, true, - def_val, level, - ) - } - .map_err(|err| err.new_position(*pos))? - }; - - // Feed the changed temp value back - if updated && !is_ref && !is_value { - let new_val = target.as_mut().clone(); - target.set_value(new_val)?; - } - - Ok((result, updated)) + self.call_method(state, lib, target, rhs, idx_val, level) } // xxx.module::fn_name(...) - syntax error Expr::FnCall(_) => unreachable!(), @@ -1230,16 +1242,26 @@ impl Engine { .map(|(v, _)| (v, false)) .map_err(|err| err.new_position(*pos)) } - // {xxx:map}.prop[expr] | {xxx:map}.prop.expr + // {xxx:map}.sub_lhs[expr] | {xxx:map}.sub_lhs.expr Expr::Index(x) | Expr::Dot(x) if target.is::() => { - let (prop, expr, pos) = x.as_ref(); + let (sub_lhs, expr, pos) = x.as_ref(); - let mut val = if let Expr::Property(p) = prop { - let ((prop, _, _), _) = p.as_ref(); - let index = prop.clone().into(); - self.get_indexed_mut(state, lib, target, index, *pos, false, level)? - } else { - unreachable!(); + let mut val = match sub_lhs { + Expr::Property(p) => { + let ((prop, _, _), _) = p.as_ref(); + let index = prop.clone().into(); + self.get_indexed_mut(state, lib, target, index, *pos, false, level)? + } + // {xxx:map}.fn_name(arg_expr_list)[expr] | {xxx:map}.fn_name(arg_expr_list).expr + Expr::FnCall(x) if x.1.is_none() => { + let (val, _) = + self.call_method(state, lib, target, sub_lhs, idx_val, level)?; + val.into() + } + // {xxx:map}.module::fn_name(...) - syntax error + Expr::FnCall(_) => unreachable!(), + // Others - syntax error + _ => unreachable!(), }; self.eval_dot_index_chain_helper( @@ -1248,49 +1270,72 @@ impl Engine { ) .map_err(|err| err.new_position(*pos)) } - // xxx.prop[expr] | xxx.prop.expr + // xxx.sub_lhs[expr] | xxx.sub_lhs.expr Expr::Index(x) | Expr::Dot(x) => { - let (prop, expr, pos) = x.as_ref(); - let args = &mut [target.as_mut(), &mut Default::default()]; + let (sub_lhs, expr, pos) = x.as_ref(); - let (mut val, updated) = if let Expr::Property(p) = prop { - let ((_, getter, _), _) = p.as_ref(); - let args = &mut args[..1]; - self.exec_fn_call( - state, lib, getter, true, 0, args, is_ref, true, None, level, - ) - .map_err(|err| err.new_position(*pos))? - } else { - unreachable!(); - }; - let val = &mut val; - let target = &mut val.into(); + match sub_lhs { + // xxx.prop[expr] | xxx.prop.expr + Expr::Property(p) => { + let ((_, getter, setter), _) = p.as_ref(); + let arg_values = &mut [target.as_mut(), &mut Default::default()]; + let args = &mut arg_values[..1]; + let (mut val, updated) = self + .exec_fn_call( + state, lib, getter, true, 0, args, is_ref, true, None, + level, + ) + .map_err(|err| err.new_position(*pos))?; - let (result, may_be_changed) = self - .eval_dot_index_chain_helper( - state, lib, this_ptr, target, expr, idx_values, next_chain, level, - new_val, - ) - .map_err(|err| err.new_position(*pos))?; + let val = &mut val; + let target = &mut val.into(); - // Feed the value back via a setter just in case it has been updated - if updated || may_be_changed { - if let Expr::Property(p) = prop { - let ((_, _, setter), _) = p.as_ref(); - // Re-use args because the first &mut parameter will not be consumed - args[1] = val; - self.exec_fn_call( - state, lib, setter, true, 0, args, is_ref, true, None, level, - ) - .or_else(|err| match *err { - // If there is no setter, no need to feed it back because the property is read-only - EvalAltResult::ErrorDotExpr(_, _) => Ok(Default::default()), - _ => Err(err.new_position(*pos)), - })?; + let (result, may_be_changed) = self + .eval_dot_index_chain_helper( + state, lib, this_ptr, target, expr, idx_values, next_chain, + level, new_val, + ) + .map_err(|err| err.new_position(*pos))?; + + // Feed the value back via a setter just in case it has been updated + if updated || may_be_changed { + // Re-use args because the first &mut parameter will not be consumed + arg_values[1] = val; + self.exec_fn_call( + state, lib, setter, true, 0, arg_values, is_ref, true, + None, level, + ) + .or_else( + |err| match *err { + // If there is no setter, no need to feed it back because the property is read-only + EvalAltResult::ErrorDotExpr(_, _) => { + Ok(Default::default()) + } + _ => Err(err.new_position(*pos)), + }, + )?; + } + + Ok((result, may_be_changed)) } - } + // xxx.fn_name(arg_expr_list)[expr] | xxx.fn_name(arg_expr_list).expr + Expr::FnCall(x) if x.1.is_none() => { + let (mut val, _) = + self.call_method(state, lib, target, sub_lhs, idx_val, level)?; + let val = &mut val; + let target = &mut val.into(); - Ok((result, may_be_changed)) + self.eval_dot_index_chain_helper( + state, lib, this_ptr, target, expr, idx_values, next_chain, + level, new_val, + ) + .map_err(|err| err.new_position(*pos)) + } + // xxx.module::fn_name(...) - syntax error + Expr::FnCall(_) => unreachable!(), + // Others - syntax error + _ => unreachable!(), + } } // Syntax error _ => Err(Box::new(EvalAltResult::ErrorDotExpr( @@ -1325,7 +1370,7 @@ impl Engine { let idx_values = &mut StaticVec::new(); self.eval_indexed_chain( - scope, mods, state, lib, this_ptr, dot_rhs, idx_values, 0, level, + scope, mods, state, lib, this_ptr, dot_rhs, chain_type, idx_values, 0, level, )?; match dot_lhs { @@ -1389,6 +1434,7 @@ impl Engine { lib: &Module, this_ptr: &mut Option<&mut Dynamic>, expr: &Expr, + chain_type: ChainType, idx_values: &mut StaticVec, size: usize, level: usize, @@ -1415,12 +1461,29 @@ impl Engine { // Evaluate in left-to-right order let lhs_val = match lhs { Expr::Property(_) => Default::default(), // Store a placeholder in case of a property + Expr::FnCall(x) if chain_type == ChainType::Dot && x.1.is_none() => { + let arg_values = x + .3 + .iter() + .map(|arg_expr| { + self.eval_expr(scope, mods, state, lib, this_ptr, arg_expr, level) + }) + .collect::, _>>()?; + + Dynamic::from(arg_values) + } + Expr::FnCall(_) => unreachable!(), _ => self.eval_expr(scope, mods, state, lib, this_ptr, lhs, level)?, }; // Push in reverse order + let chain_type = match expr { + Expr::Index(_) => ChainType::Index, + Expr::Dot(_) => ChainType::Dot, + _ => unreachable!(), + }; self.eval_indexed_chain( - scope, mods, state, lib, this_ptr, rhs, idx_values, size, level, + scope, mods, state, lib, this_ptr, rhs, chain_type, idx_values, size, level, )?; idx_values.push(lhs_val);