From a97ffc536c0a07b9c9a3f834dc08a02495079f95 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 14 Jul 2020 08:42:02 +0800 Subject: [PATCH 1/6] Bump version. --- Cargo.toml | 8 +------- RELEASES.md | 6 ++++++ doc/src/context.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6d48bc8d..fe72ee21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rhai" -version = "0.17.0" +version = "0.18.0" edition = "2018" authors = ["Jonathan Turner", "Lukáš Hozda", "Stephen Chung"] description = "Embedded scripting for Rust" @@ -73,9 +73,3 @@ optional = true [target.'cfg(target_arch = "wasm32")'.dependencies] instant= { version = "0.1.4", features = ["wasm-bindgen"] } # WASM implementation of std::time::Instant - -[package.metadata.docs.rs] -features = ["serde"] - -[package.metadata.playground] -features = ["serde"] diff --git a/RELEASES.md b/RELEASES.md index 9cde45dd..938b4d2c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,6 +1,11 @@ Rhai Release Notes ================== +Version 0.18.0 +============== + + + Version 0.17.0 ============== @@ -31,6 +36,7 @@ New features This is particularly useful when converting a Rust `struct` to a `Dynamic` _object map_ and back. * `Engine::disable_symbol` to surgically disable keywords and/or operators. * `Engine::register_custom_operator` to define a custom operator. +* `Engine::register_custom_syntax` to define a custom syntax. * New low-level API `Engine::register_raw_fn` and `Engine::register_raw_fn_XXX`. * New low-level API `Module::set_raw_fn` mirroring `Engine::register_raw_fn`. * `AST::clone_functions_only`, `AST::clone_functions_only_filtered` and `AST::clone_statements_only` to clone only part of an `AST`. diff --git a/doc/src/context.json b/doc/src/context.json index 506b72e1..025c159b 100644 --- a/doc/src/context.json +++ b/doc/src/context.json @@ -1,5 +1,5 @@ { - "version": "0.17.0", + "version": "0.18.0", "rootUrl": "", "rootUrlX": "/rhai", "rootUrlXX": "/rhai/vnext" From 16fbfbb606347c24777718a9e29e6c68fd31d5b4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 16 Jul 2020 12:09:31 +0800 Subject: [PATCH 2/6] Enable call in function-call style. --- RELEASES.md | 5 ++++ doc/src/language/fn-ptr.md | 4 +-- src/engine.rs | 54 ++++++++++++++++++++++++++++++-------- src/parser.rs | 39 ++++++++++++++++++--------- src/token.rs | 13 ++++++--- tests/functions.rs | 16 ++++++++++- 6 files changed, 102 insertions(+), 29 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 938b4d2c..ed848b2e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,6 +4,11 @@ Rhai Release Notes Version 0.18.0 ============== +New features +------------ + +* `call` can now be called function-call style for function pointers - this is to handle builds with `no_object`. +* Disallow many keywords as variables, such as `print`, `eval`, `call`, `this` etc. Version 0.17.0 diff --git a/doc/src/language/fn-ptr.md b/doc/src/language/fn-ptr.md index e4d9bc5d..51c7d39c 100644 --- a/doc/src/language/fn-ptr.md +++ b/doc/src/language/fn-ptr.md @@ -6,7 +6,7 @@ Function Pointers 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. -Call a function pointer using the `call` method, which needs to be called in method-call style. +Call a function pointer using the `call` method. Built-in methods @@ -40,7 +40,7 @@ func.call(1) == 42; // call a function pointer with the 'call' method foo(1) == 42; // <- the above de-sugars to this -call(func, 1); //<- error: 'call (Fn, i64)' is not a registered function +call(func, 1); // normal function call style also works for 'call' let len = Fn("len"); // 'Fn' also works with registered native Rust functions diff --git a/src/engine.rs b/src/engine.rs index 69eaf371..f0bdfc48 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -944,7 +944,7 @@ impl Engine { } // Has a system function an override? - fn has_override(&self, lib: &Module, (hash_fn, hash_script): (u64, u64)) -> bool { + fn has_override(&self, lib: &Module, hash_fn: u64, hash_script: u64) -> bool { // NOTE: We skip script functions for global_module and packages, and native functions for lib // First check script-defined functions @@ -986,13 +986,15 @@ impl Engine { match fn_name { // type_of - KEYWORD_TYPE_OF if args.len() == 1 && !self.has_override(lib, hashes) => Ok(( - self.map_type_name(args[0].type_name()).to_string().into(), - false, - )), + KEYWORD_TYPE_OF if args.len() == 1 && !self.has_override(lib, hashes.0, hashes.1) => { + Ok(( + self.map_type_name(args[0].type_name()).to_string().into(), + false, + )) + } // Fn - KEYWORD_FN_PTR if args.len() == 1 && !self.has_override(lib, hashes) => { + KEYWORD_FN_PTR if args.len() == 1 && !self.has_override(lib, hashes.0, hashes.1) => { Err(Box::new(EvalAltResult::ErrorRuntime( "'Fn' should not be called in method style. Try Fn(...);".into(), Position::none(), @@ -1000,7 +1002,7 @@ impl Engine { } // eval - reaching this point it must be a method-style call - KEYWORD_EVAL if args.len() == 1 && !self.has_override(lib, hashes) => { + KEYWORD_EVAL if args.len() == 1 && !self.has_override(lib, hashes.0, hashes.1) => { Err(Box::new(EvalAltResult::ErrorRuntime( "'eval' should not be called in method style. Try eval(...);".into(), Position::none(), @@ -1926,7 +1928,7 @@ impl Engine { let hash_fn = calc_fn_hash(empty(), name, 1, once(TypeId::of::())); - if !self.has_override(lib, (hash_fn, *hash)) { + if !self.has_override(lib, hash_fn, *hash) { // Fn - only in function call style let expr = args_expr.get(0); let arg_value = @@ -1952,7 +1954,7 @@ impl Engine { let hash_fn = calc_fn_hash(empty(), name, 1, once(TypeId::of::())); - if !self.has_override(lib, (hash_fn, *hash)) { + if !self.has_override(lib, hash_fn, *hash) { // eval - only in function call style let prev_len = scope.len(); let expr = args_expr.get(0); @@ -1972,6 +1974,36 @@ impl Engine { } } + // Handle call() - Redirect function call + let redirected; + let mut name = name.as_ref(); + let mut args_expr = args_expr.as_ref(); + let mut hash = *hash; + + if name == KEYWORD_FN_PTR_CALL + && args_expr.len() >= 1 + && !self.has_override(lib, 0, hash) + { + let expr = args_expr.get(0).unwrap(); + let fn_ptr = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; + + if fn_ptr.is::() { + // Redirect function name + redirected = Some(fn_ptr.cast::().take_fn_name()); + name = redirected.as_ref().unwrap(); + // Skip the first argument + args_expr = &args_expr.as_ref()[1..]; + // Recalculate hash + hash = calc_fn_hash(empty(), name, args_expr.len(), empty()); + } else { + return Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + self.map_type_name(type_name::()).into(), + fn_ptr.type_name().into(), + expr.position(), + ))); + } + } + // Normal function call - except for Fn and eval (handled above) let mut arg_values: StaticVec; let mut args: StaticVec<_>; @@ -1983,7 +2015,7 @@ impl Engine { } 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) { + match args_expr.get(0).unwrap() { // func(x, ...) -> x.func(...) lhs @ Expr::Variable(_) => { arg_values = args_expr @@ -2020,7 +2052,7 @@ impl Engine { let args = args.as_mut(); self.exec_fn_call( - state, lib, name, *native, *hash, args, is_ref, false, def_val, level, + state, lib, name, *native, hash, args, is_ref, false, def_val, level, ) .map(|(v, _)| v) .map_err(|err| err.new_position(*pos)) diff --git a/src/parser.rs b/src/parser.rs index 55fbdabf..eca7ef65 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -7,7 +7,7 @@ use crate::error::{LexError, ParseError, ParseErrorType}; use crate::module::{Module, ModuleRef}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::scope::{EntryType as ScopeEntryType, Scope}; -use crate::token::{Position, Token, TokenStream}; +use crate::token::{is_valid_identifier, Position, Token, TokenStream}; use crate::utils::{StaticVec, StraightHasherBuilder}; #[cfg(feature = "internals")] @@ -440,6 +440,8 @@ struct ParseSettings { pos: Position, /// Is the construct being parsed located at global level? is_global: bool, + /// Is the construct being parsed located at function definition level? + is_function_scope: bool, /// Is the current position inside a loop? is_breakable: bool, /// Is anonymous function allowed? @@ -1460,6 +1462,21 @@ fn parse_primary( let index = state.find_var(&s); Expr::Variable(Box::new(((s, settings.pos), None, 0, index))) } + // Function call is allowed to have reserved keyword + Token::Reserved(s) if s != KEYWORD_THIS && input.peek().unwrap().0 == Token::LeftParen => { + Expr::Variable(Box::new(((s, settings.pos), None, 0, None))) + } + // Access to `this` as a variable is OK + Token::Reserved(s) if s == KEYWORD_THIS && input.peek().unwrap().0 != Token::LeftParen => { + if !settings.is_function_scope { + return Err( + PERR::BadInput(format!("'{}' can only be used in functions", s)) + .into_err(settings.pos), + ); + } else { + Expr::Variable(Box::new(((s, settings.pos), None, 0, None))) + } + } Token::LeftParen => parse_paren_expr(input, state, lib, settings.level_up())?, #[cfg(not(feature = "no_index"))] Token::LeftBracket => parse_array_literal(input, state, lib, settings.level_up())?, @@ -1538,6 +1555,7 @@ fn parse_primary( _ => (), } + // Make sure identifiers are valid Ok(root_expr) } @@ -2315,16 +2333,6 @@ 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 @@ -2598,6 +2606,7 @@ fn parse_stmt( allow_stmt_expr: true, allow_anonymous_fn: true, is_global: false, + is_function_scope: true, is_breakable: false, level: 0, pos: pos, @@ -2695,7 +2704,11 @@ fn parse_fn( settings.ensure_level_within_max_limit(state.max_expr_depth)?; let name = match input.next().unwrap() { - (Token::Identifier(s), _) | (Token::Custom(s), _) => s, + (Token::Identifier(s), _) | (Token::Custom(s), _) | (Token::Reserved(s), _) + if s != KEYWORD_THIS && is_valid_identifier(s.chars()) => + { + s + } (_, pos) => return Err(PERR::FnMissingName.into_err(pos)), }; @@ -2790,6 +2803,7 @@ impl Engine { allow_stmt_expr: false, allow_anonymous_fn: false, is_global: true, + is_function_scope: false, is_breakable: false, level: 0, pos: Position::none(), @@ -2829,6 +2843,7 @@ impl Engine { allow_stmt_expr: true, allow_anonymous_fn: true, is_global: true, + is_function_scope: false, is_breakable: false, level: 0, pos: Position::none(), diff --git a/src/token.rs b/src/token.rs index 88196856..25e39fc8 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,6 +1,10 @@ //! Main module defining the lexer and parser. -use crate::engine::Engine; +use crate::engine::{ + Engine, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL, KEYWORD_PRINT, + KEYWORD_THIS, KEYWORD_TYPE_OF, +}; + use crate::error::LexError; use crate::parser::INT; use crate::utils::StaticVec; @@ -388,6 +392,8 @@ impl Token { "===" | "!==" | "->" | "<-" | "=>" | ":=" | "::<" | "(*" | "*)" | "#" => { Reserved(syntax.into()) } + KEYWORD_PRINT | KEYWORD_DEBUG | KEYWORD_TYPE_OF | KEYWORD_EVAL | KEYWORD_FN_PTR + | KEYWORD_FN_PTR_CALL | KEYWORD_THIS => Reserved(syntax.into()), _ => return None, }) @@ -1353,9 +1359,10 @@ impl<'a> Iterator for TokenIterator<'a, '_> { "'#' is not a valid symbol. Should it be '#{'?" .to_string(), ))), - token => Token::LexError(Box::new(LERR::ImproperSymbol( - format!("'{}' is not a valid symbol.", token) + token if !is_valid_identifier(token.chars()) => Token::LexError(Box::new(LERR::ImproperSymbol( + format!("'{}' is a reserved symbol.", token) ))), + _ => Token::Reserved(s) }, pos)), (r @ Some(_), None, None) => r, (Some((Token::Identifier(s), pos)), _, Some(custom)) if custom.contains_key(&s) => { diff --git a/tests/functions.rs b/tests/functions.rs index ed3eeb73..3c0a080b 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -52,12 +52,24 @@ fn test_functions() -> Result<(), Box> { } #[test] -#[cfg(not(feature = "no_object"))] fn test_function_pointers() -> Result<(), Box> { let engine = Engine::new(); assert_eq!(engine.eval::(r#"type_of(Fn("abc"))"#)?, "Fn"); + assert_eq!( + engine.eval::( + r#" + fn foo(x) { 40 + x } + + let f = Fn("foo"); + call(f, 2) + "# + )?, + 42 + ); + + #[cfg(not(feature = "no_object"))] assert_eq!( engine.eval::( r#" @@ -73,11 +85,13 @@ fn test_function_pointers() -> Result<(), Box> { 42 ); + #[cfg(not(feature = "no_object"))] assert!(matches!( *engine.eval::(r#"let f = Fn("abc"); f.call(0)"#).expect_err("should error"), EvalAltResult::ErrorFunctionNotFound(f, _) if f.starts_with("abc (") )); + #[cfg(not(feature = "no_object"))] assert_eq!( engine.eval::( r#" From 197f5d370fdcf042a1d8b9cd56da426ed7e9c6a7 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 16 Jul 2020 12:09:40 +0800 Subject: [PATCH 3/6] Reformat docs. --- README.md | 12 ++++++------ doc/src/advanced.md | 10 ++++++++-- doc/src/safety/max-array-size.md | 4 ++-- doc/src/safety/max-call-stack.md | 4 ++-- doc/src/safety/max-map-size.md | 4 ++-- doc/src/safety/max-operations.md | 4 ++-- doc/src/safety/max-stmt-depth.md | 4 ++-- doc/src/safety/max-string-size.md | 4 ++-- doc/src/safety/progress.md | 4 ++-- 9 files changed, 28 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 92f2ef78..1f9f5bf8 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ Supported targets and builds * WebAssembly (WASM) * `no-std` -Standard Features ----------------- +Standard features +----------------- * Easy-to-use language similar to JavaScript+Rust with dynamic typing. * Tight integration with native Rust [functions](https://schungx.github.io/rhai/rust/functions.html) and [types]([#custom-types-and-methods](https://schungx.github.io/rhai/rust/custom.html)), including [getters/setters](https://schungx.github.io/rhai/rust/getters-setters.html), [methods](https://schungx.github.io/rhai/rust/custom.html) and [indexers](https://schungx.github.io/rhai/rust/indexers.html). @@ -39,19 +39,19 @@ Standard Features * Scripts are [optimized](https://schungx.github.io/rhai/engine/optimize.html) (useful for template-based machine-generated scripts) for repeated evaluations. * Support for [minimal builds](https://schungx.github.io/rhai/start/builds/minimal.html) by excluding unneeded language [features](https://schungx.github.io/rhai/start/features.html). -Protection Against Attacks -------------------------- +Protection against attacks +-------------------------- * Sand-boxed - the scripting engine, if declared immutable, cannot mutate the containing environment unless explicitly permitted (e.g. via a `RefCell`). * Rugged - protected against malicious attacks (such as [stack-overflow](https://schungx.github.io/rhai/safety/max-call-stack.html), [over-sized data](https://schungx.github.io/rhai/safety/max-string-size.html), and [runaway scripts](https://schungx.github.io/rhai/safety/max-operations.html) etc.) that may come from untrusted third-party user-land scripts. * Track script evaluation [progress](https://schungx.github.io/rhai/safety/progress.html) and manually terminate a script run. -For Those Who Actually Want Their Own Language +For those who actually want their own language --------------------------------------------- * Use as a [DSL](https://schungx.github.io/rhai/engine/dsl.html). -* Define [custom operators](https://schungx.github.io/rhai/engine/custom-op.html). * Restrict the language by surgically [disabling keywords and operators](https://schungx.github.io/rhai/engine/disable.html). +* Define [custom operators](https://schungx.github.io/rhai/engine/custom-op.html). * Extend the language with [custom syntax](https://schungx.github.io/rhai/engine/custom-syntax.html). Documentation diff --git a/doc/src/advanced.md b/doc/src/advanced.md index 3cb28cb5..b7d69e39 100644 --- a/doc/src/advanced.md +++ b/doc/src/advanced.md @@ -5,6 +5,12 @@ Advanced Topics This section covers advanced features such as: -* [Script optimization] +* Simulated [Object Oriented Programming][OOP]. -* The dreaded (or beloved for those with twisted tastes) [`eval`] statement +* [`serde`] integration. + +* [Script optimization]. + +* [Domain-Specific Languages][DSL]. + +* The dreaded (or beloved for those with twisted tastes) [`eval`] statement. diff --git a/doc/src/safety/max-array-size.md b/doc/src/safety/max-array-size.md index af5ba6f0..d2724e0f 100644 --- a/doc/src/safety/max-array-size.md +++ b/doc/src/safety/max-array-size.md @@ -3,8 +3,8 @@ Maximum Size of Arrays {{#include ../links.md}} -Limiting How Large Arrays Can Grow ---------------------------------- +Limit How Large Arrays Can Grow +------------------------------ Rhai by default does not limit how large an [array] can be. diff --git a/doc/src/safety/max-call-stack.md b/doc/src/safety/max-call-stack.md index f10a8fbe..2d33d653 100644 --- a/doc/src/safety/max-call-stack.md +++ b/doc/src/safety/max-call-stack.md @@ -3,8 +3,8 @@ Maximum Call Stack Depth {{#include ../links.md}} -Limiting How Stack Usage by Scripts ----------------------------------- +Limit How Stack Usage by Scripts +------------------------------- Rhai by default limits function calls to a maximum depth of 128 levels (16 levels in debug build). diff --git a/doc/src/safety/max-map-size.md b/doc/src/safety/max-map-size.md index 2fcd3273..a980562b 100644 --- a/doc/src/safety/max-map-size.md +++ b/doc/src/safety/max-map-size.md @@ -3,8 +3,8 @@ Maximum Size of Object Maps {{#include ../links.md}} -Limiting How Large Object Maps Can Grow --------------------------------------- +Limit How Large Object Maps Can Grow +----------------------------------- Rhai by default does not limit how large (i.e. the number of properties) an [object map] can be. diff --git a/doc/src/safety/max-operations.md b/doc/src/safety/max-operations.md index 6dd5d7d9..e4778b88 100644 --- a/doc/src/safety/max-operations.md +++ b/doc/src/safety/max-operations.md @@ -3,8 +3,8 @@ Maximum Number of Operations {{#include ../links.md}} -Limiting How Long a Script Can Run ---------------------------------- +Limit How Long a Script Can Run +------------------------------ Rhai by default does not limit how much time or CPU a script consumes. diff --git a/doc/src/safety/max-stmt-depth.md b/doc/src/safety/max-stmt-depth.md index e8b5ee84..e9749689 100644 --- a/doc/src/safety/max-stmt-depth.md +++ b/doc/src/safety/max-stmt-depth.md @@ -3,8 +3,8 @@ Maximum Statement Depth {{#include ../links.md}} -Limiting How Deeply-Nested a Statement Can Be --------------------------------------------- +Limit How Deeply-Nested a Statement Can Be +----------------------------------------- Rhai by default limits statements and expressions nesting to a maximum depth of 128 (which should be plenty) when they are at _global_ level, but only a depth of 32 diff --git a/doc/src/safety/max-string-size.md b/doc/src/safety/max-string-size.md index 4b279d68..c693af9c 100644 --- a/doc/src/safety/max-string-size.md +++ b/doc/src/safety/max-string-size.md @@ -3,8 +3,8 @@ Maximum Length of Strings {{#include ../links.md}} -Limiting How Long Strings Can Grow ---------------------------------- +Limit How Long Strings Can Grow +------------------------------ Rhai by default does not limit how long a [string] can be. diff --git a/doc/src/safety/progress.md b/doc/src/safety/progress.md index 00c2ed0e..3432a00e 100644 --- a/doc/src/safety/progress.md +++ b/doc/src/safety/progress.md @@ -1,5 +1,5 @@ -Tracking Progress and Force-Termination -====================================== +Track Progress and Force-Termination +=================================== {{#include ../links.md}} From d119e13b7936d88349a08805e481a0908796255f Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 17 Jul 2020 10:18:07 +0800 Subject: [PATCH 4/6] Allow binding of this pointer in FnPtr calls. --- RELEASES.md | 1 + doc/src/language/fn-ptr.md | 46 +++++++++-- src/engine.rs | 26 ++++++- src/error.rs | 4 + src/parser.rs | 153 ++++++++++++++++++++++++------------- src/token.rs | 19 ++++- src/utils.rs | 6 ++ tests/call_fn.rs | 2 +- tests/fn_ptr.rs | 80 +++++++++++++++++++ 9 files changed, 267 insertions(+), 70 deletions(-) create mode 100644 tests/fn_ptr.rs diff --git a/RELEASES.md b/RELEASES.md index ed848b2e..9a48b709 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,6 +9,7 @@ New features * `call` can now be called function-call style for function pointers - this is to handle builds with `no_object`. * Disallow many keywords as variables, such as `print`, `eval`, `call`, `this` etc. +* `x.call(f, ...)` allows binding `x` to `this` for the function referenced by the function pointer `f`. Version 0.17.0 diff --git a/doc/src/language/fn-ptr.md b/doc/src/language/fn-ptr.md index 51c7d39c..57f2f24f 100644 --- a/doc/src/language/fn-ptr.md +++ b/doc/src/language/fn-ptr.md @@ -66,20 +66,18 @@ Because of their dynamic nature, function pointers cannot refer to functions in See [function namespaces] for more details. ```rust -import "foo" as f; // assume there is 'f::do_something()' +import "foo" as f; // assume there is 'f::do_work()' -f::do_something(); // works! +f::do_work(); // works! -let p = Fn("f::do_something"); +let p = Fn("f::do_work"); // error: invalid function name -p.call(); // error: function not found - 'f::do_something' - -fn do_something_now() { // call it from a local function +fn do_work_now() { // call it from a local function import "foo" as f; - f::do_something(); + f::do_work(); } -let p = Fn("do_something_now"); +let p = Fn("do_work_now"); p.call(); // works! ``` @@ -134,3 +132,35 @@ let func = sign(x) + 1; // Dynamic dispatch map[func].call(42); ``` + + +Binding the `this` Pointer +------------------------- + +When `call` is called as a _method_ but not on a `FnPtr` value, it is possible to dynamically dispatch +to a function call while binding the object in the method call to the `this` pointer of the function. + +To achieve this, pass the `FnPtr` value as the _first_ argument to `call`: + +```rust +fn add(x) { this += x; } // define function which uses 'this' + +let func = Fn("add"); // function pointer to 'add' + +func.call(1); // error: 'this' pointer is not bound + +let x = 41; + +func.call(x, 1); // error: function 'add (i64, i64)' not found + +call(func, x, 1); // error: function 'add (i64, i64)' not found + +x.call(func, 1); // 'this' is bound to 'x', dispatched to 'func' + +x == 42; +``` + +Beware that this only works for _method-call_ style. Normal function-call style cannot bind +the `this` pointer (for syntactic reasons). + +Therefore, obviously, binding the `this` pointer is unsupported under [`no_function`]. diff --git a/src/engine.rs b/src/engine.rs index f0bdfc48..dd1879cc 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -96,7 +96,7 @@ pub const MARKER_BLOCK: &str = "$block$"; pub const MARKER_IDENT: &str = "$ident$"; #[cfg(feature = "internals")] -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash)] pub struct Expression<'a>(&'a Expr); #[cfg(feature = "internals")] @@ -1088,10 +1088,10 @@ impl Engine { let idx = idx_val.downcast_mut::>().unwrap(); let mut fn_name = name.as_ref(); - // Check if it is a FnPtr call let (result, updated) = if fn_name == KEYWORD_FN_PTR_CALL && obj.is::() { + // FnPtr call // Redirect function name - fn_name = obj.as_str().unwrap(); + let fn_name = obj.as_str().unwrap(); // Recalculate hash let hash = calc_fn_hash(empty(), fn_name, idx.len(), empty()); // Arguments are passed as-is @@ -1102,6 +1102,26 @@ impl Engine { self.exec_fn_call( state, lib, fn_name, *native, hash, args, false, false, def_val, level, ) + } else if fn_name == KEYWORD_FN_PTR_CALL && idx.len() > 0 && idx[0].is::() { + // FnPtr call on object + // Redirect function name + let fn_name = idx[0] + .downcast_ref::() + .unwrap() + .get_fn_name() + .clone(); + // Recalculate hash + let hash = calc_fn_hash(empty(), &fn_name, idx.len() - 1, empty()); + // Replace the first argument with the object pointer + let mut arg_values = once(obj) + .chain(idx.iter_mut().skip(1)) + .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, is_ref, true, def_val, level, + ) } else { let redirected: Option; let mut hash = *hash; diff --git a/src/error.rs b/src/error.rs index 6b3308ba..1f4b08bf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -98,6 +98,8 @@ pub enum ParseErrorType { PropertyExpected, /// Missing a variable name after the `let`, `const` or `for` keywords. VariableExpected, + /// An identifier is a reserved keyword. + Reserved(String), /// Missing an expression. Wrapped value is the expression type. ExprExpected(String), /// Defining a function `fn` in an appropriate place (e.g. inside another function). @@ -163,6 +165,7 @@ impl ParseErrorType { Self::ForbiddenConstantExpr(_) => "Expecting a constant", Self::PropertyExpected => "Expecting name of a property", Self::VariableExpected => "Expecting name of a variable", + Self::Reserved(_) => "Invalid use of reserved keyword", Self::ExprExpected(_) => "Expecting an expression", Self::FnMissingName => "Expecting name in function declaration", Self::FnMissingParams(_) => "Expecting parameters in function declaration", @@ -224,6 +227,7 @@ impl fmt::Display for ParseErrorType { Self::LiteralTooLarge(typ, max) => { write!(f, "{} exceeds the maximum limit ({})", typ, max) } + Self::Reserved(s) => write!(f, "'{}' is a reserved keyword", s), _ => f.write_str(self.desc()), } } diff --git a/src/parser.rs b/src/parser.rs index eca7ef65..e2f0c3f2 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -25,6 +25,7 @@ use crate::stdlib::{ char, collections::HashMap, fmt, format, + hash::Hash, iter::empty, mem, num::NonZeroUsize, @@ -340,7 +341,7 @@ impl fmt::Display for FnAccess { } /// A scripted function definition. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash)] pub struct ScriptFnDef { /// Function name. pub name: String, @@ -374,7 +375,7 @@ impl fmt::Display for ScriptFnDef { } /// `return`/`throw` statement. -#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] +#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] pub enum ReturnType { /// `return` statement. Return, @@ -478,7 +479,7 @@ impl ParseSettings { /// /// Each variant is at most one pointer in size (for speed), /// with everything being allocated together in one single tuple. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Hash)] pub enum Stmt { /// No-op. Noop(Position), @@ -590,6 +591,13 @@ impl fmt::Debug for CustomExpr { } } +#[cfg(feature = "internals")] +impl Hash for CustomExpr { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + /// An expression. /// /// Each variant is at most one pointer in size (for speed), @@ -665,6 +673,18 @@ impl Default for Expr { } } +impl Hash for Expr { + fn hash(&self, state: &mut H) { + match self { + Self::FloatConstant(x) => { + state.write(&x.0.to_le_bytes()); + x.1.hash(state); + } + _ => self.hash(state), + } + } +} + impl Expr { /// Get the `Dynamic` value of a constant expression. /// @@ -1345,56 +1365,57 @@ fn parse_map_literal( eat_token(input, Token::RightBrace); break; } - _ => { - let (name, pos) = match input.next().unwrap() { - (Token::Identifier(s), pos) => (s, pos), - (Token::StringConstant(s), pos) => (s, pos), - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (_, pos) if map.is_empty() => { - return Err(PERR::MissingToken( - Token::RightBrace.into(), - MISSING_RBRACE.into(), - ) - .into_err(pos)) - } - (Token::EOF, pos) => { - return Err(PERR::MissingToken( - Token::RightBrace.into(), - MISSING_RBRACE.into(), - ) - .into_err(pos)) - } - (_, pos) => return Err(PERR::PropertyExpected.into_err(pos)), - }; - - match input.next().unwrap() { - (Token::Colon, _) => (), - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (_, pos) => { - return Err(PERR::MissingToken( - Token::Colon.into(), - format!( - "to follow the property '{}' in this object map literal", - name - ), - ) - .into_err(pos)) - } - }; - - if state.engine.max_map_size > 0 && map.len() >= state.engine.max_map_size { - return Err(PERR::LiteralTooLarge( - "Number of properties in object map literal".to_string(), - state.engine.max_map_size, - ) - .into_err(input.peek().unwrap().1)); - } - - let expr = parse_expr(input, state, lib, settings.level_up())?; - map.push(((Into::::into(name), pos), expr)); - } + _ => (), } + let (name, pos) = match input.next().unwrap() { + (Token::Identifier(s), pos) => (s, pos), + (Token::StringConstant(s), pos) => (s, pos), + (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { + return Err(PERR::Reserved(s).into_err(pos)); + } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) if map.is_empty() => { + return Err( + PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) + .into_err(pos), + ); + } + (Token::EOF, pos) => { + return Err( + PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) + .into_err(pos), + ); + } + (_, pos) => return Err(PERR::PropertyExpected.into_err(pos)), + }; + + match input.next().unwrap() { + (Token::Colon, _) => (), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) => { + return Err(PERR::MissingToken( + Token::Colon.into(), + format!( + "to follow the property '{}' in this object map literal", + name + ), + ) + .into_err(pos)) + } + }; + + if state.engine.max_map_size > 0 && map.len() >= state.engine.max_map_size { + return Err(PERR::LiteralTooLarge( + "Number of properties in object map literal".to_string(), + state.engine.max_map_size, + ) + .into_err(input.peek().unwrap().1)); + } + + let expr = parse_expr(input, state, lib, settings.level_up())?; + map.push(((Into::::into(name), pos), expr)); + match input.peek().unwrap() { (Token::Comma, _) => { eat_token(input, Token::Comma); @@ -1477,6 +1498,9 @@ fn parse_primary( Expr::Variable(Box::new(((s, settings.pos), None, 0, None))) } } + Token::Reserved(s) if is_valid_identifier(s.chars()) => { + return Err(PERR::Reserved(s).into_err(settings.pos)); + } Token::LeftParen => parse_paren_expr(input, state, lib, settings.level_up())?, #[cfg(not(feature = "no_index"))] Token::LeftBracket => parse_array_literal(input, state, lib, settings.level_up())?, @@ -1488,7 +1512,7 @@ fn parse_primary( _ => { return Err( PERR::BadInput(format!("Unexpected '{}'", token.syntax())).into_err(settings.pos) - ) + ); } }; @@ -1526,6 +1550,9 @@ fn parse_primary( Expr::Variable(Box::new(((id2, pos2), modules, 0, index))) } + (Token::Reserved(id2), pos2) if is_valid_identifier(id2.chars()) => { + return Err(PERR::Reserved(id2).into_err(pos2)); + } (_, pos2) => return Err(PERR::VariableExpected.into_err(pos2)), }, // Indexing @@ -2112,6 +2139,9 @@ fn parse_expr( (Token::Identifier(s), pos) => { exprs.push(Expr::Variable(Box::new(((s, pos), None, 0, None)))); } + (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { + return Err(PERR::Reserved(s).into_err(pos)); + } (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }, MARKER_EXPR => exprs.push(parse_expr(input, state, lib, settings)?), @@ -2279,10 +2309,12 @@ fn parse_for( let name = match input.next().unwrap() { // Variable name (Token::Identifier(s), _) => s, + // Reserved keyword + (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { + return Err(PERR::Reserved(s).into_err(pos)); + } // Bad identifier (Token::LexError(err), pos) => return Err(err.into_err(pos)), - // EOF - (Token::EOF, pos) => return Err(PERR::VariableExpected.into_err(pos)), // Not a variable name (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; @@ -2329,6 +2361,9 @@ fn parse_let( // let name ... let (name, pos) = match input.next().unwrap() { (Token::Identifier(s), pos) => (s, pos), + (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { + return Err(PERR::Reserved(s).into_err(pos)); + } (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; @@ -2398,6 +2433,9 @@ fn parse_import( // import expr as name ... let (name, _) = match input.next().unwrap() { (Token::Identifier(s), pos) => (s, pos), + (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { + return Err(PERR::Reserved(s).into_err(pos)); + } (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; @@ -2422,6 +2460,9 @@ fn parse_export( loop { let (id, id_pos) = match input.next().unwrap() { (Token::Identifier(s), pos) => (s.clone(), pos), + (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { + return Err(PERR::Reserved(s).into_err(pos)); + } (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }; @@ -2429,6 +2470,10 @@ fn parse_export( let rename = if match_token(input, Token::As)? { match input.next().unwrap() { (Token::Identifier(s), pos) => Some((s.clone(), pos)), + (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { + return Err(PERR::Reserved(s).into_err(pos)); + } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), } } else { diff --git a/src/token.rs b/src/token.rs index 25e39fc8..f3579317 100644 --- a/src/token.rs +++ b/src/token.rs @@ -279,8 +279,6 @@ impl Token { Or => "||", Ampersand => "&", And => "&&", - #[cfg(not(feature = "no_function"))] - Fn => "fn", Continue => "continue", Break => "break", Return => "return", @@ -301,8 +299,12 @@ impl Token { ModuloAssign => "%=", PowerOf => "~", PowerOfAssign => "~=", + + #[cfg(not(feature = "no_function"))] + Fn => "fn", #[cfg(not(feature = "no_function"))] Private => "private", + #[cfg(not(feature = "no_module"))] Import => "import", #[cfg(not(feature = "no_module"))] @@ -359,8 +361,6 @@ impl Token { "||" => Or, "&" => Ampersand, "&&" => And, - #[cfg(not(feature = "no_function"))] - "fn" => Fn, "continue" => Continue, "break" => Break, "return" => Return, @@ -381,14 +381,25 @@ impl Token { "%=" => ModuloAssign, "~" => PowerOf, "~=" => PowerOfAssign, + + #[cfg(not(feature = "no_function"))] + "fn" => Fn, #[cfg(not(feature = "no_function"))] "private" => Private, + #[cfg(not(feature = "no_module"))] "import" => Import, #[cfg(not(feature = "no_module"))] "export" => Export, #[cfg(not(feature = "no_module"))] "as" => As, + + #[cfg(feature = "no_function")] + "fn" | "private" => Reserved(syntax.into()), + + #[cfg(feature = "no_module")] + "import" | "export" | "as" => Reserved(syntax.into()), + "===" | "!==" | "->" | "<-" | "=>" | ":=" | "::<" | "(*" | "*)" | "#" => { Reserved(syntax.into()) } diff --git a/src/utils.rs b/src/utils.rs index 5dd619ba..01fdd26b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -156,6 +156,12 @@ impl Drop for StaticVec { } } +impl Hash for StaticVec { + fn hash(&self, state: &mut H) { + self.iter().for_each(|x| x.hash(state)); + } +} + impl Default for StaticVec { fn default() -> Self { Self { diff --git a/tests/call_fn.rs b/tests/call_fn.rs index 2eecab90..a04bd614 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -116,7 +116,7 @@ fn test_anonymous_fn() -> Result<(), Box> { #[test] #[cfg(not(feature = "no_object"))] -fn test_fn_ptr() -> Result<(), Box> { +fn test_fn_ptr_raw() -> Result<(), Box> { let mut engine = Engine::new(); engine.register_raw_fn( diff --git a/tests/fn_ptr.rs b/tests/fn_ptr.rs new file mode 100644 index 00000000..e9121475 --- /dev/null +++ b/tests/fn_ptr.rs @@ -0,0 +1,80 @@ +use rhai::{Engine, EvalAltResult, RegisterFn, INT}; + +#[test] +fn test_fn_ptr() -> Result<(), Box> { + let mut engine = Engine::new(); + + engine.register_fn("bar", |x: &mut INT, y: INT| *x += y); + + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::( + r#" + let f = Fn("bar"); + let x = 40; + f.call(x, 2); + x + "# + )?, + 40 + ); + + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::( + r#" + let f = Fn("bar"); + let x = 40; + x.call(f, 2); + x + "# + )?, + 42 + ); + + assert_eq!( + engine.eval::( + r#" + let f = Fn("bar"); + let x = 40; + call(f, x, 2); + x + "# + )?, + 42 + ); + + #[cfg(not(feature = "no_function"))] + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::( + r#" + fn foo(x) { this += x; } + + let f = Fn("foo"); + let x = 40; + x.call(f, 2); + x + "# + )?, + 42 + ); + + #[cfg(not(feature = "no_function"))] + assert!(matches!( + *engine + .eval::( + r#" + fn foo(x) { this += x; } + + let f = Fn("foo"); + call(f, 2); + x + "# + ) + .expect_err("should error"), + EvalAltResult::ErrorInFunctionCall(fn_name, err, _) if fn_name == "foo" && matches!(*err, EvalAltResult::ErrorUnboundedThis(_)) + )); + + Ok(()) +} From 3ae7cf4018a8017ecb0da2a750771766107e5054 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 17 Jul 2020 14:50:23 +0800 Subject: [PATCH 5/6] Improve treatment of disabled symbols and custom symbols. --- doc/src/engine/custom-syntax.md | 8 +-- doc/src/engine/disable.md | 3 +- src/parser.rs | 3 -- src/settings.rs | 23 +++++++-- src/syntax.rs | 5 ++ src/token.rs | 86 ++++++++++++++++++--------------- tests/modules.rs | 37 ++++++-------- tests/syntax.rs | 15 ++++-- tests/tokens.rs | 7 ++- 9 files changed, 110 insertions(+), 77 deletions(-) diff --git a/doc/src/engine/custom-syntax.md b/doc/src/engine/custom-syntax.md index e0529841..0bd9fb30 100644 --- a/doc/src/engine/custom-syntax.md +++ b/doc/src/engine/custom-syntax.md @@ -68,11 +68,13 @@ These symbol types can be used: ### The First Symbol Must be a Keyword There is no specific limit on the combination and sequencing of each symbol type, -except the _first_ symbol which must be a [custom keyword]. +except the _first_ symbol which must be a custom keyword that follows the naming rules +of [variables]. -It _cannot_ be a [built-in keyword]({{rootUrl}}/appendix/keywords.md). +The first symbol also cannot be a reserved [keyword], unless that keyword +has been [disabled][disable keywords and operators]. -However, it _may_ be a built-in keyword that has been [disabled][disable keywords and operators]. +In other words, any valid identifier that is not an active [keyword] will work fine. ### The First Symbol Must be Unique diff --git a/doc/src/engine/disable.md b/doc/src/engine/disable.md index 3810fa5e..28e35f70 100644 --- a/doc/src/engine/disable.md +++ b/doc/src/engine/disable.md @@ -20,8 +20,7 @@ engine // The following all return parse errors. engine.compile("let x = if true { 42 } else { 0 };")?; -// ^ missing ';' after statement end -// ^ 'if' is parsed as a variable name +// ^ 'if' is rejected as a reserved keyword engine.compile("let x = 40 + 2; x += 1;")?; // ^ '+=' is not recognized as an operator diff --git a/src/parser.rs b/src/parser.rs index e2f0c3f2..3f79d089 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2151,9 +2151,6 @@ fn parse_expr( exprs.push(Expr::Stmt(Box::new((stmt, pos)))) } s => match input.peek().unwrap() { - (Token::Custom(custom), _) if custom == s => { - input.next().unwrap(); - } (t, _) if t.syntax().as_ref() == s => { input.next().unwrap(); } diff --git a/src/settings.rs b/src/settings.rs index f1967cdf..e7e4fd16 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,7 +2,7 @@ use crate::engine::Engine; use crate::module::ModuleResolver; use crate::optimize::OptimizationLevel; use crate::packages::PackageLibrary; -use crate::token::is_valid_identifier; +use crate::token::{is_valid_identifier, Token}; use crate::stdlib::{boxed::Box, format, string::String}; @@ -183,8 +183,7 @@ impl Engine { /// engine.disable_symbol("if"); // disable the 'if' keyword /// /// engine.compile("let x = if true { 42 } else { 0 };")?; - /// // ^ 'if' is parsed as a variable name - /// // ^ missing ';' after statement end + /// // ^ 'if' is rejected as a reserved keyword /// # Ok(()) /// # } /// ``` @@ -252,6 +251,24 @@ impl Engine { return Err(format!("not a valid identifier: '{}'", keyword).into()); } + match Token::lookup_from_syntax(keyword) { + // Standard identifiers, reserved keywords and custom keywords are OK + None | Some(Token::Reserved(_)) | Some(Token::Custom(_)) => (), + // Disabled keywords are also OK + Some(token) + if !self + .disabled_symbols + .as_ref() + .map(|d| d.contains(token.syntax().as_ref())) + .unwrap_or(false) => + { + () + } + // Active standard keywords cannot be made custom + Some(_) => return Err(format!("'{}' is a reserved keyword", keyword).into()), + } + + // Add to custom keywords if self.custom_keywords.is_none() { self.custom_keywords = Some(Default::default()); } diff --git a/src/syntax.rs b/src/syntax.rs index 3b646d25..9bd5fad1 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -88,11 +88,16 @@ impl Engine { MARKER_EXPR | MARKER_BLOCK | MARKER_IDENT if !segments.is_empty() => s.to_string(), // Standard symbols not in first position s if !segments.is_empty() && Token::lookup_from_syntax(s).is_some() => { + // Make it a custom keyword/operator if it is a disabled standard keyword/operator + // or a reserved keyword/operator. if self .disabled_symbols .as_ref() .map(|d| d.contains(s)) .unwrap_or(false) + || Token::lookup_from_syntax(s) + .map(|token| token.is_reserved()) + .unwrap_or(false) { // If symbol is disabled, make it a custom keyword if self.custom_keywords.is_none() { diff --git a/src/token.rs b/src/token.rs index f3579317..dae43624 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1334,73 +1334,81 @@ impl<'a> Iterator for TokenIterator<'a, '_> { self.engine.disabled_symbols.as_ref(), self.engine.custom_keywords.as_ref(), ) { + // {EOF} (None, _, _) => None, - (Some((Token::Reserved(s), pos)), None, None) => return Some((match s.as_str() { - "===" => Token::LexError(Box::new(LERR::ImproperSymbol( - "'===' is not a valid operator. This is not JavaScript! Should it be '=='?" - .to_string(), + // Reserved keyword/symbol + (Some((Token::Reserved(s), pos)), disabled, custom) => Some((match + (s.as_str(), custom.map(|c| c.contains_key(&s)).unwrap_or(false)) + { + ("===", false) => Token::LexError(Box::new(LERR::ImproperSymbol( + "'===' is not a valid operator. This is not JavaScript! Should it be '=='?".to_string(), ))), - "!==" => Token::LexError(Box::new(LERR::ImproperSymbol( - "'!==' is not a valid operator. This is not JavaScript! Should it be '!='?" - .to_string(), + ("!==", false) => Token::LexError(Box::new(LERR::ImproperSymbol( + "'!==' is not a valid operator. This is not JavaScript! Should it be '!='?".to_string(), ))), - "->" => Token::LexError(Box::new(LERR::ImproperSymbol( - "'->' is not a valid symbol. This is not C or C++!".to_string(), - ))), - "<-" => Token::LexError(Box::new(LERR::ImproperSymbol( + ("->", false) => Token::LexError(Box::new(LERR::ImproperSymbol( + "'->' is not a valid symbol. This is not C or C++!".to_string()))), + ("<-", false) => Token::LexError(Box::new(LERR::ImproperSymbol( "'<-' is not a valid symbol. This is not Go! Should it be '<='?".to_string(), ))), - "=>" => Token::LexError(Box::new(LERR::ImproperSymbol( - "'=>' is not a valid symbol. This is not Rust! Should it be '>='?" - .to_string(), + ("=>", false) => Token::LexError(Box::new(LERR::ImproperSymbol( + "'=>' is not a valid symbol. This is not Rust! Should it be '>='?".to_string(), ))), - ":=" => Token::LexError(Box::new(LERR::ImproperSymbol( - "':=' is not a valid assignment operator. This is not Go! Should it be simply '='?" - .to_string(), + (":=", false) => Token::LexError(Box::new(LERR::ImproperSymbol( + "':=' is not a valid assignment operator. This is not Go! Should it be simply '='?".to_string(), ))), - "::<" => Token::LexError(Box::new(LERR::ImproperSymbol( - "'::<>' is not a valid symbol. This is not Rust! Should it be '::'?" - .to_string(), + ("::<", false) => Token::LexError(Box::new(LERR::ImproperSymbol( + "'::<>' is not a valid symbol. This is not Rust! Should it be '::'?".to_string(), ))), - "(*" | "*)" => Token::LexError(Box::new(LERR::ImproperSymbol( - "'(* .. *)' is not a valid comment format. This is not Pascal! Should it be '/* .. */'?" - .to_string(), + ("(*", false) | ("*)", false) => Token::LexError(Box::new(LERR::ImproperSymbol( + "'(* .. *)' is not a valid comment format. This is not Pascal! Should it be '/* .. */'?".to_string(), ))), - "#" => Token::LexError(Box::new(LERR::ImproperSymbol( - "'#' is not a valid symbol. Should it be '#{'?" - .to_string(), + ("#", false) => Token::LexError(Box::new(LERR::ImproperSymbol( + "'#' is not a valid symbol. Should it be '#{'?".to_string(), ))), - token if !is_valid_identifier(token.chars()) => Token::LexError(Box::new(LERR::ImproperSymbol( - format!("'{}' is a reserved symbol.", token) + // Reserved keyword/operator that is custom. + (_, true) => Token::Custom(s), + // Reserved operator that is not custom. + (token, false) if !is_valid_identifier(token.chars()) => Token::LexError(Box::new(LERR::ImproperSymbol( + format!("'{}' is a reserved symbol", token) ))), - _ => Token::Reserved(s) + // Reserved keyword that is not custom and disabled. + (token, false) if disabled.map(|d| d.contains(token)).unwrap_or(false) => Token::LexError(Box::new(LERR::ImproperSymbol( + format!("reserved symbol '{}' is disabled", token) + ))), + // Reserved keyword/operator that is not custom. + (_, false) => Token::Reserved(s), }, pos)), - (r @ Some(_), None, None) => r, + // Custom keyword (Some((Token::Identifier(s), pos)), _, Some(custom)) if custom.contains_key(&s) => { - // Convert custom keywords Some((Token::Custom(s), pos)) } - (Some((token, pos)), _, Some(custom)) - if (token.is_keyword() || token.is_operator() || token.is_reserved()) - && custom.contains_key(token.syntax().as_ref()) => + // Custom standard keyword - must be disabled + (Some((token, pos)), Some(disabled), Some(custom)) + if token.is_keyword() && custom.contains_key(token.syntax().as_ref()) => { - // Convert into custom keywords - Some((Token::Custom(token.syntax().into()), pos)) + if disabled.contains(token.syntax().as_ref()) { + // Disabled standard keyword + Some((Token::Custom(token.syntax().into()), pos)) + } else { + // Active standard keyword - should never be a custom keyword! + unreachable!() + } } + // Disabled operator (Some((token, pos)), Some(disabled), _) if token.is_operator() && disabled.contains(token.syntax().as_ref()) => { - // Convert disallowed operators into lex errors Some(( Token::LexError(Box::new(LexError::UnexpectedInput(token.syntax().into()))), pos, )) } + // Disabled standard keyword (Some((token, pos)), Some(disabled), _) if token.is_keyword() && disabled.contains(token.syntax().as_ref()) => { - // Convert disallowed keywords into identifiers - Some((Token::Identifier(token.syntax().into()), pos)) + Some((Token::Reserved(token.syntax().into()), pos)) } (r, _, _) => r, } diff --git a/tests/modules.rs b/tests/modules.rs index 3d0fc48d..b7393f14 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -1,6 +1,6 @@ #![cfg(not(feature = "no_module"))] use rhai::{ - module_resolvers::StaticModuleResolver, Engine, EvalAltResult, Module, ParseError, + module_resolvers::StaticModuleResolver, Dynamic, Engine, EvalAltResult, Module, ParseError, ParseErrorType, Scope, INT, }; @@ -26,6 +26,7 @@ fn test_module_sub_module() -> Result<(), Box> { sub_module.set_sub_module("universe", sub_module2); module.set_sub_module("life", sub_module); + module.set_var("MYSTIC_NUMBER", Dynamic::from(42 as INT)); assert!(module.contains_sub_module("life")); let m = module.get_sub_module("life").unwrap(); @@ -44,18 +45,16 @@ fn test_module_sub_module() -> Result<(), Box> { let mut engine = Engine::new(); engine.set_module_resolver(Some(resolver)); - let mut scope = Scope::new(); - assert_eq!( - engine.eval_with_scope::( - &mut scope, - r#"import "question" as q; q::life::universe::answer + 1"# - )?, + engine.eval::(r#"import "question" as q; q::MYSTIC_NUMBER"#)?, 42 ); assert_eq!( - engine.eval_with_scope::( - &mut scope, + engine.eval::(r#"import "question" as q; q::life::universe::answer + 1"#)?, + 42 + ); + assert_eq!( + engine.eval::( r#"import "question" as q; q::life::universe::inc(q::life::universe::answer)"# )?, 42 @@ -221,36 +220,30 @@ fn test_module_from_ast() -> Result<(), Box> { resolver2.insert("testing", module); engine.set_module_resolver(Some(resolver2)); - let mut scope = Scope::new(); - assert_eq!( - engine.eval_with_scope::(&mut scope, r#"import "testing" as ttt; ttt::abc"#)?, + engine.eval::(r#"import "testing" as ttt; ttt::abc"#)?, 123 ); assert_eq!( - engine.eval_with_scope::(&mut scope, r#"import "testing" as ttt; ttt::foo"#)?, + engine.eval::(r#"import "testing" as ttt; ttt::foo"#)?, 42 ); - assert!(engine - .eval_with_scope::(&mut scope, r#"import "testing" as ttt; ttt::extra::foo"#)?); + assert!(engine.eval::(r#"import "testing" as ttt; ttt::extra::foo"#)?); assert_eq!( - engine.eval_with_scope::(&mut scope, r#"import "testing" as ttt; ttt::hello"#)?, + engine.eval::(r#"import "testing" as ttt; ttt::hello"#)?, "hello, 42 worlds!" ); assert_eq!( - engine.eval_with_scope::(&mut scope, r#"import "testing" as ttt; ttt::calc(999)"#)?, + engine.eval::(r#"import "testing" as ttt; ttt::calc(999)"#)?, 1000 ); assert_eq!( - engine.eval_with_scope::( - &mut scope, - r#"import "testing" as ttt; ttt::add_len(ttt::foo, ttt::hello)"# - )?, + engine.eval::(r#"import "testing" as ttt; ttt::add_len(ttt::foo, ttt::hello)"#)?, 59 ); assert!(matches!( *engine - .eval_with_scope::<()>(&mut scope, r#"import "testing" as ttt; ttt::hidden()"#) + .consume(r#"import "testing" as ttt; ttt::hidden()"#) .expect_err("should error"), EvalAltResult::ErrorFunctionNotFound(fn_name, _) if fn_name == "ttt::hidden" )); diff --git a/tests/syntax.rs b/tests/syntax.rs index 0fdf3c07..d4737b71 100644 --- a/tests/syntax.rs +++ b/tests/syntax.rs @@ -1,16 +1,25 @@ #![cfg(feature = "internals")] use rhai::{ - Dynamic, Engine, EvalAltResult, EvalState, Expression, Imports, LexError, Module, Scope, INT, + Dynamic, Engine, EvalAltResult, EvalState, Expression, Imports, LexError, Module, ParseError, + ParseErrorType, Scope, INT, }; #[test] fn test_custom_syntax() -> Result<(), Box> { let mut engine = Engine::new(); + engine.consume("while false {}")?; + // Disable 'while' and make sure it still works with custom syntax engine.disable_symbol("while"); - engine.consume("while false {}").expect_err("should error"); - engine.consume("let while = 0")?; + assert!(matches!( + *engine.compile("while false {}").expect_err("should error").0, + ParseErrorType::Reserved(err) if err == "while" + )); + assert!(matches!( + *engine.compile("let while = 0").expect_err("should error").0, + ParseErrorType::Reserved(err) if err == "while" + )); engine .register_custom_syntax( diff --git a/tests/tokens.rs b/tests/tokens.rs index f54afa84..523beab7 100644 --- a/tests/tokens.rs +++ b/tests/tokens.rs @@ -7,8 +7,11 @@ fn test_tokens_disabled() { engine.disable_symbol("if"); // disable the 'if' keyword assert!(matches!( - *engine.compile("let x = if true { 42 } else { 0 };").expect_err("should error").0, - ParseErrorType::MissingToken(ref token, _) if token == ";" + *engine + .compile("let x = if true { 42 } else { 0 };") + .expect_err("should error") + .0, + ParseErrorType::Reserved(err) if err == "if" )); engine.disable_symbol("+="); // disable the '+=' operator From 2f33edb7620b5b0b1ff98dcbb93ca091419eeedc Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 19 Jul 2020 09:10:22 +0800 Subject: [PATCH 6/6] FIx no_std. --- doc/src/start/builds/minimal.md | 4 ++-- doc/src/start/builds/no-std.md | 6 ++++++ src/parser.rs | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/doc/src/start/builds/minimal.md b/doc/src/start/builds/minimal.md index d1da0f01..46b85007 100644 --- a/doc/src/start/builds/minimal.md +++ b/doc/src/start/builds/minimal.md @@ -31,8 +31,8 @@ Opt-Out of Features ------------------ Opt out of as many features as possible, if they are not needed, to reduce code size because, remember, by default -all code is compiled in as what a script requires cannot be predicted. If a language feature is not needed, -omitting them via special features is a prudent strategy to optimize the build for size. +all code is compiled into the final binary since what a script requires cannot be predicted. +If a language feature will never be needed, omitting it is a prudent strategy to optimize the build for size. Omitting arrays ([`no_index`]) yields the most code-size savings, followed by floating-point support ([`no_float`]), checked arithmetic/script resource limits ([`unchecked`]) and finally object maps and custom types ([`no_object`]). diff --git a/doc/src/start/builds/no-std.md b/doc/src/start/builds/no-std.md index 22b9b025..46a8e986 100644 --- a/doc/src/start/builds/no-std.md +++ b/doc/src/start/builds/no-std.md @@ -7,3 +7,9 @@ The feature [`no_std`] automatically converts the scripting engine into a `no-st Usually, a `no-std` build goes hand-in-hand with [minimal builds] because typical embedded hardware (the primary target for `no-std`) has limited storage. + + +Nightly Required +---------------- + +Currently, [`no_std`] requires the nightly compiler due to the crates that it uses. diff --git a/src/parser.rs b/src/parser.rs index 3f79d089..a256deef 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -25,7 +25,7 @@ use crate::stdlib::{ char, collections::HashMap, fmt, format, - hash::Hash, + hash::{Hash, Hasher}, iter::empty, mem, num::NonZeroUsize, @@ -674,7 +674,7 @@ impl Default for Expr { } impl Hash for Expr { - fn hash(&self, state: &mut H) { + fn hash(&self, state: &mut H) { match self { Self::FloatConstant(x) => { state.write(&x.0.to_le_bytes());