diff --git a/README.md b/README.md index ad4a5037..97c65279 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ It doesn't attempt to be a new language. For example: * No classes. Well, Rust doesn't either. On the other hand... * No traits... so it is also not Rust. Do your Rusty stuff in Rust. -* No structures - definte your types in Rust instead; Rhai can seamless work with _any Rust type_. +* No structures - define your types in Rust instead; Rhai can seamlessly work with _any Rust type_. * No first-class functions - Code your functions in Rust instead, and register them with Rhai. * No closures - do your closure magic in Rust instead; [turn a Rhai scripted function into a Rust closure](#calling-rhai-functions-from-rust). * It is best to expose an API in Rhai for scripts to call. All your core functionalities should be in Rust. @@ -300,7 +300,7 @@ let ast = engine.compile(true, r" // a function with two parameters: String and i64 fn hello(x, y) { - x.len() + y + x.len + y } // functions can be overloaded: this one takes only one parameter @@ -355,7 +355,7 @@ use rhai::{Engine, Func}; // use 'Func' for 'create_from_s let engine = Engine::new(); // create a new 'Engine' just for this -let script = "fn calc(x, y) { x + y.len() < 42 }"; +let script = "fn calc(x, y) { x + y.len < 42 }"; // Func takes two type parameters: // 1) a tuple made up of the types of the script function's parameters @@ -945,8 +945,8 @@ If the [`no_object`] feature is turned on, however, the _method_ style of functi (i.e. calling a function as an object-method) is no longer supported. ```rust -// Below is a syntax error under 'no_object' because 'len' cannot be called in method style. -let result = engine.eval::("let x = [1, 2, 3]; x.len()")?; +// Below is a syntax error under 'no_object' because 'clear' cannot be called in method style. +let result = engine.eval::<()>("let x = [1, 2, 3]; x.clear()")?; ``` [`type_of()`] works fine with custom types and returns the name of the type. @@ -1082,7 +1082,7 @@ fn main() -> Result<(), Box> // First invocation engine.eval_with_scope::<()>(&mut scope, r" - let x = 4 + 5 - y + z + s.len(); + let x = 4 + 5 - y + z + s.len; y = 1; ")?; @@ -1122,6 +1122,7 @@ Comments -------- Comments are C-style, including '`/*` ... `*/`' pairs and '`//`' for comments to the end of the line. +Comments can be nested. ```rust let /* intruder comment */ name = "Bob"; @@ -1138,14 +1139,35 @@ let /* intruder comment */ name = "Bob"; */ ``` +Keywords +-------- + +The following are reserved keywords in Rhai: + +| Keywords | Usage | Not available under feature | +| ------------------------------------------------- | --------------------- | :-------------------------: | +| `true`, `false` | Boolean constants | | +| `let`, `const` | Variable declarations | | +| `if`, `else` | Control flow | | +| `while`, `loop`, `for`, `in`, `continue`, `break` | Looping | | +| `fn`, `private` | Functions | [`no_function`] | +| `return` | Return values | | +| `throw` | Return errors | | +| `import`, `export`, `as` | Modules | [`no_module`] | + +Keywords cannot be the name of a [function] or [variable], unless the relevant exclusive feature is enabled. +For example, `fn` is a valid variable name if the [`no_function`] feature is used. + Statements ---------- -Statements are terminated by semicolons '`;`' - they are mandatory, except for the _last_ statement where it can be omitted. +Statements are terminated by semicolons '`;`' and they are mandatory, +except for the _last_ statement in a _block_ (enclosed by '`{`' .. '`}`' pairs) where it can be omitted. -A statement can be used anywhere where an expression is expected. The _last_ statement of a statement block -(enclosed by '`{`' .. '`}`' pairs) is always the return value of the statement. If a statement has no return value -(e.g. variable definitions, assignments) then the value will be [`()`]. +A statement can be used anywhere where an expression is expected. These are called, for lack of a more +creative name, "statement expressions." The _last_ statement of a statement block is _always_ the block's +return value when used as a statement. +If the last statement has no return value (e.g. variable definitions, assignments) then it is assumed to be [`()`]. ```rust let a = 42; // normal assignment statement @@ -1153,16 +1175,17 @@ let a = foo(42); // normal function call statement foo < 42; // normal expression as statement let a = { 40 + 2 }; // 'a' is set to the value of the statement block, which is the value of the last statement -// ^ notice that the last statement does not require a terminating semicolon (although it also works with it) -// ^ notice that a semicolon is required here to terminate the assignment statement; it is syntax error without it +// ^ the last statement does not require a terminating semicolon (although it also works with it) +// ^ semicolon required here to terminate the assignment statement; it is a syntax error without it -4 * 10 + 2 // this is also a statement, which is an expression, with no ending semicolon because - // it is the last statement of the whole block +4 * 10 + 2 // a statement which is just one expression; no ending semicolon is OK + // because it is the last statement of the whole block ``` Variables --------- +[variable]: #variables [variables]: #variables Variables in Rhai follow normal C naming rules (i.e. must contain only ASCII letters, digits and underscores '`_`'). @@ -1289,16 +1312,16 @@ Floating-point functions The following standard functions (defined in the [`BasicMathPackage`](#packages) but excluded if using a [raw `Engine`]) operate on `f64` only: -| Category | Functions | -| ---------------- | ------------------------------------------------------------ | -| Trigonometry | `sin`, `cos`, `tan`, `sinh`, `cosh`, `tanh` in degrees | -| Arc-trigonometry | `asin`, `acos`, `atan`, `asinh`, `acosh`, `atanh` in degrees | -| Square root | `sqrt` | -| Exponential | `exp` (base _e_) | -| Logarithmic | `ln` (base _e_), `log10` (base 10), `log` (any base) | -| Rounding | `floor`, `ceiling`, `round`, `int`, `fraction` | -| Conversion | [`to_int`] | -| Testing | `is_nan`, `is_finite`, `is_infinite` | +| Category | Functions | +| ---------------- | --------------------------------------------------------------------- | +| Trigonometry | `sin`, `cos`, `tan`, `sinh`, `cosh`, `tanh` in degrees | +| Arc-trigonometry | `asin`, `acos`, `atan`, `asinh`, `acosh`, `atanh` in degrees | +| Square root | `sqrt` | +| Exponential | `exp` (base _e_) | +| Logarithmic | `ln` (base _e_), `log10` (base 10), `log` (any base) | +| Rounding | `floor`, `ceiling`, `round`, `int`, `fraction` methods and properties | +| Conversion | [`to_int`] | +| Testing | `is_nan`, `is_finite`, `is_infinite` methods and properties | Strings and Chars ----------------- @@ -1307,8 +1330,8 @@ Strings and Chars [strings]: #strings-and-chars [char]: #strings-and-chars -String and char literals follow C-style formatting, with support for Unicode ('`\u`_xxxx_' or '`\U`_xxxxxxxx_') and -hex ('`\x`_xx_') escape sequences. +String and character literals follow C-style formatting, with support for Unicode ('`\u`_xxxx_' or '`\U`_xxxxxxxx_') +and hex ('`\x`_xx_') escape sequences. Hex sequences map to ASCII characters, while '`\u`' maps to 16-bit common Unicode code points and '`\U`' maps the full, 32-bit extended Unicode code points. @@ -1388,34 +1411,34 @@ record == "Bob X. Davis: age 42 ❤\n"; ### Built-in functions -The following standard methods (defined in the [`MoreStringPackage`](#packages) but excluded if using a [raw `Engine`]) operate on strings: +The following standard methods (mostly defined in the [`MoreStringPackage`](#packages) but excluded if using a [raw `Engine`]) operate on strings: -| Function | Parameter(s) | Description | -| ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | -| `len` | _none_ | returns the number of characters (not number of bytes) in the string | -| `pad` | character to pad, target length | pads the string with an character to at least a specified length | -| `+=` operator, `append` | character/string to append | Adds a character or a string to the end of another string | -| `clear` | _none_ | empties the string | -| `truncate` | target length | cuts off the string at exactly a specified number of characters | -| `contains` | character/sub-string to search for | checks if a certain character or sub-string occurs in the string | -| `index_of` | character/sub-string to search for, start index _(optional)_ | returns the index that a certain character or sub-string occurs in the string, or -1 if not found | -| `sub_string` | start index, length _(optional)_ | extracts a sub-string (to the end of the string if length is not specified) | -| `crop` | start index, length _(optional)_ | retains only a portion of the string (to the end of the string if length is not specified) | -| `replace` | target character/sub-string, replacement character/string | replaces a sub-string with another | -| `trim` | _none_ | trims the string of whitespace at the beginning and end | +| Function | Parameter(s) | Description | +| ------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| `len` method and property | _none_ | returns the number of characters (not number of bytes) in the string | +| `pad` | character to pad, target length | pads the string with an character to at least a specified length | +| `+=` operator, `append` | character/string to append | Adds a character or a string to the end of another string | +| `clear` | _none_ | empties the string | +| `truncate` | target length | cuts off the string at exactly a specified number of characters | +| `contains` | character/sub-string to search for | checks if a certain character or sub-string occurs in the string | +| `index_of` | character/sub-string to search for, start index _(optional)_ | returns the index that a certain character or sub-string occurs in the string, or -1 if not found | +| `sub_string` | start index, length _(optional)_ | extracts a sub-string (to the end of the string if length is not specified) | +| `crop` | start index, length _(optional)_ | retains only a portion of the string (to the end of the string if length is not specified) | +| `replace` | target character/sub-string, replacement character/string | replaces a sub-string with another | +| `trim` | _none_ | trims the string of whitespace at the beginning and end | ### Examples ```rust let full_name == " Bob C. Davis "; -full_name.len() == 14; +full_name.len == 14; full_name.trim(); -full_name.len() == 12; +full_name.len == 12; full_name == "Bob C. Davis"; full_name.pad(15, '$'); -full_name.len() == 15; +full_name.len == 15; full_name == "Bob C. Davis$$$"; let n = full_name.index_of('$'); @@ -1426,11 +1449,11 @@ full_name.index_of("$$", n + 1) == 13; full_name.sub_string(n, 3) == "$$$"; full_name.truncate(6); -full_name.len() == 6; +full_name.len == 6; full_name == "Bob C."; full_name.replace("Bob", "John"); -full_name.len() == 7; +full_name.len == 7; full_name == "John C."; full_name.contains('C') == true; @@ -1443,7 +1466,7 @@ full_name.crop(0, 1); full_name == "C"; full_name.clear(); -full_name.len() == 0; +full_name.len == 0; ``` Arrays @@ -1463,21 +1486,21 @@ Arrays are disabled via the [`no_index`] feature. ### Built-in functions -The following methods (defined in the [`BasicArrayPackage`](#packages) but excluded if using a [raw `Engine`]) operate on arrays: +The following methods (mostly defined in the [`BasicArrayPackage`](#packages) but excluded if using a [raw `Engine`]) operate on arrays: -| Function | Parameter(s) | Description | -| ----------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| `push` | element to insert | inserts an element at the end | -| `+=` operator, `append` | array to append | concatenates the second array to the end of the first | -| `+` operator | first array, second array | concatenates the first array with the second | -| `insert` | element to insert, position
(beginning if <= 0, end if >= length) | insert an element at a certain index | -| `pop` | _none_ | removes the last element and returns it ([`()`] if empty) | -| `shift` | _none_ | removes the first element and returns it ([`()`] if empty) | -| `remove` | index | removes an element at a particular index and returns it, or returns [`()`] if the index is not valid | -| `len` | _none_ | returns the number of elements | -| `pad` | element to pad, target length | pads the array with an element to at least a specified length | -| `clear` | _none_ | empties the array | -| `truncate` | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) | +| Function | Parameter(s) | Description | +| ------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `push` | element to insert | inserts an element at the end | +| `+=` operator, `append` | array to append | concatenates the second array to the end of the first | +| `+` operator | first array, second array | concatenates the first array with the second | +| `insert` | element to insert, position
(beginning if <= 0, end if >= length) | insert an element at a certain index | +| `pop` | _none_ | removes the last element and returns it ([`()`] if empty) | +| `shift` | _none_ | removes the first element and returns it ([`()`] if empty) | +| `remove` | index | removes an element at a particular index and returns it, or returns [`()`] if the index is not valid | +| `len` method and property | _none_ | returns the number of elements | +| `pad` | element to pad, target length | pads the array with an element to at least a specified length | +| `clear` | _none_ | empties the array | +| `truncate` | target length | cuts off the array at exactly a specified length (discarding all subsequent elements) | ### Examples @@ -1487,7 +1510,7 @@ let y = [2, 3]; // array literal with 2 elements y.insert(0, 1); // insert element at the beginning y.insert(999, 4); // insert element at the end -y.len() == 4; +y.len == 4; y[0] == 1; y[1] == 2; @@ -1504,7 +1527,7 @@ y[1] = 42; // array elements can be reassigned y.remove(2) == 3; // remove element -y.len() == 3; +y.len == 3; y[2] == 4; // elements after the removed element are shifted @@ -1528,7 +1551,7 @@ foo == 1; y.push(4); // 4 elements y.push(5); // 5 elements -y.len() == 5; +y.len == 5; let first = y.shift(); // remove the first element, 4 elements remaining first == 1; @@ -1536,7 +1559,7 @@ first == 1; let last = y.pop(); // remove the last element, 3 elements remaining last == 5; -y.len() == 3; +y.len == 3; for item in y { // arrays can be iterated with a 'for' statement print(item); @@ -1544,15 +1567,15 @@ for item in y { // arrays can be iterated with a 'for' statement y.pad(10, "hello"); // pad the array up to 10 elements -y.len() == 10; +y.len == 10; y.truncate(5); // truncate the array to 5 elements -y.len() == 5; +y.len == 5; y.clear(); // empty the array -y.len() == 0; +y.len == 0; ``` `push` and `pad` are only defined for standard built-in types. For custom types, type-specific versions must be registered: @@ -1722,10 +1745,10 @@ The Rust type of a timestamp is `std::time::Instant`. [`type_of()`] a timestamp The following methods (defined in the [`BasicTimePackage`](#packages) but excluded if using a [raw `Engine`]) operate on timestamps: -| Function | Parameter(s) | Description | -| ------------ | ---------------------------------- | -------------------------------------------------------- | -| `elapsed` | _none_ | returns the number of seconds since the timestamp | -| `-` operator | later timestamp, earlier timestamp | returns the number of seconds between the two timestamps | +| Function | Parameter(s) | Description | +| ------------------ | ---------------------------------- | -------------------------------------------------------- | +| `elapsed` property | _none_ | returns the number of seconds since the timestamp | +| `-` operator | later timestamp, earlier timestamp | returns the number of seconds between the two timestamps | ### Examples @@ -1734,7 +1757,7 @@ let now = timestamp(); // Do some lengthy operation... -if now.elapsed() > 30.0 { +if now.elapsed > 30.0 { print("takes too long (over 30 seconds)!") } ``` @@ -1963,6 +1986,9 @@ println!(result); // prints "Runtime error: 42 is too large! (line 5, Functions --------- +[function]: #functions +[functions]: #functions + Rhai supports defining functions in script (unless disabled with [`no_function`]): ```rust @@ -2007,8 +2033,9 @@ fn foo() { x } // <- syntax error: variable 'x' doesn't exist Functions defined in script always take [`Dynamic`] parameters (i.e. the parameter can be of any type). It is important to remember that all arguments are passed by _value_, so all functions are _pure_ -(i.e. they never modifytheir arguments). -Any update to an argument will **not** be reflected back to the caller. This can introduce subtle bugs, if not careful. +(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 @@ -2016,7 +2043,7 @@ fn change(s) { // 's' is passed by value } let x = 500; -x.change(); // de-sugars to change(x) +x.change(); // de-sugars to 'change(x)' x == 500; // 'x' is NOT changed! ``` @@ -2067,14 +2094,24 @@ Members and methods ------------------- Properties and methods in a Rust custom type registered with the [`Engine`] can be called just like in Rust. +Unlike functions defined in script (for which all arguments are passed by _value_), +native Rust functions may mutate the object (or the first argument if called in normal function call style). ```rust let a = new_ts(); // constructor function -a.field = 500; // property access -a.update(); // method call, 'a' can be changed +a.field = 500; // property setter +a.update(); // method call, 'a' can be modified -update(a); // this works, but 'a' is unchanged because only - // a COPY of 'a' is passed to 'update' by VALUE +update(a); // <- this de-sugars to 'a.update()' this if 'a' is a simple variable + // unlike scripted functions, 'a' can be modified and is not a copy + +let array = [ a ]; + +update(array[0]); // <- 'array[0]' is an expression returning a calculated value, + // a transient (i.e. a copy) so this statement has no effect + // except waste a lot of time cloning + +array[0].update(); // <- call this method-call style will update 'a' ``` Custom types, properties and methods can be disabled via the [`no_object`] feature. @@ -2244,7 +2281,7 @@ let ast = engine.compile(r#" x + 1 } fn add_len(x, y) { - x + y.len() + x + y.len } // Imported modules can become sub-modules diff --git a/RELEASES.md b/RELEASES.md index bc5a2125..286aac4a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,6 +9,12 @@ Regression fix * Do not optimize script with `eval_expression` - it is assumed to be one-off and short. +Bug fixes +--------- + +* Indexing with an index or dot expression now works property (it compiled wrongly before). + For example, `let s = "hello"; s[s.len-1] = 'x';` now works property instead of an error. + Breaking changes ---------------- @@ -22,13 +28,19 @@ Breaking changes This is to avoid excessive cloning of strings. All native-Rust functions taking string parameters should switch to `rhai::ImmutableString` (which is either `Rc` or `Arc` depending on whether the `sync` feature is used). +* Native Rust functions registered with the `Engine` also mutates the first argument when called in + normal function-call style (previously the first argument will be passed by _value_ if not called + in method-call style). Of course, if the first argument is a calculated value (e.g. result of an + expression), then mutating it has no effect, but at least it is not cloned. +* Some built-in methods (e.g. `len` for string, `floor` for `FLOAT`) now have _property_ versions in + addition to methods to simplify coding. New features ------------ * Set limit on maximum level of nesting expressions and statements to avoid panics during parsing. * New `EvalPackage` to disable `eval`. -* More benchmarks. +* `Module::set_getter_fn`, `Module::set_setter_fn` and `Module:set_indexer_fn` to register getter/setter/indexer functions. Speed enhancements ------------------ @@ -43,6 +55,11 @@ Speed enhancements (and therefore the `CorePackage`) because they are now always available, even under `Engine::new_raw`. * Operator-assignment statements (e.g. `+=`) are now handled directly and much faster. * Strings are now _immutable_ and use the `rhai::ImmutableString` type, eliminating large amounts of cloning. +* For Native Rust functions taking a first `&mut` parameter, the first argument is passed by reference instead of + by value, even if not called in method-call style. This allows many functions declared with `&mut` parameter to avoid + excessive cloning. For example, if `a` is a large array, getting its length in this manner: `len(a)` used to result + in a full clone of `a` before taking the length and throwing the copy away. Now, `a` is simply passed by reference, + avoiding the cloning altogether. Version 0.14.1 diff --git a/benches/eval_expression.rs b/benches/eval_expression.rs index affb25bb..ceaaaa17 100644 --- a/benches/eval_expression.rs +++ b/benches/eval_expression.rs @@ -48,7 +48,7 @@ fn bench_eval_expression_optimized_simple(bench: &mut Bencher) { 2 > 1 && "something" != "nothing" || "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" && - [array, with, spaces].len() <= #{prop:name}.len() && + [array, with, spaces].len <= #{prop:name}.len && modifierTest + 1000 / 2 > (80 * 100 % 2) "#; @@ -65,7 +65,7 @@ fn bench_eval_expression_optimized_full(bench: &mut Bencher) { 2 > 1 && "something" != "nothing" || "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" && - [array, with, spaces].len() <= #{prop:name}.len() && + [array, with, spaces].len <= #{prop:name}.len && modifierTest + 1000 / 2 > (80 * 100 % 2) "#; @@ -82,7 +82,7 @@ fn bench_eval_call_expression(bench: &mut Bencher) { 2 > 1 && "something" != "nothing" || "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" && - [array, with, spaces].len() <= #{prop:name}.len() && + [array, with, spaces].len <= #{prop:name}.len && modifierTest + 1000 / 2 > (80 * 100 % 2) "#; @@ -97,7 +97,7 @@ fn bench_eval_call(bench: &mut Bencher) { 2 > 1 && "something" != "nothing" || "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" && - [array, with, spaces].len() <= #{prop:name}.len() && + [array, with, spaces].len <= #{prop:name}.len && modifierTest + 1000 / 2 > (80 * 100 % 2) "#; diff --git a/benches/parsing.rs b/benches/parsing.rs index 01011877..8e84bd8d 100644 --- a/benches/parsing.rs +++ b/benches/parsing.rs @@ -32,7 +32,7 @@ fn bench_parse_full(bench: &mut Bencher) { 2 > 1 && "something" != "nothing" || "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" && - [array, with, spaces].len() <= #{prop:name}.len() && + [array, with, spaces].len <= #{prop:name}.len && modifierTest + 1000 / 2 > (80 * 100 % 2) "#; @@ -94,7 +94,7 @@ fn bench_parse_primes(bench: &mut Bencher) { } print("Total " + total_primes_found + " primes."); - print("Run time = " + now.elapsed() + " seconds."); + print("Run time = " + now.elapsed + " seconds."); "#; let mut engine = Engine::new(); @@ -109,7 +109,7 @@ fn bench_parse_optimize_simple(bench: &mut Bencher) { 2 > 1 && "something" != "nothing" || "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" && - [array, with, spaces].len() <= #{prop:name}.len() && + [array, with, spaces].len <= #{prop:name}.len && modifierTest + 1000 / 2 > (80 * 100 % 2) "#; @@ -125,7 +125,7 @@ fn bench_parse_optimize_full(bench: &mut Bencher) { 2 > 1 && "something" != "nothing" || "2014-01-20" < "Wed Jul 8 23:07:35 MDT 2015" && - [array, with, spaces].len() <= #{prop:name}.len() && + [array, with, spaces].len <= #{prop:name}.len && modifierTest + 1000 / 2 > (80 * 100 % 2) "#; diff --git a/scripts/fibonacci.rhai b/scripts/fibonacci.rhai index 730dadae..3455c5f6 100644 --- a/scripts/fibonacci.rhai +++ b/scripts/fibonacci.rhai @@ -17,7 +17,7 @@ let now = timestamp(); let result = fib(target); -print("Finished. Run time = " + now.elapsed() + " seconds."); +print("Finished. Run time = " + now.elapsed + " seconds."); print("Fibonacci number #" + target + " = " + result); diff --git a/scripts/mat_mul.rhai b/scripts/mat_mul.rhai index 59011e22..b93a1a5e 100644 --- a/scripts/mat_mul.rhai +++ b/scripts/mat_mul.rhai @@ -24,9 +24,9 @@ fn mat_gen(n) { } fn mat_mul(a, b) { - let m = a.len(); - let n = a[0].len(); - let p = b[0].len(); + let m = a.len; + let n = a[0].len; + let p = b[0].len; let b2 = new_mat(n, p); @@ -38,13 +38,13 @@ fn mat_mul(a, b) { let c = new_mat(m, p); - for i in range(0, c.len()) { + for i in range(0, c.len) { let ci = c[i]; - for j in range(0, ci.len()) { + for j in range(0, ci.len) { let b2j = b2[j]; ci[j] = 0.0; - for z in range(0, a[i].len()) { + for z in range(0, a[i].len) { let x = a[i][z]; let y = b2j[z]; ci[j] += x * y; @@ -66,4 +66,4 @@ for i in range(0, SIZE) { print(c[i]); } -print("Finished. Run time = " + now.elapsed() + " seconds."); +print("Finished. Run time = " + now.elapsed + " seconds."); diff --git a/scripts/primes.rhai b/scripts/primes.rhai index 2b67ec50..7f66976f 100644 --- a/scripts/primes.rhai +++ b/scripts/primes.rhai @@ -26,7 +26,7 @@ for p in range(2, MAX_NUMBER_TO_CHECK) { } print("Total " + total_primes_found + " primes <= " + MAX_NUMBER_TO_CHECK); -print("Run time = " + now.elapsed() + " seconds."); +print("Run time = " + now.elapsed + " seconds."); if total_primes_found != 9_592 { print("The answer is WRONG! Should be 9,592!"); diff --git a/scripts/speed_test.rhai b/scripts/speed_test.rhai index e07bd7ec..edb9bd0e 100644 --- a/scripts/speed_test.rhai +++ b/scripts/speed_test.rhai @@ -10,4 +10,4 @@ while x > 0 { x -= 1; } -print("Finished. Run time = " + now.elapsed() + " seconds."); +print("Finished. Run time = " + now.elapsed + " seconds."); diff --git a/scripts/string.rhai b/scripts/string.rhai index ab30a29a..835b2556 100644 --- a/scripts/string.rhai +++ b/scripts/string.rhai @@ -11,7 +11,7 @@ print("foo" >= "bar"); // string comparison print("the answer is " + 42); // string building using non-string types let s = "hello, world!"; // string variable -print("length=" + s.len()); // should be 13 +print("length=" + s.len); // should be 13 -s[s.len()-1] = '?'; // change the string -print(s); // should print 'hello, world?' +s[s.len-1] = '?'; // change the string +print("Question: " + s); // should print 'Question: hello, world?' diff --git a/src/any.rs b/src/any.rs index 3bcbb049..f685c303 100644 --- a/src/any.rs +++ b/src/any.rs @@ -630,6 +630,14 @@ impl From> for Dynamic { ))) } } +#[cfg(not(feature = "no_index"))] +impl From<&[T]> for Dynamic { + fn from(value: &[T]) -> Self { + Self(Union::Array(Box::new( + value.iter().cloned().map(Dynamic::from).collect(), + ))) + } +} #[cfg(not(feature = "no_object"))] impl From> for Dynamic { fn from(value: HashMap) -> Self { diff --git a/src/api.rs b/src/api.rs index 89e045c1..8a30a480 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1051,8 +1051,7 @@ impl Engine { let mut state = State::new(); let args = args.as_mut(); - let result = - self.call_script_fn(Some(scope), &mut state, &lib, name, fn_def, args, pos, 0)?; + let result = self.call_script_fn(scope, &mut state, &lib, name, fn_def, args, pos, 0)?; let return_type = self.map_type_name(result.type_name()); diff --git a/src/engine.rs b/src/engine.rs index 80b84110..2c761c7e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -432,11 +432,11 @@ fn default_print(s: &str) { } /// Search for a variable within the scope -fn search_scope<'a>( - scope: &'a mut Scope, +fn search_scope<'s, 'a>( + scope: &'s mut Scope, state: &mut State, expr: &'a Expr, -) -> Result<(&'a mut Dynamic, &'a str, ScopeEntryType, Position), Box> { +) -> Result<(&'s mut Dynamic, &'a str, ScopeEntryType, Position), Box> { let ((name, pos), modules, hash_var, index) = match expr { Expr::Variable(x) => x.as_ref(), _ => unreachable!(), @@ -592,7 +592,7 @@ impl Engine { /// **DO NOT** reuse the argument values unless for the first `&mut` argument - all others are silently replaced by `()`! pub(crate) fn call_fn_raw( &self, - scope: Option<&mut Scope>, + scope: &mut Scope, state: &mut State, lib: &FunctionsLib, fn_name: &str, @@ -615,11 +615,55 @@ impl Engine { return Err(Box::new(EvalAltResult::ErrorStackOverflow(pos))); } } + + let mut this_copy: Dynamic = Default::default(); + let mut old_this_ptr: Option<&mut Dynamic> = None; + + /// This function replaces the first argument of a method call with a clone copy. + /// This is to prevent a pure function unintentionally consuming the first argument. + fn normalize_first_arg<'a>( + normalize: bool, + this_copy: &mut Dynamic, + old_this_ptr: &mut Option<&'a mut Dynamic>, + args: &mut FnCallArgs<'a>, + ) { + // Only do it for method calls with arguments. + if !normalize || args.is_empty() { + return; + } + + // Clone the original value. + *this_copy = args[0].clone(); + + // Replace the first reference with a reference to the clone, force-casting the lifetime. + // Keep the original reference. Must remember to restore it later with `restore_first_arg_of_method_call`. + let this_pointer = mem::replace( + args.get_mut(0).unwrap(), + unsafe_mut_cast_to_lifetime(this_copy), + ); + + *old_this_ptr = Some(this_pointer); + } + + /// This function restores the first argument that was replaced by `normalize_first_arg_of_method_call`. + fn restore_first_arg<'a>(old_this_ptr: Option<&'a mut Dynamic>, args: &mut FnCallArgs<'a>) { + if let Some(this_pointer) = old_this_ptr { + mem::replace(args.get_mut(0).unwrap(), this_pointer); + } + } + // First search in script-defined functions (can override built-in) if !native_only { if let Some(fn_def) = lib.get(&hashes.1) { + normalize_first_arg(is_ref, &mut this_copy, &mut old_this_ptr, args); + + // Run scripted function let result = self.call_script_fn(scope, state, lib, fn_name, fn_def, args, pos, level)?; + + // Restore the original reference + restore_first_arg(old_this_ptr, args); + return Ok((result, false)); } } @@ -631,29 +675,18 @@ impl Engine { .or_else(|| self.packages.get_fn(hashes.0)) { // Calling pure function in method-call? - let mut this_copy: Option; - let mut this_pointer: Option<&mut Dynamic> = None; - - if func.is_pure() && is_ref && !args.is_empty() { - // Clone the original value. It'll be consumed because the function - // is pure and doesn't know that the first value is a reference (i.e. `is_ref`) - this_copy = Some(args[0].clone()); - - // Replace the first reference with a reference to the clone, force-casting the lifetime. - // Keep the original reference. Must remember to restore it before existing this function. - this_pointer = Some(mem::replace( - args.get_mut(0).unwrap(), - unsafe_mut_cast_to_lifetime(this_copy.as_mut().unwrap()), - )); - } + normalize_first_arg( + func.is_pure() && is_ref, + &mut this_copy, + &mut old_this_ptr, + args, + ); // Run external function let result = func.get_native_fn()(args); // Restore the original reference - if let Some(this_pointer) = this_pointer { - mem::replace(args.get_mut(0).unwrap(), this_pointer); - } + restore_first_arg(old_this_ptr, args); let result = result.map_err(|err| err.new_position(pos))?; @@ -741,7 +774,7 @@ impl Engine { /// **DO NOT** reuse the argument values unless for the first `&mut` argument - all others are silently replaced by `()`! pub(crate) fn call_script_fn( &self, - scope: Option<&mut Scope>, + scope: &mut Scope, state: &mut State, lib: &FunctionsLib, fn_name: &str, @@ -753,88 +786,42 @@ impl Engine { let orig_scope_level = state.scope_level; state.scope_level += 1; - match scope { - // Extern scope passed in which is not empty - Some(scope) if !scope.is_empty() => { - let scope_len = scope.len(); + let scope_len = scope.len(); - // Put arguments into scope as variables - // Actually consume the arguments instead of cloning them - scope.extend( - fn_def - .params - .iter() - .zip(args.iter_mut().map(|v| mem::take(*v))) - .map(|(name, value)| { - let var_name = unsafe_cast_var_name_to_lifetime(name.as_str(), state); - (var_name, ScopeEntryType::Normal, value) - }), - ); + // Put arguments into scope as variables + // Actually consume the arguments instead of cloning them + scope.extend( + fn_def + .params + .iter() + .zip(args.iter_mut().map(|v| mem::take(*v))) + .map(|(name, value)| { + let var_name = unsafe_cast_var_name_to_lifetime(name.as_str(), state); + (var_name, ScopeEntryType::Normal, value) + }), + ); - // Evaluate the function at one higher level of call depth - let result = self - .eval_stmt(scope, state, lib, &fn_def.body, level + 1) - .or_else(|err| match *err { - // Convert return statement to return value - EvalAltResult::Return(x, _) => Ok(x), - EvalAltResult::ErrorInFunctionCall(name, err, _) => { - Err(Box::new(EvalAltResult::ErrorInFunctionCall( - format!("{} > {}", fn_name, name), - err, - pos, - ))) - } - _ => Err(Box::new(EvalAltResult::ErrorInFunctionCall( - fn_name.to_string(), - err, - pos, - ))), - }); + // Evaluate the function at one higher level of call depth + let result = self + .eval_stmt(scope, state, lib, &fn_def.body, level + 1) + .or_else(|err| match *err { + // Convert return statement to return value + EvalAltResult::Return(x, _) => Ok(x), + EvalAltResult::ErrorInFunctionCall(name, err, _) => Err(Box::new( + EvalAltResult::ErrorInFunctionCall(format!("{} > {}", fn_name, name), err, pos), + )), + _ => Err(Box::new(EvalAltResult::ErrorInFunctionCall( + fn_name.to_string(), + err, + pos, + ))), + }); - // Remove all local variables - scope.rewind(scope_len); - state.scope_level = orig_scope_level; + // Remove all local variables + scope.rewind(scope_len); + state.scope_level = orig_scope_level; - return result; - } - // No new scope - create internal scope - _ => { - let mut scope = Scope::new(); - - // Put arguments into scope as variables - // Actually consume the arguments instead of cloning them - scope.extend( - fn_def - .params - .iter() - .zip(args.iter_mut().map(|v| mem::take(*v))) - .map(|(name, value)| (name, ScopeEntryType::Normal, value)), - ); - - // Evaluate the function at one higher level of call depth - let result = self - .eval_stmt(&mut scope, state, lib, &fn_def.body, level + 1) - .or_else(|err| match *err { - // Convert return statement to return value - EvalAltResult::Return(x, _) => Ok(x), - EvalAltResult::ErrorInFunctionCall(name, err, _) => { - Err(Box::new(EvalAltResult::ErrorInFunctionCall( - format!("{} > {}", fn_name, name), - err, - pos, - ))) - } - _ => Err(Box::new(EvalAltResult::ErrorInFunctionCall( - fn_name.to_string(), - err, - pos, - ))), - }); - - state.scope_level = orig_scope_level; - return result; - } - } + result } // Has a system function an override? @@ -892,9 +879,12 @@ impl Engine { } // Normal function call - _ => self.call_fn_raw( - None, state, lib, fn_name, hashes, args, is_ref, def_val, pos, level, - ), + _ => { + let mut scope = Scope::new(); + self.call_fn_raw( + &mut scope, state, lib, fn_name, hashes, args, is_ref, def_val, pos, level, + ) + } } } @@ -1219,14 +1209,16 @@ impl Engine { Expr::FnCall(_) => unreachable!(), Expr::Property(_) => idx_values.push(()), // Store a placeholder - no need to copy the property name Expr::Index(x) | Expr::Dot(x) => { + let (lhs, rhs, _) = x.as_ref(); + // Evaluate in left-to-right order - let lhs_val = match x.0 { + let lhs_val = match lhs { Expr::Property(_) => Default::default(), // Store a placeholder in case of a property - _ => self.eval_expr(scope, state, lib, &x.0, level)?, + _ => self.eval_expr(scope, state, lib, lhs, level)?, }; // Push in reverse order - self.eval_indexed_chain(scope, state, lib, &x.1, idx_values, size, level)?; + self.eval_indexed_chain(scope, state, lib, rhs, idx_values, size, level)?; idx_values.push(lhs_val); } @@ -1347,6 +1339,7 @@ impl Engine { Dynamic(Union::Array(mut rhs_value)) => { let op = "=="; let def_value = false.into(); + let mut scope = Scope::new(); // Call the `==` operator to compare each value for value in rhs_value.iter_mut() { @@ -1361,7 +1354,7 @@ impl Engine { ); let (r, _) = self.call_fn_raw( - None, state, lib, op, hashes, args, false, def_value, pos, level, + &mut scope, state, lib, op, hashes, args, false, def_value, pos, level, )?; if r.as_bool().unwrap_or(false) { return Ok(true.into()); @@ -1399,6 +1392,8 @@ impl Engine { self.inc_operations(state, expr.position())?; match expr { + Expr::Expr(x) => self.eval_expr(scope, state, lib, x.as_ref(), level), + Expr::IntegerConstant(x) => Ok(x.0.into()), #[cfg(not(feature = "no_float"))] Expr::FloatConstant(x) => Ok(x.0.into()), @@ -1550,14 +1545,8 @@ impl Engine { let ((name, native, pos), _, hash, args_expr, def_val) = x.as_ref(); let def_val = def_val.as_ref(); - let mut arg_values = args_expr - .iter() - .map(|expr| self.eval_expr(scope, state, lib, expr, level)) - .collect::, _>>()?; - - let mut args: StaticVec<_> = arg_values.iter_mut().collect(); - - if name == KEYWORD_EVAL && args.len() == 1 && args.get(0).is::() { + // Handle eval + if name == KEYWORD_EVAL && args_expr.len() == 1 { let hash_fn = calc_fn_hash(empty(), name, 1, once(TypeId::of::())); @@ -1567,7 +1556,8 @@ impl Engine { let pos = args_expr.get(0).position(); // Evaluate the text string as a script - let result = self.eval_script_expr(scope, state, lib, args.pop(), pos); + let script = self.eval_expr(scope, state, lib, args_expr.get(0), level)?; + let result = self.eval_script_expr(scope, state, lib, &script, pos); if scope.len() != prev_len { // IMPORTANT! If the eval defines new variables in the current scope, @@ -1580,9 +1570,52 @@ impl Engine { } // Normal function call - except for eval (handled above) + let mut arg_values: StaticVec; + let mut args: StaticVec<_>; + let mut is_ref = false; + + if args_expr.is_empty() { + // No arguments + args = Default::default(); + } else { + // See if the first argument is a variable, if so, convert to method-call style + // in order to leverage potential &mut first argument and avoid cloning the value + match args_expr.get(0) { + // func(x, ...) -> x.func(...) + lhs @ Expr::Variable(_) => { + arg_values = args_expr + .iter() + .skip(1) + .map(|expr| self.eval_expr(scope, state, lib, expr, level)) + .collect::>()?; + + let (target, _, typ, pos) = search_scope(scope, state, lhs)?; + self.inc_operations(state, pos)?; + + match typ { + ScopeEntryType::Module => unreachable!(), + ScopeEntryType::Constant | ScopeEntryType::Normal => (), + } + + args = once(target).chain(arg_values.iter_mut()).collect(); + + is_ref = true; + } + // func(..., ...) + _ => { + arg_values = args_expr + .iter() + .map(|expr| self.eval_expr(scope, state, lib, expr, level)) + .collect::>()?; + + args = arg_values.iter_mut().collect(); + } + } + } + let args = args.as_mut(); self.exec_fn_call( - state, lib, name, *native, *hash, args, false, def_val, *pos, level, + state, lib, name, *native, *hash, args, is_ref, def_val, *pos, level, ) .map(|(v, _)| v) } @@ -1639,9 +1672,8 @@ impl Engine { Ok(x) if x.is_script() => { let args = args.as_mut(); let fn_def = x.get_fn_def(); - let result = - self.call_script_fn(None, state, lib, name, fn_def, args, *pos, level)?; - Ok(result) + let mut scope = Scope::new(); + self.call_script_fn(&mut scope, state, lib, name, fn_def, args, *pos, level) } Ok(x) => x.get_native_fn()(args.as_mut()).map_err(|err| err.new_position(*pos)), Err(err) @@ -1701,9 +1733,9 @@ impl Engine { } /// Evaluate a statement - pub(crate) fn eval_stmt<'s>( + pub(crate) fn eval_stmt( &self, - scope: &mut Scope<'s>, + scope: &mut Scope, state: &mut State, lib: &FunctionsLib, stmt: &Stmt, @@ -2012,8 +2044,8 @@ fn run_builtin_binary_op( } if args_type == TypeId::of::() { - let x = x.downcast_ref::().unwrap().clone(); - let y = y.downcast_ref::().unwrap().clone(); + let x = *x.downcast_ref::().unwrap(); + let y = *y.downcast_ref::().unwrap(); #[cfg(not(feature = "unchecked"))] match op { @@ -2054,8 +2086,8 @@ fn run_builtin_binary_op( _ => (), } } else if args_type == TypeId::of::() { - let x = x.downcast_ref::().unwrap().clone(); - let y = y.downcast_ref::().unwrap().clone(); + let x = *x.downcast_ref::().unwrap(); + let y = *y.downcast_ref::().unwrap(); match op { "&" => return Ok(Some((x && y).into())), @@ -2079,8 +2111,8 @@ fn run_builtin_binary_op( _ => (), } } else if args_type == TypeId::of::() { - let x = x.downcast_ref::().unwrap().clone(); - let y = y.downcast_ref::().unwrap().clone(); + let x = *x.downcast_ref::().unwrap(); + let y = *y.downcast_ref::().unwrap(); match op { "==" => return Ok(Some((x == y).into())), @@ -2102,8 +2134,8 @@ fn run_builtin_binary_op( #[cfg(not(feature = "no_float"))] { if args_type == TypeId::of::() { - let x = x.downcast_ref::().unwrap().clone(); - let y = y.downcast_ref::().unwrap().clone(); + let x = *x.downcast_ref::().unwrap(); + let y = *y.downcast_ref::().unwrap(); match op { "+" => return Ok(Some((x + y).into())), @@ -2142,7 +2174,7 @@ fn run_builtin_op_assignment( if args_type == TypeId::of::() { let x = x.downcast_mut::().unwrap(); - let y = y.downcast_ref::().unwrap().clone(); + let y = *y.downcast_ref::().unwrap(); #[cfg(not(feature = "unchecked"))] match op { @@ -2178,7 +2210,7 @@ fn run_builtin_op_assignment( } } else if args_type == TypeId::of::() { let x = x.downcast_mut::().unwrap(); - let y = y.downcast_ref::().unwrap().clone(); + let y = *y.downcast_ref::().unwrap(); match op { "&=" => return Ok(Some(*x = *x && y)), @@ -2199,7 +2231,7 @@ fn run_builtin_op_assignment( { if args_type == TypeId::of::() { let x = x.downcast_mut::().unwrap(); - let y = y.downcast_ref::().unwrap().clone(); + let y = *y.downcast_ref::().unwrap(); match op { "+=" => return Ok(Some(*x += y)), diff --git a/src/module.rs b/src/module.rs index 457f1b1c..498739b7 100644 --- a/src/module.rs +++ b/src/module.rs @@ -2,7 +2,7 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; -use crate::engine::{Engine, FunctionsLib}; +use crate::engine::{make_getter, make_setter, Engine, FunctionsLib, FUNC_INDEXER}; use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn}; use crate::parser::{ FnAccess, @@ -378,9 +378,9 @@ impl Module { ) } - /// Set a Rust function taking two parameters into the module, returning a hash key. + /// Set a Rust getter function taking one mutable parameter, returning a hash key. /// - /// If there is a similar existing Rust function, it is replaced. + /// If there is a similar existing Rust getter function, it is replaced. /// /// # Examples /// @@ -388,7 +388,30 @@ impl Module { /// use rhai::Module; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_2("calc", |x: i64, y: String| { + /// let hash = module.set_getter_fn("value", |x: &mut i64| { Ok(*x) }); + /// assert!(module.get_fn(hash).is_some()); + /// ``` + #[cfg(not(feature = "no_object"))] + pub fn set_getter_fn( + &mut self, + name: impl Into, + #[cfg(not(feature = "sync"))] func: impl Fn(&mut A) -> FuncReturn + 'static, + #[cfg(feature = "sync")] func: impl Fn(&mut A) -> FuncReturn + Send + Sync + 'static, + ) -> u64 { + self.set_fn_1_mut(make_getter(&name.into()), func) + } + + /// Set a Rust function taking two parameters into the module, returning a hash key. + /// + /// If there is a similar existing Rust function, it is replaced. + /// + /// # Examples + /// + /// ``` + /// use rhai::{Module, ImmutableString}; + /// + /// let mut module = Module::new(); + /// let hash = module.set_fn_2("calc", |x: i64, y: ImmutableString| { /// Ok(x + y.len() as i64) /// }); /// assert!(module.get_fn(hash).is_some()); @@ -417,13 +440,15 @@ impl Module { /// Set a Rust function taking two parameters (the first one mutable) into the module, /// returning a hash key. /// + /// If there is a similar existing Rust function, it is replaced. + /// /// # Examples /// /// ``` - /// use rhai::Module; + /// use rhai::{Module, ImmutableString}; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_2_mut("calc", |x: &mut i64, y: String| { + /// let hash = module.set_fn_2_mut("calc", |x: &mut i64, y: ImmutableString| { /// *x += y.len() as i64; Ok(*x) /// }); /// assert!(module.get_fn(hash).is_some()); @@ -449,6 +474,59 @@ impl Module { ) } + /// Set a Rust setter function taking two parameters (the first one mutable) into the module, + /// returning a hash key. + /// + /// If there is a similar existing setter Rust function, it is replaced. + /// + /// # Examples + /// + /// ``` + /// use rhai::{Module, ImmutableString}; + /// + /// let mut module = Module::new(); + /// let hash = module.set_setter_fn("value", |x: &mut i64, y: ImmutableString| { + /// *x = y.len() as i64; + /// Ok(()) + /// }); + /// assert!(module.get_fn(hash).is_some()); + /// ``` + #[cfg(not(feature = "no_object"))] + pub fn set_setter_fn( + &mut self, + name: impl Into, + #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B) -> FuncReturn<()> + 'static, + #[cfg(feature = "sync")] func: impl Fn(&mut A, B) -> FuncReturn<()> + Send + Sync + 'static, + ) -> u64 { + self.set_fn_2_mut(make_setter(&name.into()), func) + } + + /// Set a Rust indexer function taking two parameters (the first one mutable) into the module, + /// returning a hash key. + /// + /// If there is a similar existing setter Rust function, it is replaced. + /// + /// # Examples + /// + /// ``` + /// use rhai::{Module, ImmutableString}; + /// + /// let mut module = Module::new(); + /// let hash = module.set_indexer_fn(|x: &mut i64, y: ImmutableString| { + /// Ok(*x + y.len() as i64) + /// }); + /// assert!(module.get_fn(hash).is_some()); + /// ``` + #[cfg(not(feature = "no_object"))] + #[cfg(not(feature = "no_index"))] + pub fn set_indexer_fn( + &mut self, + #[cfg(not(feature = "sync"))] func: impl Fn(&mut A, B) -> FuncReturn + 'static, + #[cfg(feature = "sync")] func: impl Fn(&mut A, B) -> FuncReturn + Send + Sync + 'static, + ) -> u64 { + self.set_fn_2_mut(FUNC_INDEXER, func) + } + /// Set a Rust function taking three parameters into the module, returning a hash key. /// /// If there is a similar existing Rust function, it is replaced. @@ -456,10 +534,10 @@ impl Module { /// # Examples /// /// ``` - /// use rhai::Module; + /// use rhai::{Module, ImmutableString}; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_3("calc", |x: i64, y: String, z: i64| { + /// let hash = module.set_fn_3("calc", |x: i64, y: ImmutableString, z: i64| { /// Ok(x + y.len() as i64 + z) /// }); /// assert!(module.get_fn(hash).is_some()); @@ -499,10 +577,10 @@ impl Module { /// # Examples /// /// ``` - /// use rhai::Module; + /// use rhai::{Module, ImmutableString}; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_3_mut("calc", |x: &mut i64, y: String, z: i64| { + /// let hash = module.set_fn_3_mut("calc", |x: &mut i64, y: ImmutableString, z: i64| { /// *x += y.len() as i64 + z; Ok(*x) /// }); /// assert!(module.get_fn(hash).is_some()); @@ -541,10 +619,10 @@ impl Module { /// # Examples /// /// ``` - /// use rhai::Module; + /// use rhai::{Module, ImmutableString}; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_3("calc", |x: i64, y: String, z: i64, _w: ()| { + /// let hash = module.set_fn_4("calc", |x: i64, y: ImmutableString, z: i64, _w: ()| { /// Ok(x + y.len() as i64 + z) /// }); /// assert!(module.get_fn(hash).is_some()); @@ -583,7 +661,7 @@ impl Module { ) } - /// Set a Rust function taking three parameters (the first one mutable) into the module, + /// Set a Rust function taking four parameters (the first one mutable) into the module, /// returning a hash key. /// /// If there is a similar existing Rust function, it is replaced. @@ -591,10 +669,10 @@ impl Module { /// # Examples /// /// ``` - /// use rhai::Module; + /// use rhai::{Module, ImmutableString}; /// /// let mut module = Module::new(); - /// let hash = module.set_fn_3_mut("calc", |x: &mut i64, y: String, z: i64, _w: ()| { + /// let hash = module.set_fn_4_mut("calc", |x: &mut i64, y: ImmutableString, z: i64, _w: ()| { /// *x += y.len() as i64 + z; Ok(*x) /// }); /// assert!(module.get_fn(hash).is_some()); @@ -1044,6 +1122,9 @@ mod stat { /// Module resolution service that serves modules added into it. /// + /// `StaticModuleResolver` is a smart pointer to a `HashMap`. + /// It can simply be treated as `&HashMap`. + /// /// # Examples /// /// ``` diff --git a/src/optimize.rs b/src/optimize.rs index dedd1135..f8b0d1bb 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -122,7 +122,7 @@ fn call_fn_with_constant_arguments( state .engine .call_fn_raw( - None, + &mut Scope::new(), &mut Default::default(), state.lib, fn_name, @@ -138,7 +138,7 @@ fn call_fn_with_constant_arguments( } /// Optimize a statement. -fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) -> Stmt { +fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { match stmt { // if expr { Noop } Stmt::IfThenElse(x) if matches!(x.1, Stmt::Noop(_)) => { @@ -359,11 +359,13 @@ fn optimize_stmt<'a>(stmt: Stmt, state: &mut State<'a>, preserve_result: bool) - } /// Optimize an expression. -fn optimize_expr<'a>(expr: Expr, state: &mut State<'a>) -> Expr { +fn optimize_expr(expr: Expr, state: &mut State) -> Expr { // These keywords are handled specially const DONT_EVAL_KEYWORDS: [&str; 3] = [KEYWORD_PRINT, KEYWORD_DEBUG, KEYWORD_EVAL]; match expr { + // expr - do not promote because there is a reason it is wrapped in an `Expr::Expr` + Expr::Expr(x) => Expr::Expr(Box::new(optimize_expr(*x, state))), // ( stmt ) Expr::Stmt(x) => match optimize_stmt(x.0, state, true) { // ( Noop ) -> () diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 5b96ffe2..19206e36 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -105,6 +105,10 @@ def_package!(crate:BasicArrayPackage:"Basic array utilities.", lib, { }, ); lib.set_fn_1_mut("len", |list: &mut Array| Ok(list.len() as INT)); + + #[cfg(not(feature = "no_object"))] + lib.set_getter_fn("len", |list: &mut Array| Ok(list.len() as INT)); + lib.set_fn_1_mut("clear", |list: &mut Array| { list.clear(); Ok(()) diff --git a/src/packages/math_basic.rs b/src/packages/math_basic.rs index 9c7313ed..eea49727 100644 --- a/src/packages/math_basic.rs +++ b/src/packages/math_basic.rs @@ -43,6 +43,18 @@ def_package!(crate:BasicMathPackage:"Basic mathematic functions.", lib, { lib.set_fn_1("is_finite", |x: FLOAT| Ok(x.is_finite())); lib.set_fn_1("is_infinite", |x: FLOAT| Ok(x.is_infinite())); + #[cfg(not(feature = "no_object"))] + { + lib.set_getter_fn("floor", |x: &mut FLOAT| Ok(x.floor())); + lib.set_getter_fn("ceiling", |x: &mut FLOAT| Ok(x.ceil())); + lib.set_getter_fn("round", |x: &mut FLOAT| Ok(x.ceil())); + lib.set_getter_fn("int", |x: &mut FLOAT| Ok(x.trunc())); + lib.set_getter_fn("fraction", |x: &mut FLOAT| Ok(x.fract())); + lib.set_getter_fn("is_nan", |x: &mut FLOAT| Ok(x.is_nan())); + lib.set_getter_fn("is_finite", |x: &mut FLOAT| Ok(x.is_finite())); + lib.set_getter_fn("is_infinite", |x: &mut FLOAT| Ok(x.is_infinite())); + } + // Register conversion functions lib.set_fn_1("to_float", |x: INT| Ok(x as FLOAT)); lib.set_fn_1("to_float", |x: f32| Ok(x as FLOAT)); diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index c3c22c31..e86f5fb1 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -101,22 +101,26 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str #[cfg(not(feature = "no_index"))] { - lib.set_fn_2("+", |x: ImmutableString, y: Array| Ok(format!("{}{:?}", x, y))); + lib.set_fn_2_mut("+", |x: &mut ImmutableString, y: Array| Ok(format!("{}{:?}", x, y))); lib.set_fn_2_mut("+", |x: &mut Array, y: ImmutableString| Ok(format!("{:?}{}", x, y))); } - lib.set_fn_1("len", |s: ImmutableString| Ok(s.chars().count() as INT)); - lib.set_fn_2( + lib.set_fn_1_mut("len", |s: &mut ImmutableString| Ok(s.chars().count() as INT)); + + #[cfg(not(feature = "no_object"))] + lib.set_getter_fn("len", |s: &mut ImmutableString| Ok(s.chars().count() as INT)); + + lib.set_fn_2_mut( "contains", - |s: ImmutableString, ch: char| Ok(s.contains(ch)), + |s: &mut ImmutableString, ch: char| Ok(s.contains(ch)), ); - lib.set_fn_2( + lib.set_fn_2_mut( "contains", - |s: ImmutableString, find: ImmutableString| Ok(s.contains(find.as_str())), + |s: &mut ImmutableString, find: ImmutableString| Ok(s.contains(find.as_str())), ); - lib.set_fn_3( + lib.set_fn_3_mut( "index_of", - |s: ImmutableString, ch: char, start: INT| { + |s: &mut ImmutableString, ch: char, start: INT| { let start = if start < 0 { 0 } else if (start as usize) >= s.chars().count() { @@ -131,17 +135,17 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str .unwrap_or(-1 as INT)) }, ); - lib.set_fn_2( + lib.set_fn_2_mut( "index_of", - |s: ImmutableString, ch: char| { + |s: &mut ImmutableString, ch: char| { Ok(s.find(ch) .map(|index| s[0..index].chars().count() as INT) .unwrap_or(-1 as INT)) }, ); - lib.set_fn_3( + lib.set_fn_3_mut( "index_of", - |s: ImmutableString, find: ImmutableString, start: INT| { + |s: &mut ImmutableString, find: ImmutableString, start: INT| { let start = if start < 0 { 0 } else if (start as usize) >= s.chars().count() { @@ -156,9 +160,9 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str .unwrap_or(-1 as INT)) }, ); - lib.set_fn_2( + lib.set_fn_2_mut( "index_of", - |s: ImmutableString, find: ImmutableString| { + |s: &mut ImmutableString, find: ImmutableString| { Ok(s.find(find.as_str()) .map(|index| s[0..index].chars().count() as INT) .unwrap_or(-1 as INT)) diff --git a/src/packages/time_basic.rs b/src/packages/time_basic.rs index 7d80344c..b8e13604 100644 --- a/src/packages/time_basic.rs +++ b/src/packages/time_basic.rs @@ -10,6 +10,9 @@ use crate::token::Position; #[cfg(not(feature = "no_std"))] use crate::stdlib::time::Instant; +#[cfg(not(feature = "no_float"))] +use crate::parser::FLOAT; + #[cfg(not(feature = "no_std"))] def_package!(crate:BasicTimePackage:"Basic timing utilities.", lib, { // Register date/time functions @@ -70,27 +73,29 @@ def_package!(crate:BasicTimePackage:"Basic timing utilities.", lib, { lib.set_fn_2("==", eq::); lib.set_fn_2("!=", ne::); - lib.set_fn_1( - "elapsed", - |timestamp: Instant| { - #[cfg(not(feature = "no_float"))] - return Ok(timestamp.elapsed().as_secs_f64()); + #[cfg(not(feature = "no_float"))] + fn elapsed (timestamp: &mut Instant) -> Result> { + Ok(timestamp.elapsed().as_secs_f64()) + } - #[cfg(feature = "no_float")] - { - let seconds = timestamp.elapsed().as_secs(); + #[cfg(feature = "no_float")] + fn elapsed (timestamp: &mut Instant) -> Result> { + let seconds = timestamp.elapsed().as_secs(); - #[cfg(not(feature = "unchecked"))] - { - if seconds > (MAX_INT as u64) { - return Err(Box::new(EvalAltResult::ErrorArithmetic( - format!("Integer overflow for timestamp.elapsed(): {}", seconds), - Position::none(), - ))); - } - } - return Ok(seconds as INT); + #[cfg(not(feature = "unchecked"))] + { + if seconds > (MAX_INT as u64) { + return Err(Box::new(EvalAltResult::ErrorArithmetic( + format!("Integer overflow for timestamp.elapsed: {}", seconds), + Position::none(), + ))); } - }, - ); + } + Ok(seconds as INT) + } + + lib.set_fn_1_mut("elapsed", elapsed); + + #[cfg(not(feature = "no_object"))] + lib.set_getter_fn("elapsed", elapsed); }); diff --git a/src/parser.rs b/src/parser.rs index 24e6f76e..688dc7b1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -23,6 +23,7 @@ use crate::stdlib::{ collections::HashMap, format, iter::{empty, Peekable}, + mem, num::NonZeroUsize, ops::{Add, Deref, DerefMut}, string::{String, ToString}, @@ -391,6 +392,8 @@ pub enum Expr { Property(Box<((String, String, String), Position)>), /// { stmt } Stmt(Box<(Stmt, Position)>), + /// Wrapped expression - should not be optimized away. + Expr(Box), /// func(expr, ... ) - ((function name, native_only, position), optional modules, hash, arguments, optional default value) /// Use `Cow<'static, str>` because a lot of operators (e.g. `==`, `>=`) are implemented as function calls /// and the function names are predictable, so no need to allocate a new `String`. @@ -441,6 +444,8 @@ impl Expr { /// Panics when the expression is not constant. pub fn get_constant_value(&self) -> Dynamic { match self { + Self::Expr(x) => x.get_constant_value(), + Self::IntegerConstant(x) => x.0.into(), #[cfg(not(feature = "no_float"))] Self::FloatConstant(x) => x.0.into(), @@ -475,6 +480,8 @@ impl Expr { /// Panics when the expression is not constant. pub fn get_constant_str(&self) -> String { match self { + Self::Expr(x) => x.get_constant_str(), + #[cfg(not(feature = "no_float"))] Self::FloatConstant(x) => x.0.to_string(), @@ -494,6 +501,8 @@ impl Expr { /// Get the `Position` of the expression. pub fn position(&self) -> Position { match self { + Self::Expr(x) => x.position(), + #[cfg(not(feature = "no_float"))] Self::FloatConstant(x) => x.1, @@ -519,6 +528,11 @@ impl Expr { /// Override the `Position` of the expression. pub(crate) fn set_position(mut self, new_pos: Position) -> Self { match &mut self { + Self::Expr(ref mut x) => { + let expr = mem::take(x); + *x = Box::new(expr.set_position(new_pos)); + } + #[cfg(not(feature = "no_float"))] Self::FloatConstant(x) => x.1 = new_pos, @@ -550,6 +564,8 @@ impl Expr { /// A pure expression has no side effects. pub fn is_pure(&self) -> bool { match self { + Self::Expr(x) => x.is_pure(), + Self::Array(x) => x.0.iter().all(Self::is_pure), Self::Index(x) | Self::And(x) | Self::Or(x) | Self::In(x) => { @@ -568,6 +584,8 @@ impl Expr { /// Is the expression a constant? pub fn is_constant(&self) -> bool { match self { + Self::Expr(x) => x.is_constant(), + #[cfg(not(feature = "no_float"))] Self::FloatConstant(_) => true, @@ -598,6 +616,8 @@ impl Expr { /// Is a particular token allowed as a postfix operator to this expression? pub fn is_valid_postfix(&self, token: &Token) -> bool { match self { + Self::Expr(x) => x.is_valid_postfix(token), + #[cfg(not(feature = "no_float"))] Self::FloatConstant(_) => false, @@ -996,7 +1016,7 @@ fn parse_index_chain<'a>( (Token::LeftBracket, _) => { let idx_pos = eat_token(input, Token::LeftBracket); // Recursively parse the indexing chain, right-binding each - let idx = parse_index_chain( + let idx_expr = parse_index_chain( input, state, idx_expr, @@ -1005,10 +1025,20 @@ fn parse_index_chain<'a>( allow_stmt_expr, )?; // Indexing binds to right - Ok(Expr::Index(Box::new((lhs, idx, pos)))) + Ok(Expr::Index(Box::new((lhs, idx_expr, pos)))) } // Otherwise terminate the indexing chain - _ => Ok(Expr::Index(Box::new((lhs, idx_expr, pos)))), + _ => { + match idx_expr { + // Terminate with an `Expr::Expr` wrapper to prevent the last index expression + // inside brackets to be mis-parsed as another level of indexing, or a + // dot expression/function call to be mis-parsed as following the indexing chain. + Expr::Index(_) | Expr::Dot(_) | Expr::FnCall(_) => Ok(Expr::Index( + Box::new((lhs, Expr::Expr(Box::new(idx_expr)), pos)), + )), + _ => Ok(Expr::Index(Box::new((lhs, idx_expr, pos)))), + } + } } } (Token::LexError(err), pos) => return Err(PERR::BadInput(err.to_string()).into_err(*pos)), @@ -1732,37 +1762,34 @@ fn parse_binary_op<'a>( let cmp_def = Some(false.into()); let op = op_token.syntax(); let hash = calc_fn_hash(empty(), &op, 2, empty()); + let op = (op, true, pos); let mut args = StaticVec::new(); args.push(root); args.push(rhs); root = match op_token { - Token::Plus => Expr::FnCall(Box::new(((op, true, pos), None, hash, args, None))), - Token::Minus => Expr::FnCall(Box::new(((op, true, pos), None, hash, args, None))), - Token::Multiply => Expr::FnCall(Box::new(((op, true, pos), None, hash, args, None))), - Token::Divide => Expr::FnCall(Box::new(((op, true, pos), None, hash, args, None))), + Token::Plus + | Token::Minus + | Token::Multiply + | Token::Divide + | Token::LeftShift + | Token::RightShift + | Token::Modulo + | Token::PowerOf + | Token::Ampersand + | Token::Pipe + | Token::XOr => Expr::FnCall(Box::new((op, None, hash, args, None))), - Token::LeftShift => Expr::FnCall(Box::new(((op, true, pos), None, hash, args, None))), - Token::RightShift => Expr::FnCall(Box::new(((op, true, pos), None, hash, args, None))), - Token::Modulo => Expr::FnCall(Box::new(((op, true, pos), None, hash, args, None))), - Token::PowerOf => Expr::FnCall(Box::new(((op, true, pos), None, hash, args, None))), + // '!=' defaults to true when passed invalid operands + Token::NotEqualsTo => Expr::FnCall(Box::new((op, None, hash, args, Some(true.into())))), // Comparison operators default to false when passed invalid operands - Token::EqualsTo => Expr::FnCall(Box::new(((op, true, pos), None, hash, args, cmp_def))), - Token::NotEqualsTo => { - Expr::FnCall(Box::new(((op, true, pos), None, hash, args, cmp_def))) - } - Token::LessThan => Expr::FnCall(Box::new(((op, true, pos), None, hash, args, cmp_def))), - Token::LessThanEqualsTo => { - Expr::FnCall(Box::new(((op, true, pos), None, hash, args, cmp_def))) - } - Token::GreaterThan => { - Expr::FnCall(Box::new(((op, true, pos), None, hash, args, cmp_def))) - } - Token::GreaterThanEqualsTo => { - Expr::FnCall(Box::new(((op, true, pos), None, hash, args, cmp_def))) - } + Token::EqualsTo + | Token::LessThan + | Token::LessThanEqualsTo + | Token::GreaterThan + | Token::GreaterThanEqualsTo => Expr::FnCall(Box::new((op, None, hash, args, cmp_def))), Token::Or => { let rhs = args.pop(); @@ -1774,10 +1801,6 @@ fn parse_binary_op<'a>( let current_lhs = args.pop(); Expr::And(Box::new((current_lhs, rhs, pos))) } - Token::Ampersand => Expr::FnCall(Box::new(((op, true, pos), None, hash, args, None))), - Token::Pipe => Expr::FnCall(Box::new(((op, true, pos), None, hash, args, None))), - Token::XOr => Expr::FnCall(Box::new(((op, true, pos), None, hash, args, None))), - Token::In => { let rhs = args.pop(); let current_lhs = args.pop(); diff --git a/tests/arrays.rs b/tests/arrays.rs index ea710eba..741156e9 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -26,7 +26,7 @@ fn test_arrays() -> Result<(), Box> { let y = [4, 5]; x.append(y); - x.len() + r + x.len + r " )?, 14 diff --git a/tests/for.rs b/tests/for.rs index 2512ec72..cddff6ed 100644 --- a/tests/for.rs +++ b/tests/for.rs @@ -48,7 +48,7 @@ fn test_for_object() -> Result<(), Box> { sum += value; } - keys.len() + sum + keys.len + sum "#; assert_eq!(engine.eval::(script)?, 9); diff --git a/tests/functions.rs b/tests/functions.rs index 26d9b387..ae12e052 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -7,12 +7,23 @@ fn test_functions() -> Result<(), Box> { assert_eq!(engine.eval::("fn add(x, n) { x + n } add(40, 2)")?, 42); + assert_eq!( + engine.eval::("fn add(x, n) { x + n } let a = 40; add(a, 2); a")?, + 40 + ); + #[cfg(not(feature = "no_object"))] assert_eq!( engine.eval::("fn add(x, n) { x + 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 + ); + assert_eq!(engine.eval::("fn mul2(x) { x * 2 } mul2(21)")?, 42); #[cfg(not(feature = "no_object"))] @@ -21,5 +32,11 @@ fn test_functions() -> Result<(), Box> { 42 ); + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::("fn mul2(x) { x *= 2; } let x = 21; x.mul2(); x")?, + 21 + ); + Ok(()) } diff --git a/tests/method_call.rs b/tests/method_call.rs index 850cf247..02c7a950 100644 --- a/tests/method_call.rs +++ b/tests/method_call.rs @@ -33,7 +33,7 @@ fn test_method_call() -> Result<(), Box> { assert_eq!( engine.eval::("let x = new_ts(); update(x, 1000); x")?, - TestStruct { x: 1 } + TestStruct { x: 1001 } ); Ok(()) diff --git a/tests/string.rs b/tests/string.rs index 981d19eb..b868f050 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -20,21 +20,20 @@ fn test_string() -> Result<(), Box> { assert!(engine.eval::(r#"let y = "hello, world!"; 'w' in y"#)?); assert!(!engine.eval::(r#"let y = "hello, world!"; "hey" in y"#)?); - #[cfg(not(feature = "no_stdlib"))] assert_eq!(engine.eval::(r#""foo" + 123"#)?, "foo123"); - #[cfg(not(feature = "no_stdlib"))] #[cfg(not(feature = "no_object"))] assert_eq!(engine.eval::("(42).to_string()")?, "42"); + #[cfg(not(feature = "no_object"))] + assert_eq!(engine.eval::(r#"let y = "hello"; y[y.len-1]"#)?, 'o'); + #[cfg(not(feature = "no_float"))] - #[cfg(not(feature = "no_stdlib"))] assert_eq!(engine.eval::(r#""foo" + 123.4556"#)?, "foo123.4556"); Ok(()) } -#[cfg(not(feature = "no_stdlib"))] #[cfg(not(feature = "no_object"))] #[test] fn test_string_substring() -> Result<(), Box> { diff --git a/tests/time.rs b/tests/time.rs index aa5239b1..2590e174 100644 --- a/tests/time.rs +++ b/tests/time.rs @@ -1,4 +1,3 @@ -#![cfg(not(feature = "no_stdlib"))] #![cfg(not(feature = "no_std"))] use rhai::{Engine, EvalAltResult, INT};