From 175c3ccaec6c7c8a0247f87beda28bda5ba06437 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 26 Jun 2020 10:39:18 +0800 Subject: [PATCH] OOP support. --- Cargo.toml | 2 +- RELEASES.md | 8 +- doc/src/SUMMARY.md | 6 +- doc/src/about/features.md | 2 + doc/src/about/non-design.md | 1 + doc/src/appendix/keywords.md | 55 ++-- doc/src/context.json | 2 +- doc/src/language/fn-ptr.md | 2 + doc/src/language/functions.md | 67 +++-- doc/src/language/logic.md | 8 +- doc/src/language/object-maps-oop.md | 23 ++ doc/src/language/oop.md | 44 ++++ doc/src/links.md | 3 + doc/src/rust/custom.md | 5 +- src/api.rs | 15 +- src/engine.rs | 383 ++++++++++++++++++---------- src/error.rs | 8 +- src/fn_native.rs | 2 +- src/optimize.rs | 1 + src/packages/array_basic.rs | 2 +- src/packages/string_more.rs | 1 + src/parser.rs | 27 +- src/result.rs | 6 + tests/bool_op.rs | 16 +- tests/functions.rs | 17 +- tests/maps.rs | 22 ++ tests/modules.rs | 4 +- 27 files changed, 498 insertions(+), 234 deletions(-) create mode 100644 doc/src/language/object-maps-oop.md create mode 100644 doc/src/language/oop.md diff --git a/Cargo.toml b/Cargo.toml index f7ed22b4..2ffbd617 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhai" -version = "0.15.2" +version = "0.16.0" edition = "2018" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"] description = "Embedded scripting for Rust" diff --git a/RELEASES.md b/RELEASES.md index 205491a4..f42790be 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,18 +1,24 @@ Rhai Release Notes ================== -Version 0.15.2 +Version 0.16.0 ============== +The major new feature in this version is OOP - well, poor man's OOP, that is. + Breaking changes ---------------- * The trait function `ModuleResolver::resolve` no longer takes a `Scope` as argument. +* Functions defined in script now differentiates between using method-call style and normal function-call style. + The method-call style will bind the object to the `this` parameter instead of consuming the first parameter. New features ------------ * Support for _function pointers_ via `Fn(name)` and `Fn.call(...)` syntax - a poor man's first-class function. +* Support for calling script-defined functions in method-call style with `this` binding to the object. +* Special support in object maps for OOP. Enhancements ------------ diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index ea307d34..6ea2fc76 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -56,6 +56,7 @@ The Rhai Scripting Language 5. [Arrays](language/arrays.md) 6. [Object Maps](language/object-maps.md) 1. [Parse from JSON](language/json.md) + 2. [Special Support for OOP](language/object-maps-oop.md) 7. [Time-Stamps](language/timestamps.md) 3. [Keywords](language/keywords.md) 4. [Statements](language/statements.md) @@ -92,14 +93,15 @@ The Rhai Scripting Language 8. [Maximum Call Stack Depth](safety/max-call-stack.md) 9. [Maximum Statement Depth](safety/max-stmt-depth.md) 7. [Advanced Topics](advanced.md) - 1. [Script Optimization](engine/optimize/index.md) + 1. [Object-Oriented Programming (OOP)](language/oop.md) + 2. [Script Optimization](engine/optimize/index.md) 1. [Optimization Levels](engine/optimize/optimize-levels.md) 2. [Re-Optimize an AST](engine/optimize/reoptimize.md) 3. [Eager Function Evaluation](engine/optimize/eager.md) 4. [Side-Effect Considerations](engine/optimize/side-effects.md) 5. [Volatility Considerations](engine/optimize/volatility.md) 6. [Subtle Semantic Changes](engine/optimize/semantics.md) - 2. [Eval Statement](language/eval.md) + 3. [Eval Statement](language/eval.md) 8. [Appendix](appendix/index.md) 1. [Keywords](appendix/keywords.md) 2. [Operators](appendix/operators.md) diff --git a/doc/src/about/features.md b/doc/src/about/features.md index 5264f050..a079873f 100644 --- a/doc/src/about/features.md +++ b/doc/src/about/features.md @@ -37,6 +37,8 @@ Dynamic * Dynamic dispatch via [function pointers]. +* Some support for [OOP]. + Safe ---- diff --git a/doc/src/about/non-design.md b/doc/src/about/non-design.md index 6ab4f9ed..f93bb9ec 100644 --- a/doc/src/about/non-design.md +++ b/doc/src/about/non-design.md @@ -13,6 +13,7 @@ It doesn't attempt to be a new language. For example: * No structures/records - define your types in Rust instead; Rhai can seamlessly work with _any Rust type_. There is, however, a built-in [object map] type which is adequate for most uses. + It is possible to simulate [OOP] by storing [function pointers] in [object map] properties, turning them into _methods_. * No first-class functions - Code your functions in Rust instead, and register them with Rhai. diff --git a/doc/src/appendix/keywords.md b/doc/src/appendix/keywords.md index 85b9dc48..6e13178f 100644 --- a/doc/src/appendix/keywords.md +++ b/doc/src/appendix/keywords.md @@ -3,30 +3,31 @@ Keywords List {{#include ../links.md}} -| Keyword | Description | Not available under | -| :-------------------: | --------------------------------------- | ------------------- | -| `true` | Boolean true literal | | -| `false` | Boolean false literal | | -| `let` | Variable declaration | | -| `const` | Constant declaration | | -| `if` | If statement | | -| `else` | else block of if statement | | -| `while` | While loop | | -| `loop` | Infinite loop | | -| `for` | For loop | | -| `in` | Containment test, part of for loop | | -| `continue` | Continue a loop at the next iteration | | -| `break` | Loop breaking | | -| `return` | Return value | | -| `throw` | Throw exception | | -| `private` | Mark function private | [`no_function`] | -| `import` | Import module | [`no_module`] | -| `export` | Export variable | [`no_module`] | -| `as` | Alias for variable export | [`no_module`] | -| `fn` (lower-case `f`) | Function definition | [`no_function`] | -| `Fn` (capital `F`) | Function to create a [function pointer] | [`no_function`] | -| `call` | Call a [function pointer] | [`no_function`] | -| `type_of` | Get type name of value | | -| `print` | Print value | | -| `debug` | Print value in debug format | | -| `eval` | Evaluate script | | +| Keyword | Description | Not available under | +| :-------------------: | ---------------------------------------- | :-----------------: | +| `true` | Boolean true literal | | +| `false` | Boolean false literal | | +| `let` | Variable declaration | | +| `const` | Constant declaration | | +| `if` | If statement | | +| `else` | else block of if statement | | +| `while` | While loop | | +| `loop` | Infinite loop | | +| `for` | For loop | | +| `in` | Containment test, part of for loop | | +| `continue` | Continue a loop at the next iteration | | +| `break` | Loop breaking | | +| `return` | Return value | | +| `throw` | Throw exception | | +| `import` | Import module | [`no_module`] | +| `export` | Export variable | [`no_module`] | +| `as` | Alias for variable export | [`no_module`] | +| `private` | Mark function private | [`no_function`] | +| `fn` (lower-case `f`) | Function definition | [`no_function`] | +| `Fn` (capital `F`) | Function to create a [function pointer] | [`no_function`] | +| `call` | Call a [function pointer] | [`no_function`] | +| `this` | Reference to base object for method call | [`no_function`] | +| `type_of` | Get type name of value | | +| `print` | Print value | | +| `debug` | Print value in debug format | | +| `eval` | Evaluate script | | diff --git a/doc/src/context.json b/doc/src/context.json index 187955d4..6da8d6bb 100644 --- a/doc/src/context.json +++ b/doc/src/context.json @@ -1,5 +1,5 @@ { - "version": "0.15.1", + "version": "0.16.0", "rootUrl": "", "rootUrlX": "/rhai" } \ No newline at end of file diff --git a/doc/src/language/fn-ptr.md b/doc/src/language/fn-ptr.md index b509ca21..a2f7f83f 100644 --- a/doc/src/language/fn-ptr.md +++ b/doc/src/language/fn-ptr.md @@ -1,6 +1,8 @@ Function Pointers ================= +{{#include ../links.md}} + It is possible to store a _function pointer_ in a variable just like a normal value. In fact, internally a function pointer simply stores the _name_ of the function as a string. diff --git a/doc/src/language/functions.md b/doc/src/language/functions.md index 47a6c717..72dac0b4 100644 --- a/doc/src/language/functions.md +++ b/doc/src/language/functions.md @@ -53,29 +53,6 @@ fn foo() { x } // <- syntax error: variable 'x' doesn't exist ``` -Arguments Passed by Value ------------------------- - -Functions defined in script always take [`Dynamic`] parameters (i.e. the parameter can be of any type). -Therefore, functions with the same name and same _number_ of parameters are equivalent. - -It is important to remember that all arguments are passed by _value_, so all Rhai script-defined functions -are _pure_ (i.e. they never modify their arguments). -Any update to an argument will **not** be reflected back to the caller. - -This can introduce subtle bugs, if not careful, especially when using the _method-call_ style. - -```rust -fn change(s) { // 's' is passed by value - s = 42; // only a COPY of 's' is changed -} - -let x = 500; -x.change(); // de-sugars to 'change(x)' -x == 500; // 'x' is NOT changed! -``` - - Global Definitions Only ---------------------- @@ -106,3 +83,47 @@ A function does not need to be defined prior to being used in a script; a statement in the script can freely call a function defined afterwards. This is similar to Rust and many other modern languages, such as JavaScript's `function` keyword. + + +Arguments Passed by Value +------------------------ + +Functions defined in script always take [`Dynamic`] parameters (i.e. the parameter can be of any type). +Therefore, functions with the same name and same _number_ of parameters are equivalent. + +It is important to remember that all arguments are passed by _value_, so all Rhai script-defined functions +are _pure_ (i.e. they never modify their arguments). +Any update to an argument will **not** be reflected back to the caller. + +```rust +fn change(s) { // 's' is passed by value + s = 42; // only a COPY of 's' is changed +} + +let x = 500; + +change(x); + +x == 500; // 'x' is NOT changed! +``` + + +`this` - Simulating an Object Method +----------------------------------- + +Functions can also be called in method-call style. When this is the case, the keyword '`this`' +binds to the object in the method call and can be changed. + +```rust +fn change() { // not that the object does not need a parameter + this = 42; // 'this' binds to the object in method-call +} + +let x = 500; + +x.change(); // call 'change' in method-call style, 'this' binds to 'x' + +x == 42; // 'x' is changed! + +change(); // <- error: `this` is unbounded +``` diff --git a/doc/src/language/logic.md b/doc/src/language/logic.md index c660aa52..113641f6 100644 --- a/doc/src/language/logic.md +++ b/doc/src/language/logic.md @@ -57,13 +57,13 @@ if the first one already proves the condition wrong. Single boolean operators `&` and `|` always evaluate both operands. ```rust -this() || that(); // that() is not evaluated if this() is true +a() || b(); // b() is not evaluated if a() is true -this() && that(); // that() is not evaluated if this() is false +a() && b(); // b() is not evaluated if a() is false -this() | that(); // both this() and that() are evaluated +a() | b(); // both a() and b() are evaluated -this() & that(); // both this() and that() are evaluated +a() & b(); // both a() and b() are evaluated ``` Compound Assignment Operators diff --git a/doc/src/language/object-maps-oop.md b/doc/src/language/object-maps-oop.md new file mode 100644 index 00000000..1a37d897 --- /dev/null +++ b/doc/src/language/object-maps-oop.md @@ -0,0 +1,23 @@ +Special Support for OOP via Object Maps +====================================== + +{{#include ../links.md}} + +[Object maps] can be used to simulate object-oriented programming ([OOP]) by storing data +as properties and methods as properties holding [function pointers]. + +If an [object map]'s property holding a [function pointer] is called in method-call style, +it calls the function referenced by the [function pointer]. + +```rust +fn do_action(x) { print(this + x); } // 'this' binds to the object when called + +let obj = #{ + data: 40, + action: Fn("do_action") // 'action' holds a function pointer to 'do_action' + }; + +obj.action(2); // prints 42 + +obj.action.call(2); // <- the above de-sugars to this +``` diff --git a/doc/src/language/oop.md b/doc/src/language/oop.md new file mode 100644 index 00000000..120b42d0 --- /dev/null +++ b/doc/src/language/oop.md @@ -0,0 +1,44 @@ +Object-Oriented Programming (OOP) +================================ + +{{#include ../links.md}} + +Rhai does not have _objects_ per se, but it is possible to _simulate_ object-oriented programming. + + +Use [Object Maps] to Simulate OOP +-------------------------------- + +Rhai's [object maps] has [special support for OOP]({{rootUrl}}/language/object-maps-oop.md). + +| Rhai concept | Maps to OOP | +| ----------------------------------------------------- | :---------: | +| [Object maps] | objects | +| [Object map] properties holding values | properties | +| [Object map] properties that hold [function pointers] | methods | + + +Examples +-------- + +```rust +// Define the object +let obj = #{ + data: 0, + increment: Fn("add"), // when called, 'this' binds to 'obj' + update: Fn("update"), // when called, 'this' binds to 'obj' + action: Fn("action") // when called, 'this' binds to 'obj' + }; + +// Define functions +fn add(x) { this.data += x; } // update using 'this' +fn update(x) { this.data = x; } // update using 'this' +fn action() { print(this.data); } // access properties of 'this' + +// Use the object +obj.increment(1); +obj.action(); // prints 1 + +obj.update(42); +obj.action(); // prints 42 +``` diff --git a/doc/src/links.md b/doc/src/links.md index a9587253..8f3bcf15 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -77,6 +77,9 @@ [`eval`]: {{rootUrl}}/language/eval.md +[OOP]: {{rootUrl}}/language/oop.md + + [maximum statement depth]: {{rootUrl}}/safety/max-stmt-depth.md [maximum call stack depth]: {{rootUrl}}/safety/max-call-stack.md [maximum number of operations]: {{rootUrl}}/safety/max-operations.md diff --git a/doc/src/rust/custom.md b/doc/src/rust/custom.md index da9af45d..b87f6409 100644 --- a/doc/src/rust/custom.md +++ b/doc/src/rust/custom.md @@ -97,16 +97,17 @@ println!("result: {}", result.field); // prints 42 Method-Call Style vs. Function-Call Style ---------------------------------------- -In fact, any function with a first argument that is a `&mut` reference can be used +Any function with a first argument that is a `&mut` reference can be used as method calls because internally they are the same thing: methods on a type is implemented as a functions taking a `&mut` first argument. +This design is similar to Rust. ```rust fn foo(ts: &mut TestStruct) -> i64 { ts.field } -engine.register_fn("foo", foo); // register ad hoc function with correct signature +engine.register_fn("foo", foo); // register a Rust native function let result = engine.eval::( "let x = new_ts(); x.foo()" // 'foo' can be called like a method on 'x' diff --git a/src/api.rs b/src/api.rs index cce53dd3..8ea047f9 100644 --- a/src/api.rs +++ b/src/api.rs @@ -996,7 +996,7 @@ impl Engine { ast.statements() .iter() .try_fold(().into(), |_, stmt| { - self.eval_stmt(scope, &mut state, ast.lib(), stmt, 0) + self.eval_stmt(scope, &mut state, ast.lib(), &mut None, stmt, 0) }) .or_else(|err| match *err { EvalAltResult::Return(out, _) => Ok(out), @@ -1062,7 +1062,7 @@ impl Engine { ast.statements() .iter() .try_fold(().into(), |_, stmt| { - self.eval_stmt(scope, &mut state, ast.lib(), stmt, 0) + self.eval_stmt(scope, &mut state, ast.lib(), &mut None, stmt, 0) }) .map_or_else( |err| match *err { @@ -1203,7 +1203,16 @@ impl Engine { let mut state = State::new(); let args = args.as_mut(); - self.call_script_fn(scope, &mut state, ast.lib(), name, fn_def, args, 0) + self.call_script_fn( + scope, + &mut state, + ast.lib(), + &mut None, + name, + fn_def, + args, + 0, + ) } /// Optimize the `AST` with constants defined in an external Scope. diff --git a/src/engine.rs b/src/engine.rs index fa91e450..5d340e59 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -4,7 +4,6 @@ use crate::any::{Dynamic, Union, Variant}; use crate::calc_fn_hash; use crate::error::ParseErrorType; use crate::fn_native::{CallableFunction, Callback, FnCallArgs, FnPtr}; -use crate::fn_register::RegisterResultFn; use crate::module::{resolvers, Module, ModuleResolver}; use crate::optimize::OptimizationLevel; use crate::packages::{Package, PackageLibrary, PackagesCollection, StandardPackage}; @@ -12,7 +11,7 @@ use crate::parser::{Expr, FnAccess, ImmutableString, ReturnType, ScriptFnDef, St use crate::r#unsafe::{unsafe_cast_var_name_to_lifetime, unsafe_mut_cast_to_lifetime}; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; -use crate::token::{Position, Token}; +use crate::token::Position; use crate::utils::StaticVec; #[cfg(not(feature = "no_float"))] @@ -73,6 +72,7 @@ pub const KEYWORD_DEBUG: &str = "debug"; pub const KEYWORD_TYPE_OF: &str = "type_of"; pub const KEYWORD_EVAL: &str = "eval"; pub const KEYWORD_FN_PTR_CALL: &str = "call"; +pub const KEYWORD_THIS: &str = "this"; pub const FN_TO_STRING: &str = "to_string"; pub const FN_GET: &str = "get$"; pub const FN_SET: &str = "set$"; @@ -189,7 +189,7 @@ impl> From for Target<'_> { /// /// This type uses some unsafe code, mainly for avoiding cloning of local variable names via /// direct lifetime casting. -#[derive(Debug, Eq, PartialEq, Hash, Clone, Default)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] pub struct State { /// 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. @@ -393,13 +393,23 @@ fn default_print(s: &str) { fn search_scope<'s, 'a>( scope: &'s mut Scope, state: &mut State, + this_ptr: &'s mut Option<&mut Dynamic>, expr: &'a Expr, ) -> Result<(&'s mut Dynamic, &'a str, ScopeEntryType, Position), Box> { let ((name, pos), modules, hash_var, index) = match expr { - Expr::Variable(x) => x.as_ref(), + Expr::Variable(v) => v.as_ref(), _ => unreachable!(), }; + // Check if the variable is `this` + if name == KEYWORD_THIS { + if let Some(val) = this_ptr { + return Ok(((*val).into(), KEYWORD_THIS, ScopeEntryType::Normal, *pos)); + } else { + return Err(Box::new(EvalAltResult::ErrorUnboundedThis(*pos))); + } + } + // Check if it is qualified if let Some(modules) = modules.as_ref() { // Qualified - check if the root module is directly indexed @@ -654,6 +664,7 @@ impl Engine { (hash_fn, hash_script): (u64, u64), args: &mut FnCallArgs, is_ref: bool, + is_method: bool, def_val: Option<&Dynamic>, level: usize, ) -> Result<(Dynamic, bool), Box> { @@ -724,9 +735,9 @@ impl Engine { .or_else(|| self.packages.get_fn(hash_fn)); if let Some(func) = func { - // Calling pure function in method-call? + // Calling pure function but the first argument is a reference? normalize_first_arg( - (func.is_pure() || func.is_script()) && is_ref, + is_ref && (func.is_pure() || (func.is_script() && !is_method)), &mut this_copy, &mut old_this_ptr, args, @@ -735,13 +746,33 @@ impl Engine { if func.is_script() { // Run scripted function let fn_def = func.get_fn_def(); - let result = - self.call_script_fn(scope, state, lib, fn_name, fn_def, args, level)?; - // Restore the original reference - restore_first_arg(old_this_ptr, args); + // Method call of script function - map first argument to `this` + if is_method { + let (first, rest) = args.split_at_mut(1); + return Ok(( + self.call_script_fn( + scope, + state, + lib, + &mut Some(first[0]), + fn_name, + fn_def, + rest, + level, + )?, + false, + )); + } else { + let result = self.call_script_fn( + scope, state, lib, &mut None, fn_name, fn_def, args, level, + )?; - return Ok((result, false)); + // Restore the original reference + restore_first_arg(old_this_ptr, args); + + return Ok((result, false)); + }; } else { // Run external function let result = func.get_native_fn()(self, args)?; @@ -860,6 +891,7 @@ impl Engine { scope: &mut Scope, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, fn_name: &str, fn_def: &ScriptFnDef, args: &mut FnCallArgs, @@ -885,7 +917,7 @@ impl Engine { // Evaluate the function at one higher level of call depth let result = self - .eval_stmt(scope, state, lib, &fn_def.body, level + 1) + .eval_stmt(scope, state, lib, this_ptr, &fn_def.body, level + 1) .or_else(|err| match *err { // Convert return statement to return value EvalAltResult::Return(x, _) => Ok(x), @@ -942,6 +974,7 @@ impl Engine { hash_script: u64, args: &mut FnCallArgs, is_ref: bool, + is_method: bool, def_val: Option<&Dynamic>, level: usize, ) -> Result<(Dynamic, bool), Box> { @@ -977,7 +1010,8 @@ impl Engine { _ => { let mut scope = Scope::new(); self.call_fn_raw( - &mut scope, state, lib, fn_name, hashes, args, is_ref, def_val, level, + &mut scope, state, lib, fn_name, hashes, args, is_ref, is_method, def_val, + level, ) } } @@ -1027,6 +1061,7 @@ impl Engine { &self, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, target: &mut Target, rhs: &Expr, idx_values: &mut StaticVec, @@ -1060,11 +1095,12 @@ impl Engine { Expr::Dot(x) | Expr::Index(x) => { let (idx, expr, pos) = x.as_ref(); let idx_pos = idx.position(); - let this_ptr = &mut self - .get_indexed_mut(state, lib, target, idx_val, idx_pos, false)?; + let obj_ptr = &mut self + .get_indexed_mut(state, lib, target, idx_val, idx_pos, false, level)?; self.eval_dot_index_chain_helper( - state, lib, this_ptr, expr, idx_values, next_chain, level, new_val, + state, lib, this_ptr, obj_ptr, expr, idx_values, next_chain, level, + new_val, ) .map_err(|err| EvalAltResult::new_position(err, *pos)) } @@ -1072,15 +1108,16 @@ impl Engine { _ if new_val.is_some() => { let mut idx_val2 = idx_val.clone(); - match self.get_indexed_mut(state, lib, target, idx_val, pos, true) { + match self.get_indexed_mut(state, lib, target, idx_val, pos, true, level) { // Indexed value is an owned value - the only possibility is an indexer // Try to call an index setter - Ok(this_ptr) if this_ptr.is_value() => { + Ok(obj_ptr) if obj_ptr.is_value() => { let args = &mut [target.as_mut(), &mut idx_val2, &mut new_val.unwrap()]; self.exec_fn_call( - state, lib, FN_IDX_SET, true, 0, args, is_ref, None, 0, + state, lib, FN_IDX_SET, true, 0, args, is_ref, true, None, + level, ) .or_else(|err| match *err { // If there is no index setter, no need to set it back because the indexer is read-only @@ -1093,8 +1130,8 @@ impl Engine { })?; } // Indexed value is a reference - update directly - Ok(ref mut this_ptr) => { - this_ptr.set_value(new_val.unwrap()).map_err(|err| { + Ok(ref mut obj_ptr) => { + obj_ptr.set_value(new_val.unwrap()).map_err(|err| { EvalAltResult::new_position(err, rhs.position()) })?; } @@ -1108,7 +1145,8 @@ impl Engine { ]; self.exec_fn_call( - state, lib, FN_IDX_SET, true, 0, args, is_ref, None, 0, + state, lib, FN_IDX_SET, true, 0, args, is_ref, true, None, + level, )?; } // Error @@ -1119,7 +1157,7 @@ impl Engine { } // xxx[rhs] _ => self - .get_indexed_mut(state, lib, target, idx_val, pos, false) + .get_indexed_mut(state, lib, target, idx_val, pos, false, level) .map(|v| (v.clone_into_dynamic(), false)), } } @@ -1138,29 +1176,55 @@ impl Engine { let idx = idx_val.downcast_mut::>().unwrap(); // Check if it is a FnPtr call - let (fn_name, mut arg_values, hash_fn, is_native) = - if name == KEYWORD_FN_PTR_CALL && obj.is::() { - // Redirect function name - let name = obj.as_fn_name().unwrap(); - // Recalculate hash - let hash = calc_fn_hash(empty(), name, idx.len(), empty()); - // Arguments are passed as-is - (name, idx.iter_mut().collect::>(), hash, false) - } else { - // Attached object pointer in front of the arguments - ( - name.as_ref(), - once(obj).chain(idx.iter_mut()).collect::>(), - *hash, - *native, - ) + if name == KEYWORD_FN_PTR_CALL && obj.is::() { + // Redirect function name + let fn_name = obj.as_fn_name().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 mut fn_name = name.clone(); + let mut redirected = None; + 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(name.as_ref()) { + if let Some(f) = val.downcast_ref::() { + // Remap the function name + redirected = Some(f.get_fn_name().clone()); + fn_name = redirected.as_ref().unwrap().as_str().into(); + + // Recalculate the hash based on the new function name + hash = calc_fn_hash( + empty(), + fn_name.as_ref(), + idx.len(), + empty(), + ); + } + } }; - let args = arg_values.as_mut(); + // 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(); + let fn_name = fn_name.as_ref(); - self.exec_fn_call( - state, lib, fn_name, is_native, hash_fn, args, is_ref, def_val, 0, - ) + self.exec_fn_call( + state, lib, fn_name, *native, hash, args, is_ref, true, + def_val, level, + ) + } .map_err(|err| EvalAltResult::new_position(err, *pos))? }; @@ -1179,7 +1243,7 @@ impl Engine { let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); let mut val = - self.get_indexed_mut(state, lib, target, index, *pos, true)?; + self.get_indexed_mut(state, lib, target, index, *pos, true, level)?; val.set_value(new_val.unwrap()) .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; @@ -1189,7 +1253,8 @@ impl Engine { Expr::Property(x) if target.is::() => { let ((prop, _, _), pos) = x.as_ref(); let index = prop.clone().into(); - let val = self.get_indexed_mut(state, lib, target, index, *pos, false)?; + let val = + self.get_indexed_mut(state, lib, target, index, *pos, false, level)?; Ok((val.clone_into_dynamic(), false)) } @@ -1197,17 +1262,21 @@ impl Engine { Expr::Property(x) if new_val.is_some() => { let ((_, _, setter), pos) = x.as_ref(); let mut args = [target.as_mut(), new_val.as_mut().unwrap()]; - self.exec_fn_call(state, lib, setter, true, 0, &mut args, is_ref, None, 0) - .map(|(v, _)| (v, true)) - .map_err(|err| EvalAltResult::new_position(err, *pos)) + self.exec_fn_call( + state, lib, setter, true, 0, &mut args, is_ref, true, None, level, + ) + .map(|(v, _)| (v, true)) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } // xxx.id Expr::Property(x) => { let ((_, getter, _), pos) = x.as_ref(); let mut args = [target.as_mut()]; - self.exec_fn_call(state, lib, getter, true, 0, &mut args, is_ref, None, 0) - .map(|(v, _)| (v, false)) - .map_err(|err| EvalAltResult::new_position(err, *pos)) + self.exec_fn_call( + state, lib, getter, true, 0, &mut args, is_ref, true, None, level, + ) + .map(|(v, _)| (v, false)) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } // {xxx:map}.prop[expr] | {xxx:map}.prop.expr Expr::Index(x) | Expr::Dot(x) if target.is::() => { @@ -1216,13 +1285,14 @@ impl Engine { 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)? + self.get_indexed_mut(state, lib, target, index, *pos, false, level)? } else { unreachable!(); }; self.eval_dot_index_chain_helper( - state, lib, &mut val, expr, idx_values, next_chain, level, new_val, + state, lib, this_ptr, &mut val, expr, idx_values, next_chain, level, + new_val, ) .map_err(|err| EvalAltResult::new_position(err, *pos)) } @@ -1234,8 +1304,10 @@ impl Engine { 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, None, 0) - .map_err(|err| EvalAltResult::new_position(err, *pos))? + self.exec_fn_call( + state, lib, getter, true, 0, args, is_ref, true, None, level, + ) + .map_err(|err| EvalAltResult::new_position(err, *pos))? } else { unreachable!(); }; @@ -1244,7 +1316,8 @@ impl Engine { let (result, may_be_changed) = self .eval_dot_index_chain_helper( - state, lib, target, expr, idx_values, next_chain, level, new_val, + state, lib, this_ptr, target, expr, idx_values, next_chain, level, + new_val, ) .map_err(|err| EvalAltResult::new_position(err, *pos))?; @@ -1255,7 +1328,7 @@ impl Engine { // 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, None, 0, + 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 @@ -1285,6 +1358,7 @@ impl Engine { scope: &mut Scope, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, expr: &Expr, level: usize, new_val: Option, @@ -1297,30 +1371,33 @@ impl Engine { let idx_values = &mut StaticVec::new(); - self.eval_indexed_chain(scope, state, lib, dot_rhs, idx_values, 0, level)?; + self.eval_indexed_chain(scope, state, lib, this_ptr, dot_rhs, idx_values, 0, level)?; match dot_lhs { // id.??? or id[???] - Expr::Variable(_) => { - let (target, name, typ, pos) = search_scope(scope, state, dot_lhs)?; + Expr::Variable(x) => { + let (var_name, var_pos) = &x.0; + self.inc_operations(state) - .map_err(|err| EvalAltResult::new_position(err, pos))?; + .map_err(|err| EvalAltResult::new_position(err, *var_pos))?; + + let (target, _, typ, pos) = search_scope(scope, state, this_ptr, dot_lhs)?; // Constants cannot be modified match typ { ScopeEntryType::Module => unreachable!(), ScopeEntryType::Constant if new_val.is_some() => { return Err(Box::new(EvalAltResult::ErrorAssignmentToConstant( - name.to_string(), + var_name.to_string(), pos, ))); } ScopeEntryType::Constant | ScopeEntryType::Normal => (), } - let this_ptr = &mut target.into(); + let obj_ptr = &mut target.into(); self.eval_dot_index_chain_helper( - state, lib, this_ptr, dot_rhs, idx_values, chain_type, level, new_val, + state, lib, &mut None, obj_ptr, dot_rhs, idx_values, chain_type, level, new_val, ) .map(|(v, _)| v) .map_err(|err| EvalAltResult::new_position(err, *op_pos)) @@ -1333,10 +1410,10 @@ impl Engine { } // {expr}.??? or {expr}[???] expr => { - let val = self.eval_expr(scope, state, lib, expr, level)?; - let this_ptr = &mut val.into(); + let val = self.eval_expr(scope, state, lib, this_ptr, expr, level)?; + let obj_ptr = &mut val.into(); self.eval_dot_index_chain_helper( - state, lib, this_ptr, dot_rhs, idx_values, chain_type, level, new_val, + state, lib, this_ptr, obj_ptr, dot_rhs, idx_values, chain_type, level, new_val, ) .map(|(v, _)| v) .map_err(|err| EvalAltResult::new_position(err, *op_pos)) @@ -1354,6 +1431,7 @@ impl Engine { scope: &mut Scope, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, expr: &Expr, idx_values: &mut StaticVec, size: usize, @@ -1364,10 +1442,11 @@ impl Engine { match expr { Expr::FnCall(x) if x.1.is_none() => { - let arg_values = - x.3.iter() - .map(|arg_expr| self.eval_expr(scope, state, lib, arg_expr, level)) - .collect::, _>>()?; + let arg_values = x + .3 + .iter() + .map(|arg_expr| self.eval_expr(scope, state, lib, this_ptr, arg_expr, level)) + .collect::, _>>()?; idx_values.push(Dynamic::from(arg_values)); } @@ -1379,15 +1458,15 @@ 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 - _ => self.eval_expr(scope, state, lib, lhs, level)?, + _ => self.eval_expr(scope, state, lib, this_ptr, lhs, level)?, }; // Push in reverse order - self.eval_indexed_chain(scope, state, lib, rhs, idx_values, size, level)?; + self.eval_indexed_chain(scope, state, lib, this_ptr, rhs, idx_values, size, level)?; idx_values.push(lhs_val); } - _ => idx_values.push(self.eval_expr(scope, state, lib, expr, level)?), + _ => idx_values.push(self.eval_expr(scope, state, lib, this_ptr, expr, level)?), } Ok(()) @@ -1403,6 +1482,7 @@ impl Engine { mut idx: Dynamic, idx_pos: Position, create: bool, + level: usize, ) -> Result, Box> { self.inc_operations(state)?; @@ -1477,14 +1557,16 @@ impl Engine { _ => { let type_name = self.map_type_name(val.type_name()); let args = &mut [val, &mut idx]; - self.exec_fn_call(state, lib, FN_IDX_GET, true, 0, args, is_ref, None, 0) - .map(|(v, _)| v.into()) - .map_err(|_| { - Box::new(EvalAltResult::ErrorIndexingType( - type_name.into(), - Position::none(), - )) - }) + self.exec_fn_call( + state, lib, FN_IDX_GET, true, 0, args, is_ref, true, None, level, + ) + .map(|(v, _)| v.into()) + .map_err(|_| { + Box::new(EvalAltResult::ErrorIndexingType( + type_name.into(), + Position::none(), + )) + }) } #[cfg(feature = "no_index")] @@ -1501,6 +1583,7 @@ impl Engine { scope: &mut Scope, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, lhs: &Expr, rhs: &Expr, level: usize, @@ -1508,8 +1591,8 @@ impl Engine { self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; - let lhs_value = self.eval_expr(scope, state, lib, lhs, level)?; - let rhs_value = self.eval_expr(scope, state, lib, rhs, level)?; + let lhs_value = self.eval_expr(scope, state, lib, this_ptr, lhs, level)?; + let rhs_value = self.eval_expr(scope, state, lib, this_ptr, rhs, level)?; match rhs_value { #[cfg(not(feature = "no_index"))] @@ -1531,7 +1614,8 @@ impl Engine { let (r, _) = self .call_fn_raw( - &mut scope, state, lib, op, hashes, args, false, def_value, level, + &mut scope, state, lib, op, hashes, args, false, false, def_value, + level, ) .map_err(|err| EvalAltResult::new_position(err, rhs.position()))?; if r.as_bool().unwrap_or(false) { @@ -1566,6 +1650,7 @@ impl Engine { scope: &mut Scope, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, expr: &Expr, level: usize, ) -> Result> { @@ -1573,27 +1658,34 @@ impl Engine { .map_err(|err| EvalAltResult::new_position(err, expr.position()))?; let result = match expr { - Expr::Expr(x) => self.eval_expr(scope, state, lib, x.as_ref(), level), + Expr::Expr(x) => self.eval_expr(scope, state, lib, this_ptr, x.as_ref(), level), Expr::IntegerConstant(x) => Ok(x.0.into()), #[cfg(not(feature = "no_float"))] Expr::FloatConstant(x) => Ok(x.0.into()), Expr::StringConstant(x) => Ok(x.0.to_string().into()), Expr::CharConstant(x) => Ok(x.0.into()), + Expr::Variable(x) if (x.0).0 == KEYWORD_THIS => { + if let Some(ref val) = this_ptr { + Ok((*val).clone()) + } else { + Err(Box::new(EvalAltResult::ErrorUnboundedThis((x.0).1))) + } + } Expr::Variable(_) => { - let (val, _, _, _) = search_scope(scope, state, expr)?; + let (val, _, _, _) = search_scope(scope, state, this_ptr, expr)?; Ok(val.clone()) } Expr::Property(_) => unreachable!(), // Statement block - Expr::Stmt(x) => self.eval_stmt(scope, state, lib, &x.0, level), + Expr::Stmt(x) => self.eval_stmt(scope, state, lib, this_ptr, &x.0, level), // var op= rhs Expr::Assignment(x) if matches!(x.0, Expr::Variable(_)) => { let (lhs_expr, op, rhs_expr, op_pos) = x.as_ref(); - let mut rhs_val = self.eval_expr(scope, state, lib, rhs_expr, level)?; - let (lhs_ptr, name, typ, pos) = search_scope(scope, state, lhs_expr)?; + let mut rhs_val = self.eval_expr(scope, state, lib, this_ptr, rhs_expr, level)?; + let (lhs_ptr, name, typ, pos) = search_scope(scope, state, this_ptr, lhs_expr)?; self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, pos))?; @@ -1632,7 +1724,9 @@ impl Engine { // Set variable value *lhs_ptr = self - .exec_fn_call(state, lib, op, true, hash, args, false, None, level) + .exec_fn_call( + state, lib, op, true, hash, args, false, false, None, level, + ) .map(|(v, _)| v) .map_err(|err| EvalAltResult::new_position(err, *op_pos))?; } @@ -1646,7 +1740,7 @@ impl Engine { // lhs op= rhs Expr::Assignment(x) => { let (lhs_expr, op, rhs_expr, op_pos) = x.as_ref(); - let mut rhs_val = self.eval_expr(scope, state, lib, rhs_expr, level)?; + let mut rhs_val = self.eval_expr(scope, state, lib, this_ptr, rhs_expr, level)?; let new_val = Some(if op.is_empty() { // Normal assignment @@ -1656,10 +1750,10 @@ impl Engine { let op = &op[..op.len() - 1]; // extract operator without = let hash = calc_fn_hash(empty(), op, 2, empty()); let args = &mut [ - &mut self.eval_expr(scope, state, lib, lhs_expr, level)?, + &mut self.eval_expr(scope, state, lib, this_ptr, lhs_expr, level)?, &mut rhs_val, ]; - self.exec_fn_call(state, lib, op, true, hash, args, false, None, level) + self.exec_fn_call(state, lib, op, true, hash, args, false, false, None, level) .map(|(v, _)| v) .map_err(|err| EvalAltResult::new_position(err, *op_pos))? }); @@ -1669,14 +1763,14 @@ impl Engine { Expr::Variable(_) => unreachable!(), // idx_lhs[idx_expr] op= rhs #[cfg(not(feature = "no_index"))] - Expr::Index(_) => { - self.eval_dot_index_chain(scope, state, lib, lhs_expr, level, new_val) - } + Expr::Index(_) => self.eval_dot_index_chain( + scope, state, lib, this_ptr, lhs_expr, level, new_val, + ), // dot_lhs.dot_rhs op= rhs #[cfg(not(feature = "no_object"))] - Expr::Dot(_) => { - self.eval_dot_index_chain(scope, state, lib, lhs_expr, level, new_val) - } + Expr::Dot(_) => self.eval_dot_index_chain( + scope, state, lib, this_ptr, lhs_expr, level, new_val, + ), // Error assignment to constant expr if expr.is_constant() => { Err(Box::new(EvalAltResult::ErrorAssignmentToConstant( @@ -1693,16 +1787,20 @@ impl Engine { // lhs[idx_expr] #[cfg(not(feature = "no_index"))] - Expr::Index(_) => self.eval_dot_index_chain(scope, state, lib, expr, level, None), + Expr::Index(_) => { + self.eval_dot_index_chain(scope, state, lib, this_ptr, expr, level, None) + } // lhs.dot_rhs #[cfg(not(feature = "no_object"))] - Expr::Dot(_) => self.eval_dot_index_chain(scope, state, lib, expr, level, None), + Expr::Dot(_) => { + self.eval_dot_index_chain(scope, state, lib, this_ptr, expr, level, None) + } #[cfg(not(feature = "no_index"))] Expr::Array(x) => Ok(Dynamic(Union::Array(Box::new( x.0.iter() - .map(|item| self.eval_expr(scope, state, lib, item, level)) + .map(|item| self.eval_expr(scope, state, lib, this_ptr, item, level)) .collect::, _>>()?, )))), @@ -1710,7 +1808,7 @@ impl Engine { Expr::Map(x) => Ok(Dynamic(Union::Map(Box::new( x.0.iter() .map(|((key, _), expr)| { - self.eval_expr(scope, state, lib, expr, level) + self.eval_expr(scope, state, lib, this_ptr, expr, level) .map(|val| (key.clone(), val)) }) .collect::, _>>()?, @@ -1729,7 +1827,7 @@ impl Engine { if !self.has_override(lib, (hash_fn, *hash)) { // Fn - only in function call style let expr = args_expr.get(0); - let arg_value = self.eval_expr(scope, state, lib, expr, level)?; + let arg_value = self.eval_expr(scope, state, lib, this_ptr, expr, level)?; return arg_value .take_immutable_string() .map(|s| FnPtr::from(s).into()) @@ -1751,7 +1849,7 @@ impl Engine { // eval - only in function call style let prev_len = scope.len(); let expr = args_expr.get(0); - let script = self.eval_expr(scope, state, lib, expr, level)?; + let script = self.eval_expr(scope, state, lib, this_ptr, expr, level)?; let result = self .eval_script_expr(scope, state, lib, &script) .map_err(|err| EvalAltResult::new_position(err, expr.position())); @@ -1766,7 +1864,7 @@ impl Engine { } } - // Normal function call - except for eval (handled above) + // Normal function call - except for Fn and eval (handled above) let mut arg_values: StaticVec; let mut args: StaticVec<_>; let mut is_ref = false; @@ -1783,18 +1881,21 @@ impl Engine { arg_values = args_expr .iter() .skip(1) - .map(|expr| self.eval_expr(scope, state, lib, expr, level)) + .map(|expr| { + self.eval_expr(scope, state, lib, this_ptr, expr, level) + }) .collect::>()?; - let (target, _, typ, pos) = search_scope(scope, state, lhs)?; - self.inc_operations(state) - .map_err(|err| EvalAltResult::new_position(err, pos))?; + let (target, _, typ, pos) = search_scope(scope, state, this_ptr, lhs)?; match typ { ScopeEntryType::Module => unreachable!(), ScopeEntryType::Constant | ScopeEntryType::Normal => (), } + self.inc_operations(state) + .map_err(|err| EvalAltResult::new_position(err, pos))?; + args = once(target).chain(arg_values.iter_mut()).collect(); is_ref = true; @@ -1803,7 +1904,9 @@ impl Engine { _ => { arg_values = args_expr .iter() - .map(|expr| self.eval_expr(scope, state, lib, expr, level)) + .map(|expr| { + self.eval_expr(scope, state, lib, this_ptr, expr, level) + }) .collect::>()?; args = arg_values.iter_mut().collect(); @@ -1813,7 +1916,7 @@ impl Engine { let args = args.as_mut(); self.exec_fn_call( - state, lib, name, *native, *hash, args, is_ref, def_val, level, + state, lib, name, *native, *hash, args, is_ref, false, def_val, level, ) .map(|(v, _)| v) .map_err(|err| EvalAltResult::new_position(err, *pos)) @@ -1826,7 +1929,7 @@ impl Engine { let mut arg_values = args_expr .iter() - .map(|expr| self.eval_expr(scope, state, lib, expr, level)) + .map(|expr| self.eval_expr(scope, state, lib, this_ptr, expr, level)) .collect::, _>>()?; let mut args: StaticVec<_> = arg_values.iter_mut().collect(); @@ -1872,8 +1975,10 @@ impl Engine { let args = args.as_mut(); let fn_def = f.get_fn_def(); let mut scope = Scope::new(); - self.call_script_fn(&mut scope, state, lib, name, fn_def, args, level) - .map_err(|err| EvalAltResult::new_position(err, *pos)) + self.call_script_fn( + &mut scope, state, lib, &mut None, name, fn_def, args, level, + ) + .map_err(|err| EvalAltResult::new_position(err, *pos)) } Ok(f) => { f.get_native_fn()(self, args.as_mut()).map_err(|err| err.new_position(*pos)) @@ -1888,19 +1993,19 @@ impl Engine { } } - Expr::In(x) => self.eval_in_expr(scope, state, lib, &x.0, &x.1, level), + Expr::In(x) => self.eval_in_expr(scope, state, lib, this_ptr, &x.0, &x.1, level), Expr::And(x) => { let (lhs, rhs, _) = x.as_ref(); Ok((self - .eval_expr(scope, state, lib, lhs, level)? + .eval_expr(scope, state, lib, this_ptr, lhs, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("AND".into(), lhs.position()) })? && // Short-circuit using && self - .eval_expr(scope, state, lib, rhs, level)? + .eval_expr(scope, state, lib, this_ptr, rhs, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("AND".into(), rhs.position()) @@ -1911,14 +2016,14 @@ impl Engine { Expr::Or(x) => { let (lhs, rhs, _) = x.as_ref(); Ok((self - .eval_expr(scope, state, lib, lhs, level)? + .eval_expr(scope, state, lib, this_ptr, lhs, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("OR".into(), lhs.position()) })? || // Short-circuit using || self - .eval_expr(scope, state, lib, rhs, level)? + .eval_expr(scope, state, lib, this_ptr, rhs, level)? .as_bool() .map_err(|_| { EvalAltResult::ErrorBooleanArgMismatch("OR".into(), rhs.position()) @@ -1942,6 +2047,7 @@ impl Engine { scope: &mut Scope, state: &mut State, lib: &Module, + this_ptr: &mut Option<&mut Dynamic>, stmt: &Stmt, level: usize, ) -> Result> { @@ -1954,7 +2060,7 @@ impl Engine { // Expression as statement Stmt::Expr(expr) => { - let result = self.eval_expr(scope, state, lib, expr, level)?; + let result = self.eval_expr(scope, state, lib, this_ptr, expr, level)?; Ok(match expr.as_ref() { // If it is a simple assignment, erase the result at the root @@ -1969,7 +2075,7 @@ impl Engine { state.scope_level += 1; let result = x.0.iter().try_fold(Default::default(), |_, stmt| { - self.eval_stmt(scope, state, lib, stmt, level) + self.eval_stmt(scope, state, lib, this_ptr, stmt, level) }); scope.rewind(prev_len); @@ -1986,14 +2092,14 @@ impl Engine { Stmt::IfThenElse(x) => { let (expr, if_block, else_block) = x.as_ref(); - self.eval_expr(scope, state, lib, expr, level)? + self.eval_expr(scope, state, lib, this_ptr, expr, level)? .as_bool() .map_err(|_| Box::new(EvalAltResult::ErrorLogicGuard(expr.position()))) .and_then(|guard_val| { if guard_val { - self.eval_stmt(scope, state, lib, if_block, level) + self.eval_stmt(scope, state, lib, this_ptr, if_block, level) } else if let Some(stmt) = else_block { - self.eval_stmt(scope, state, lib, stmt, level) + self.eval_stmt(scope, state, lib, this_ptr, stmt, level) } else { Ok(Default::default()) } @@ -2004,8 +2110,11 @@ impl Engine { Stmt::While(x) => loop { let (expr, body) = x.as_ref(); - match self.eval_expr(scope, state, lib, expr, level)?.as_bool() { - Ok(true) => match self.eval_stmt(scope, state, lib, body, level) { + match self + .eval_expr(scope, state, lib, this_ptr, expr, level)? + .as_bool() + { + Ok(true) => match self.eval_stmt(scope, state, lib, this_ptr, body, level) { Ok(_) => (), Err(err) => match *err { EvalAltResult::ErrorLoopBreak(false, _) => (), @@ -2022,7 +2131,7 @@ impl Engine { // Loop statement Stmt::Loop(body) => loop { - match self.eval_stmt(scope, state, lib, body, level) { + match self.eval_stmt(scope, state, lib, this_ptr, body, level) { Ok(_) => (), Err(err) => match *err { EvalAltResult::ErrorLoopBreak(false, _) => (), @@ -2035,7 +2144,7 @@ impl Engine { // For loop Stmt::For(x) => { let (name, expr, stmt) = x.as_ref(); - let iter_type = self.eval_expr(scope, state, lib, expr, level)?; + let iter_type = self.eval_expr(scope, state, lib, this_ptr, expr, level)?; let tid = iter_type.type_id(); if let Some(func) = self @@ -2054,7 +2163,7 @@ impl Engine { self.inc_operations(state) .map_err(|err| EvalAltResult::new_position(err, stmt.position()))?; - match self.eval_stmt(scope, state, lib, stmt, level) { + match self.eval_stmt(scope, state, lib, this_ptr, stmt, level) { Ok(_) => (), Err(err) => match *err { EvalAltResult::ErrorLoopBreak(false, _) => (), @@ -2081,7 +2190,7 @@ impl Engine { // Return value Stmt::ReturnWithVal(x) if x.1.is_some() && (x.0).0 == ReturnType::Return => { Err(Box::new(EvalAltResult::Return( - self.eval_expr(scope, state, lib, x.1.as_ref().unwrap(), level)?, + self.eval_expr(scope, state, lib, this_ptr, x.1.as_ref().unwrap(), level)?, (x.0).1, ))) } @@ -2093,7 +2202,8 @@ impl Engine { // Throw value Stmt::ReturnWithVal(x) if x.1.is_some() && (x.0).0 == ReturnType::Exception => { - let val = self.eval_expr(scope, state, lib, x.1.as_ref().unwrap(), level)?; + let val = + self.eval_expr(scope, state, lib, this_ptr, x.1.as_ref().unwrap(), level)?; Err(Box::new(EvalAltResult::ErrorRuntime( val.take_string().unwrap_or_else(|_| "".into()), (x.0).1, @@ -2110,7 +2220,8 @@ impl Engine { // Let statement Stmt::Let(x) if x.1.is_some() => { let ((var_name, _), expr) = x.as_ref(); - let val = self.eval_expr(scope, state, lib, expr.as_ref().unwrap(), level)?; + let val = + self.eval_expr(scope, state, lib, this_ptr, expr.as_ref().unwrap(), level)?; let var_name = unsafe_cast_var_name_to_lifetime(var_name, &state); scope.push_dynamic_value(var_name, ScopeEntryType::Normal, val, false); Ok(Default::default()) @@ -2126,7 +2237,7 @@ impl Engine { // Const statement Stmt::Const(x) if x.1.is_constant() => { let ((var_name, _), expr) = x.as_ref(); - let val = self.eval_expr(scope, state, lib, &expr, level)?; + let val = self.eval_expr(scope, state, lib, this_ptr, &expr, level)?; let var_name = unsafe_cast_var_name_to_lifetime(var_name, &state); scope.push_dynamic_value(var_name, ScopeEntryType::Constant, val, true); Ok(Default::default()) @@ -2145,7 +2256,7 @@ impl Engine { } if let Some(path) = self - .eval_expr(scope, state, lib, &expr, level)? + .eval_expr(scope, state, lib, this_ptr, &expr, level)? .try_cast::() { #[cfg(not(feature = "no_module"))] diff --git a/src/error.rs b/src/error.rs index f4792f11..58423ef9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,7 +3,13 @@ use crate::result::EvalAltResult; use crate::token::Position; -use crate::stdlib::{boxed::Box, char, error::Error, fmt, string::String}; +use crate::stdlib::{ + boxed::Box, + char, + error::Error, + fmt, + string::{String, ToString}, +}; /// Error when tokenizing the script text. #[derive(Debug, Eq, PartialEq, Clone, Hash)] diff --git a/src/fn_native.rs b/src/fn_native.rs index f3ab7053..e89cf334 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -132,7 +132,7 @@ impl CallableFunction { Self::Method(_) | Self::Iterator(_) | Self::Script(_) => false, } } - /// Is this a pure native Rust method-call? + /// Is this a native Rust method function? pub fn is_method(&self) -> bool { match self { Self::Method(_) => true, diff --git a/src/optimize.rs b/src/optimize.rs index c599a44c..7582aa6d 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -128,6 +128,7 @@ fn call_fn_with_constant_arguments( (hash_fn, 0), arg_values.iter_mut().collect::>().as_mut(), false, + false, None, 0, ) diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 59358063..d492f52f 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -8,7 +8,7 @@ use crate::parser::{ImmutableString, INT}; use crate::result::EvalAltResult; use crate::token::Position; -use crate::stdlib::{any::TypeId, boxed::Box}; +use crate::stdlib::{any::TypeId, boxed::Box, string::ToString}; // Register array utility functions fn push(list: &mut Array, item: T) -> FuncReturn<()> { diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 46805750..a472c2dd 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -12,6 +12,7 @@ use crate::engine::Array; use crate::stdlib::{ any::TypeId, + boxed::Box, fmt::Display, format, string::{String, ToString}, diff --git a/src/parser.rs b/src/parser.rs index 9d2d7e8d..9a318849 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,7 +2,7 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; -use crate::engine::{make_getter, make_setter, Engine}; +use crate::engine::{make_getter, make_setter, Engine, KEYWORD_THIS}; use crate::error::{LexError, ParseError, ParseErrorType}; use crate::module::{Module, ModuleRef}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; @@ -1847,19 +1847,8 @@ fn parse_binary_op( #[cfg(not(feature = "no_object"))] Token::Period => { - let mut rhs = args.pop(); + let rhs = args.pop(); let current_lhs = args.pop(); - - match &mut rhs { - // current_lhs.rhs(...) - method call - Expr::FnCall(x) => { - let ((id, _, _), _, hash, args, _) = x.as_mut(); - // Recalculate function call hash because there is an additional argument - *hash = calc_fn_hash(empty(), id, args.len() + 1, empty()); - } - _ => (), - } - make_dot_expr(current_lhs, rhs, pos)? } @@ -2057,6 +2046,16 @@ fn parse_let( (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; + // Check if the name is allowed + match name.as_str() { + KEYWORD_THIS => { + return Err( + PERR::BadInput(LexError::MalformedIdentifier(name).to_string()).into_err(pos), + ) + } + _ => (), + } + // let name = ... if match_token(input, Token::Equals)? { // let name = expr @@ -2073,7 +2072,7 @@ fn parse_let( state.push((name.clone(), ScopeEntryType::Constant)); Ok(Stmt::Const(Box::new(((name, pos), init_value)))) } - // const name = expr - error + // const name = expr: error ScopeEntryType::Constant => { Err(PERR::ForbiddenConstantExpr(name).into_err(init_value.position())) } diff --git a/src/result.rs b/src/result.rs index 5fb89758..06bc80d3 100644 --- a/src/result.rs +++ b/src/result.rs @@ -39,6 +39,8 @@ pub enum EvalAltResult { /// An error has occurred inside a called function. /// Wrapped values re the name of the function and the interior error. ErrorInFunctionCall(String, Box, Position), + /// Access to `this` that is not bounded. + ErrorUnboundedThis(Position), /// Non-boolean operand encountered for boolean operator. Wrapped value is the operator. ErrorBooleanArgMismatch(String, Position), /// Non-character value encountered where a character is required. @@ -110,6 +112,7 @@ impl EvalAltResult { Self::ErrorParsing(p, _) => p.desc(), Self::ErrorInFunctionCall(_, _, _) => "Error in called function", Self::ErrorFunctionNotFound(_, _) => "Function not found", + Self::ErrorUnboundedThis(_) => "'this' is not bounded", Self::ErrorBooleanArgMismatch(_, _) => "Boolean operator expects boolean operands", Self::ErrorCharMismatch(_) => "Character expected", Self::ErrorNumericIndexExpr(_) => { @@ -184,6 +187,7 @@ impl fmt::Display for EvalAltResult { Self::ErrorIndexingType(_, _) | Self::ErrorNumericIndexExpr(_) | Self::ErrorStringIndexExpr(_) + | Self::ErrorUnboundedThis(_) | Self::ErrorImportExpr(_) | Self::ErrorLogicGuard(_) | Self::ErrorFor(_) @@ -270,6 +274,7 @@ impl EvalAltResult { Self::ErrorParsing(_, pos) | Self::ErrorFunctionNotFound(_, pos) | Self::ErrorInFunctionCall(_, _, pos) + | Self::ErrorUnboundedThis(pos) | Self::ErrorBooleanArgMismatch(_, pos) | Self::ErrorCharMismatch(pos) | Self::ErrorArrayBounds(_, _, pos) @@ -309,6 +314,7 @@ impl EvalAltResult { Self::ErrorParsing(_, pos) | Self::ErrorFunctionNotFound(_, pos) | Self::ErrorInFunctionCall(_, _, pos) + | Self::ErrorUnboundedThis(pos) | Self::ErrorBooleanArgMismatch(_, pos) | Self::ErrorCharMismatch(pos) | Self::ErrorArrayBounds(_, _, pos) diff --git a/tests/bool_op.rs b/tests/bool_op.rs index 913106e6..84c3d44f 100644 --- a/tests/bool_op.rs +++ b/tests/bool_op.rs @@ -39,9 +39,9 @@ fn test_bool_op_short_circuit() -> Result<(), Box> { assert_eq!( engine.eval::( r" - let this = true; + let x = true; - this || { throw; }; + x || { throw; }; " )?, true @@ -50,9 +50,9 @@ fn test_bool_op_short_circuit() -> Result<(), Box> { assert_eq!( engine.eval::( r" - let this = false; + let x = false; - this && { throw; }; + x && { throw; }; " )?, false @@ -68,9 +68,9 @@ fn test_bool_op_no_short_circuit1() { assert!(engine .eval::( r" - let this = true; + let x = true; - this | { throw; } + x | { throw; } " ) .is_err()); @@ -83,9 +83,9 @@ fn test_bool_op_no_short_circuit2() { assert!(engine .eval::( r" - let this = false; + let x = false; - this & { throw; } + x & { throw; } " ) .is_err()); diff --git a/tests/functions.rs b/tests/functions.rs index c59f0717..49789da4 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -19,28 +19,33 @@ fn test_functions() -> Result<(), Box> { #[cfg(not(feature = "no_object"))] assert_eq!( - engine.eval::("fn add(x, n) { x + n } let x = 40; x.add(2)")?, + engine.eval::("fn add(n) { this + n } let x = 40; x.add(2)")?, 42 ); #[cfg(not(feature = "no_object"))] assert_eq!( - engine.eval::("fn add(x, n) { x += n; } let x = 40; x.add(2); x")?, - 40 + engine.eval::("fn add(n) { this += n; } let x = 40; x.add(2); x")?, + 42 ); assert_eq!(engine.eval::("fn mul2(x) { x * 2 } mul2(21)")?, 42); + assert_eq!( + engine.eval::("fn mul2(x) { x *= 2 } let a = 21; mul2(a); a")?, + 21 + ); + #[cfg(not(feature = "no_object"))] assert_eq!( - engine.eval::("fn mul2(x) { x * 2 } let x = 21; x.mul2()")?, + engine.eval::("fn mul2() { this * 2 } let x = 21; x.mul2()")?, 42 ); #[cfg(not(feature = "no_object"))] assert_eq!( - engine.eval::("fn mul2(x) { x *= 2; } let x = 21; x.mul2(); x")?, - 21 + engine.eval::("fn mul2() { this *= 2; } let x = 21; x.mul2(); x")?, + 42 ); Ok(()) diff --git a/tests/maps.rs b/tests/maps.rs index 8061f1ed..bc2b24a9 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -250,3 +250,25 @@ fn test_map_json() -> Result<(), Box> { Ok(()) } + +#[test] +#[cfg(not(feature = "no_function"))] +fn test_map_oop() -> Result<(), Box> { + let engine = Engine::new(); + + assert_eq!( + engine.eval::( + r#" + let obj = #{ data: 40, action: Fn("abc") }; + + fn abc(x) { this.data += x; } + + obj.action(2); + obj.data + "#, + )?, + 42 + ); + + Ok(()) +} diff --git a/tests/modules.rs b/tests/modules.rs index 771aa490..e6d604b6 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -1,9 +1,7 @@ #![cfg(not(feature = "no_module"))] use rhai::{ - module_resolvers, Dynamic, Engine, EvalAltResult, Module, ParseError, ParseErrorType, Scope, - INT, + module_resolvers, Engine, EvalAltResult, Module, ParseError, ParseErrorType, Scope, INT, }; -use std::any::TypeId; #[test] fn test_module() {