From 3ae7cf4018a8017ecb0da2a750771766107e5054 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 17 Jul 2020 14:50:23 +0800 Subject: [PATCH 01/11] 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 02/11] 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()); From cf36dc5a57d8d060e41e59e3093ea8059356781e Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 19 Jul 2020 17:14:55 +0800 Subject: [PATCH 03/11] Support for anonymous functions. --- RELEASES.md | 5 + doc/src/language/fn-ptr.md | 57 +++++++- doc/src/language/object-maps-oop.md | 16 +- doc/src/language/oop.md | 21 ++- doc/src/links.md | 2 + src/any.rs | 1 + src/engine.rs | 23 ++- src/error.rs | 3 + src/module.rs | 2 +- src/optimize.rs | 20 ++- src/parser.rs | 218 ++++++++++++++++++++++------ 11 files changed, 294 insertions(+), 74 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 9a48b709..b09e10ce 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,12 +4,17 @@ Rhai Release Notes Version 0.18.0 ============== +This version adds: + +* Anonymous functions (in closure syntax). Simplifies creation of ad hoc functions. + 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`. +* Anonymous functions in the syntax of a closure, e.g. `|x, y, z| x + y - z`. Version 0.17.0 diff --git a/doc/src/language/fn-ptr.md b/doc/src/language/fn-ptr.md index 57f2f24f..1552a46a 100644 --- a/doc/src/language/fn-ptr.md +++ b/doc/src/language/fn-ptr.md @@ -163,4 +163,59 @@ 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`]. +Therefore, obviously, binding the `this` pointer is unsupported under [`no_object`]. + + +Anonymous Functions +------------------- + +Sometimes it gets tedious to define separate functions only to dispatch them via single function pointers. +This scenario is especially common when simulating object-oriented programming ([OOP]). + +```rust +// Define object +let obj = #{ + data: 42, + increment: Fn("inc_obj"), // use function pointers to + decrement: Fn("dec_obj"), // refer to method functions + print: Fn("print_obj") +}; + +// Define method functions one-by-one +fn inc_obj(x) { this.data += x; } +fn dec_obj(x) { this.data -= x; } +fn print_obj() { print(this.data); } +``` + +The above can be replaced by using _anonymous functions_ which have the same syntax as Rust's closures +(but they are **NOT** closures, merely syntactic sugar): + +```rust +let obj = #{ + data: 42, + increment: |x| this.data += x, // one-liner + decrement: |x| this.data -= x, + print_obj: || { print(this.data); } // full function body +}; +``` + +The anonymous functions will be expanded into separate functions in the global namespace: + +```rust +let obj = #{ + data: 42, + increment: Fn("anon_fn_1000"), + decrement: Fn("anon_fn_1001"), + print: Fn("anon_fn_1002") +}; + +fn anon_fn_1000(x) { this.data += x; } +fn anon_fn_1001(x) { this.data -= x; } +fn anon_fn_1002() { print this.data; } +``` + +### WARNING - NOT Closures + +Remember: anonymous functions, though having the same syntax as Rust _closures_, are themselves +**not** closures. In particular, they do not capture their running environment. They are more like +Rust's function pointers. diff --git a/doc/src/language/object-maps-oop.md b/doc/src/language/object-maps-oop.md index 4727871b..c93f6847 100644 --- a/doc/src/language/object-maps-oop.md +++ b/doc/src/language/object-maps-oop.md @@ -20,17 +20,21 @@ to the [object map] before the function is called. There is no way to simulate via a normal function-call syntax because all scripted function arguments are passed by value. ```rust -fn do_action(x) { print(this.data + x); } // 'this' binds to the object when called +fn do_action(x) { this.data += x; } // 'this' binds to the object when called let obj = #{ data: 40, - action: Fn("do_action") // 'action' holds a function pointer to 'do_action' + action: Fn("do_action") // 'action' holds a function pointer to 'do_action' }; -obj.action(2); // Short-hand syntax: prints 42 +obj.action(2); // Calls 'do_action' with `this` bound to 'obj' -// To achieve the above with normal function pointer calls: -fn do_action(map, x) { print(map.data + x); } +obj.call(obj.action, 2); // The above de-sugars to this -obj.action.call(obj, 2); // this call cannot mutate 'obj' +obj.data == 42; + +// To achieve the above with normal function pointer call will fail. +fn do_action(map, x) { map.data += x; } // 'map' is a copy + +obj.action.call(obj, 2); // 'obj' is passed as a copy by value ``` diff --git a/doc/src/language/oop.md b/doc/src/language/oop.md index 120b42d0..e34a8ab2 100644 --- a/doc/src/language/oop.md +++ b/doc/src/language/oop.md @@ -17,23 +17,22 @@ Rhai's [object maps] has [special support for OOP]({{rootUrl}}/language/object-m | [Object map] properties holding values | properties | | [Object map] properties that hold [function pointers] | methods | +When a property of an [object map] is called like a method function, and if it happens to hold +a valid [function pointer] (perhaps defined via an [anonymous function]), then the call will be +dispatched to the actual function with `this` binding to the [object map] itself. Examples -------- ```rust // Define the object -let obj = #{ - data: 0, - increment: Fn("add"), // when called, 'this' binds to 'obj' - update: Fn("update"), // when called, 'this' binds to 'obj' - action: Fn("action") // when called, 'this' binds to 'obj' - }; - -// Define functions -fn add(x) { this.data += x; } // update using 'this' -fn update(x) { this.data = x; } // update using 'this' -fn action() { print(this.data); } // access properties of 'this' +let obj = + #{ + data: 0, + increment: |x| this.data += x, // when called, 'this' binds to 'obj' + update: |x| this.data = x, // when called, 'this' binds to 'obj' + action: || print(this.data) // when called, 'this' binds to 'obj' + }; // Use the object obj.increment(1); diff --git a/doc/src/links.md b/doc/src/links.md index 82ceab3b..4cdb83e8 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -78,6 +78,8 @@ [function pointers]: {{rootUrl}}/language/fn-ptr.md [function namespace]: {{rootUrl}}/language/fn-namespaces.md [function namespaces]: {{rootUrl}}/language/fn-namespaces.md +[anonymous function]: {{rootUrl}}/language/fn-ptr.md#anonymous-functions +[anonymous functions]: {{rootUrl}}/language/fn-ptr.md#anonymous-functions [`Module`]: {{rootUrl}}/language/modules/index.md [module]: {{rootUrl}}/language/modules/index.md diff --git a/src/any.rs b/src/any.rs index 5182da08..4a330cff 100644 --- a/src/any.rs +++ b/src/any.rs @@ -18,6 +18,7 @@ use crate::stdlib::{ boxed::Box, collections::HashMap, fmt, + hash::Hash, string::String, vec::Vec, }; diff --git a/src/engine.rs b/src/engine.rs index dd1879cc..16205a2f 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -87,6 +87,7 @@ pub const FN_GET: &str = "get$"; pub const FN_SET: &str = "set$"; pub const FN_IDX_GET: &str = "index$get$"; pub const FN_IDX_SET: &str = "index$set$"; +pub const FN_ANONYMOUS: &str = "anon$"; #[cfg(feature = "internals")] pub const MARKER_EXPR: &str = "$expr$"; @@ -654,7 +655,7 @@ impl Engine { args: &mut FnCallArgs, is_ref: bool, is_method: bool, - def_val: Option<&Dynamic>, + def_val: Option, level: usize, ) -> Result<(Dynamic, bool), Box> { self.inc_operations(state)?; @@ -815,7 +816,7 @@ impl Engine { // Return default value (if any) if let Some(val) = def_val { - return Ok((val.clone(), false)); + return Ok((val.into(), false)); } // Getter function not found? @@ -976,7 +977,7 @@ impl Engine { args: &mut FnCallArgs, is_ref: bool, is_method: bool, - def_val: Option<&Dynamic>, + def_val: Option, level: usize, ) -> Result<(Dynamic, bool), Box> { // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. @@ -1081,7 +1082,6 @@ impl Engine { let is_ref = target.is_ref(); let is_value = target.is_value(); - let def_val = def_val.as_ref(); // Get a reference to the mutation target Dynamic let obj = target.as_mut(); @@ -1100,7 +1100,7 @@ impl Engine { // Map it to name(args) in function-call style self.exec_fn_call( - state, lib, fn_name, *native, hash, args, false, false, def_val, level, + 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 @@ -1120,7 +1120,7 @@ impl Engine { // 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, + state, lib, &fn_name, *native, hash, args, is_ref, true, *def_val, level, ) } else { let redirected: Option; @@ -1146,7 +1146,7 @@ impl Engine { let args = arg_values.as_mut(); self.exec_fn_call( - state, lib, fn_name, *native, hash, args, is_ref, true, def_val, level, + state, lib, fn_name, *native, hash, args, is_ref, true, *def_val, level, ) } .map_err(|err| err.new_position(*pos))?; @@ -1694,13 +1694,12 @@ impl Engine { #[cfg(not(feature = "no_index"))] 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() { + let def_value = Some(false); let args = &mut [&mut lhs_value.clone(), value]; - let def_value = Some(&def_value); let hashes = ( // Qualifiers (none) + function name + number of arguments + argument `TypeId`'s. @@ -1782,6 +1781,7 @@ impl Engine { Expr::FloatConstant(x) => Ok(x.0.into()), Expr::StringConstant(x) => Ok(x.0.to_string().into()), Expr::CharConstant(x) => Ok(x.0.into()), + Expr::FnPointer(x) => Ok(FnPtr::new_unchecked(x.0.clone()).into()), Expr::Variable(x) if (x.0).0 == KEYWORD_THIS => { if let Some(ref val) = this_ptr { Ok((*val).clone()) @@ -1941,7 +1941,6 @@ impl Engine { // Normal function call Expr::FnCall(x) if x.1.is_none() => { let ((name, native, pos), _, hash, args_expr, def_val) = x.as_ref(); - let def_val = def_val.as_ref(); // Handle Fn() if name == KEYWORD_FN_PTR && args_expr.len() == 1 { @@ -2072,7 +2071,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)) @@ -2166,7 +2165,7 @@ impl Engine { .map_err(|err| err.new_position(*pos)), Err(err) => match *err { EvalAltResult::ErrorFunctionNotFound(_, _) if def_val.is_some() => { - Ok(def_val.clone().unwrap()) + Ok(def_val.unwrap().into()) } EvalAltResult::ErrorFunctionNotFound(_, _) => { Err(Box::new(EvalAltResult::ErrorFunctionNotFound( diff --git a/src/error.rs b/src/error.rs index 1f4b08bf..86c49091 100644 --- a/src/error.rs +++ b/src/error.rs @@ -206,6 +206,9 @@ impl fmt::Display for ParseErrorType { Self::FnMissingParams(s) => write!(f, "Expecting parameters for function '{}'", s), + Self::FnMissingBody(s) if s.is_empty() => { + f.write_str("Expecting body statement block for anonymous function") + } Self::FnMissingBody(s) => { write!(f, "Expecting body statement block for function '{}'", s) } diff --git a/src/module.rs b/src/module.rs index 5a6a604c..3ee25995 100644 --- a/src/module.rs +++ b/src/module.rs @@ -1098,7 +1098,7 @@ impl Module { /// /// A `StaticVec` is used because most module-level access contains only one level, /// and it is wasteful to always allocate a `Vec` with one element. -#[derive(Clone, Eq, PartialEq, Default)] +#[derive(Clone, Eq, PartialEq, Default, Hash)] pub struct ModuleRef(StaticVec<(String, Position)>, Option); impl fmt::Debug for ModuleRef { diff --git a/src/optimize.rs b/src/optimize.rs index 3da46ffa..cbfbffe0 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,9 +1,12 @@ use crate::any::Dynamic; use crate::calc_fn_hash; -use crate::engine::{Engine, Imports, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_PRINT, KEYWORD_TYPE_OF}; +use crate::engine::{ + Engine, Imports, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_PRINT, KEYWORD_TYPE_OF, +}; use crate::module::Module; use crate::parser::{map_dynamic_to_expr, Expr, ReturnType, ScriptFnDef, Stmt, AST}; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; +use crate::token::is_valid_identifier; use crate::utils::StaticVec; #[cfg(feature = "internals")] @@ -528,6 +531,19 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { Expr::FnCall(x) } + // Fn("...") + Expr::FnCall(x) + if x.1.is_none() + && (x.0).0 == KEYWORD_FN_PTR + && x.3.len() == 1 + && matches!(x.3[0], Expr::StringConstant(_)) + => { + match &x.3[0] { + Expr::StringConstant(s) if is_valid_identifier(s.0.chars()) => Expr::FnPointer(s.clone()), + _ => Expr::FnCall(x) + } + } + // Eagerly call functions Expr::FnCall(mut x) if x.1.is_none() // Non-qualified @@ -571,7 +587,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { Some(arg_for_type_of.to_string().into()) } else { // Otherwise use the default value, if any - def_value.clone() + def_value.map(|v| v.into()) } }) .and_then(|result| map_dynamic_to_expr(result, *pos)) diff --git a/src/parser.rs b/src/parser.rs index a256deef..610bc6e2 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,7 +2,7 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; -use crate::engine::{make_getter, make_setter, Engine, KEYWORD_THIS}; +use crate::engine::{make_getter, make_setter, Engine, FN_ANONYMOUS, KEYWORD_THIS}; use crate::error::{LexError, ParseError, ParseErrorType}; use crate::module::{Module, ModuleRef}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; @@ -35,6 +35,12 @@ use crate::stdlib::{ vec::Vec, }; +#[cfg(not(feature = "no_std"))] +use crate::stdlib::collections::hash_map::DefaultHasher; + +#[cfg(feature = "no_std")] +use ahash::AHasher; + /// The system integer type. /// /// If the `only_i32` feature is enabled, this will be `i32` instead. @@ -598,21 +604,34 @@ impl Hash for CustomExpr { } } +#[cfg(not(feature = "no_float"))] +#[derive(Debug, PartialEq, PartialOrd, Clone)] +pub struct FloatWrapper(pub FLOAT, pub Position); + +impl Hash for FloatWrapper { + fn hash(&self, state: &mut H) { + state.write(&self.0.to_le_bytes()); + self.1.hash(state); + } +} + /// An expression. /// /// 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 Expr { /// Integer constant. IntegerConstant(Box<(INT, Position)>), /// Floating-point constant. #[cfg(not(feature = "no_float"))] - FloatConstant(Box<(FLOAT, Position)>), + FloatConstant(Box), /// Character constant. CharConstant(Box<(char, Position)>), /// String constant. StringConstant(Box<(ImmutableString, Position)>), + /// FnPtr constant. + FnPointer(Box<(ImmutableString, Position)>), /// Variable access - ((variable name, position), optional modules, hash, optional index) Variable( Box<( @@ -637,7 +656,7 @@ pub enum Expr { Option>, u64, StaticVec, - Option, + Option, )>, ), /// expr op= expr @@ -673,18 +692,6 @@ 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. /// @@ -758,6 +765,7 @@ impl Expr { Self::IntegerConstant(x) => x.1, Self::CharConstant(x) => x.1, Self::StringConstant(x) => x.1, + Self::FnPointer(x) => x.1, Self::Array(x) => x.1, Self::Map(x) => x.1, Self::Property(x) => x.1, @@ -791,6 +799,7 @@ impl Expr { Self::IntegerConstant(x) => x.1 = new_pos, Self::CharConstant(x) => x.1 = new_pos, Self::StringConstant(x) => x.1 = new_pos, + Self::FnPointer(x) => x.1 = new_pos, Self::Array(x) => x.1 = new_pos, Self::Map(x) => x.1 = new_pos, Self::Variable(x) => (x.0).1 = new_pos, @@ -847,6 +856,7 @@ impl Expr { Self::IntegerConstant(_) | Self::CharConstant(_) | Self::StringConstant(_) + | Self::FnPointer(_) | Self::True(_) | Self::False(_) | Self::Unit(_) => true, @@ -878,6 +888,7 @@ impl Expr { Self::IntegerConstant(_) | Self::CharConstant(_) + | Self::FnPointer(_) | Self::In(_) | Self::And(_) | Self::Or(_) @@ -1476,7 +1487,7 @@ fn parse_primary( let mut root_expr = match token { Token::IntegerConstant(x) => Expr::IntegerConstant(Box::new((x, settings.pos))), #[cfg(not(feature = "no_float"))] - Token::FloatConstant(x) => Expr::FloatConstant(Box::new((x, settings.pos))), + Token::FloatConstant(x) => Expr::FloatConstant(Box::new(FloatWrapper(x, settings.pos))), Token::CharConstant(c) => Expr::CharConstant(Box::new((c, settings.pos))), Token::StringConstant(s) => Expr::StringConstant(Box::new((s.into(), settings.pos))), Token::Identifier(s) => { @@ -1616,7 +1627,10 @@ fn parse_unary( .map(|i| Expr::IntegerConstant(Box::new((i, pos)))) .or_else(|| { #[cfg(not(feature = "no_float"))] - return Some(Expr::FloatConstant(Box::new((-(x.0 as FLOAT), pos)))); + return Some(Expr::FloatConstant(Box::new(FloatWrapper( + -(x.0 as FLOAT), + pos, + )))); #[cfg(feature = "no_float")] return None; }) @@ -1625,7 +1639,9 @@ fn parse_unary( // Negative float #[cfg(not(feature = "no_float"))] - Expr::FloatConstant(x) => Ok(Expr::FloatConstant(Box::new((-x.0, x.1)))), + Expr::FloatConstant(x) => { + Ok(Expr::FloatConstant(Box::new(FloatWrapper(-x.0, x.1)))) + } // Call negative function expr => { @@ -1664,9 +1680,38 @@ fn parse_unary( None, hash, args, - Some(false.into()), // NOT operator, when operating on invalid operand, defaults to false + Some(false), // NOT operator, when operating on invalid operand, defaults to false )))) } + // | ... + #[cfg(not(feature = "no_function"))] + Token::Pipe | Token::Or => { + let mut state = ParseState::new( + state.engine, + state.max_function_expr_depth, + state.max_function_expr_depth, + ); + + let settings = ParseSettings { + allow_if_expr: true, + allow_stmt_expr: true, + allow_anonymous_fn: true, + is_global: false, + is_function_scope: true, + is_breakable: false, + level: 0, + pos: *token_pos, + }; + + let (expr, func) = parse_anon_fn(input, &mut state, lib, settings)?; + + // Qualifiers (none) + function name + number of arguments. + let hash = calc_fn_hash(empty(), &func.name, func.params.len(), empty()); + + lib.insert(hash, func); + + Ok(expr) + } // Token::EOF => Err(PERR::UnexpectedEOF.into_err(settings.pos)), // All other tokens @@ -2018,7 +2063,7 @@ fn parse_binary_op( settings.pos = pos; settings.ensure_level_within_max_limit(state.max_expr_depth)?; - let cmp_def = Some(false.into()); + let cmp_def = Some(false); let op = op_token.syntax(); let hash = calc_fn_hash(empty(), &op, 2, empty()); let op = (op, true, pos); @@ -2041,7 +2086,7 @@ fn parse_binary_op( | Token::XOr => Expr::FnCall(Box::new((op, None, hash, args, None))), // '!=' defaults to true when passed invalid operands - Token::NotEqualsTo => Expr::FnCall(Box::new((op, None, hash, args, Some(true.into())))), + Token::NotEqualsTo => Expr::FnCall(Box::new((op, None, hash, args, Some(true)))), // Comparison operators default to false when passed invalid operands Token::EqualsTo @@ -2762,32 +2807,28 @@ fn parse_fn( let mut params = Vec::new(); if !match_token(input, Token::RightParen)? { - let end_err = format!("to close the parameters list of function '{}'", name); let sep_err = format!("to separate the parameters of function '{}'", name); loop { - match input.peek().unwrap() { - (Token::RightParen, _) => (), - _ => match input.next().unwrap() { - (Token::Identifier(s), pos) => { - state.stack.push((s.clone(), ScopeEntryType::Normal)); - params.push((s, pos)) - } - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (_, pos) => { - return Err( - PERR::MissingToken(Token::RightParen.into(), end_err).into_err(pos) - ) - } - }, + match input.next().unwrap() { + (Token::RightParen, _) => break, + (Token::Identifier(s), pos) => { + state.stack.push((s.clone(), ScopeEntryType::Normal)); + params.push((s, pos)) + } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) => { + return Err(PERR::MissingToken( + Token::RightParen.into(), + format!("to close the parameters list of function '{}'", name), + ) + .into_err(pos)) + } } match input.next().unwrap() { (Token::RightParen, _) => break, (Token::Comma, _) => (), - (Token::Identifier(_), pos) => { - return Err(PERR::MissingToken(Token::Comma.into(), sep_err).into_err(pos)) - } (Token::LexError(err), pos) => return Err(err.into_err(pos)), (_, pos) => { return Err(PERR::MissingToken(Token::Comma.into(), sep_err).into_err(pos)) @@ -2831,6 +2872,101 @@ fn parse_fn( }) } +/// Parse an anonymous function definition. +#[cfg(not(feature = "no_function"))] +fn parse_anon_fn( + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FunctionsLib, + mut settings: ParseSettings, +) -> Result<(Expr, ScriptFnDef), ParseError> { + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + let mut params = Vec::new(); + + if input.next().unwrap().0 != Token::Or { + if !match_token(input, Token::Pipe)? { + loop { + match input.next().unwrap() { + (Token::Pipe, _) => break, + (Token::Identifier(s), pos) => { + state.stack.push((s.clone(), ScopeEntryType::Normal)); + params.push((s, pos)) + } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) => { + return Err(PERR::MissingToken( + Token::Pipe.into(), + "to close the parameters list of anonymous function".into(), + ) + .into_err(pos)) + } + } + + match input.next().unwrap() { + (Token::Pipe, _) => break, + (Token::Comma, _) => (), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (_, pos) => { + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the parameters of anonymous function".into(), + ) + .into_err(pos)) + } + } + } + } + } + + // Check for duplicating parameters + params + .iter() + .enumerate() + .try_for_each(|(i, (p1, _))| { + params + .iter() + .skip(i + 1) + .find(|(p2, _)| p2 == p1) + .map_or_else(|| Ok(()), |(p2, pos)| Err((p2, *pos))) + }) + .map_err(|(p, pos)| PERR::FnDuplicatedParam("".to_string(), p.to_string()).into_err(pos))?; + + // Parse function body + settings.is_breakable = false; + let pos = input.peek().unwrap().1; + let body = parse_stmt(input, state, lib, settings.level_up()) + .map(|stmt| stmt.unwrap_or_else(|| Stmt::Noop(pos)))?; + + let params: StaticVec<_> = params.into_iter().map(|(p, _)| p).collect(); + + // Calculate hash + #[cfg(feature = "no_std")] + let mut s: AHasher = Default::default(); + #[cfg(not(feature = "no_std"))] + let mut s = DefaultHasher::new(); + + s.write_usize(params.len()); + params.iter().for_each(|a| a.hash(&mut s)); + body.hash(&mut s); + let hash = s.finish(); + + // Create unique function name + let fn_name = format!("{}{}", FN_ANONYMOUS, hash); + + let script = ScriptFnDef { + name: fn_name.clone(), + access: FnAccess::Public, + params, + body, + pos: settings.pos, + }; + + let expr = Expr::FnPointer(Box::new((fn_name.into(), settings.pos))); + + Ok((expr, script)) +} + impl Engine { pub(crate) fn parse_global_expr( &self, @@ -2951,7 +3087,7 @@ impl Engine { pub fn map_dynamic_to_expr(value: Dynamic, pos: Position) -> Option { match value.0 { #[cfg(not(feature = "no_float"))] - Union::Float(value) => Some(Expr::FloatConstant(Box::new((value, pos)))), + Union::Float(value) => Some(Expr::FloatConstant(Box::new(FloatWrapper(value, pos)))), Union::Unit(_) => Some(Expr::Unit(pos)), Union::Int(value) => Some(Expr::IntegerConstant(Box::new((value, pos)))), From 686d40d4ae10005480e8cb34f827d582dc46483e Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 19 Jul 2020 21:18:13 +0800 Subject: [PATCH 04/11] Move anonymous function to own chapter. --- doc/src/SUMMARY.md | 1 + doc/src/language/fn-anon.md | 57 +++++++++++++++++++++++++++++++++++++ doc/src/language/fn-ptr.md | 55 ----------------------------------- doc/src/links.md | 4 +-- 4 files changed, 60 insertions(+), 57 deletions(-) create mode 100644 doc/src/language/fn-anon.md diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index e344c306..d86aae6a 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -75,6 +75,7 @@ The Rhai Scripting Language 2. [Overloading](language/overload.md) 3. [Namespaces](language/fn-namespaces.md) 4. [Function Pointers](language/fn-ptr.md) + 5. [Anonymous Functions](language/fn-anon.md) 15. [Print and Debug](language/print-debug.md) 16. [Modules](language/modules/index.md) 1. [Export Variables, Functions and Sub-Modules](language/modules/export.md) diff --git a/doc/src/language/fn-anon.md b/doc/src/language/fn-anon.md new file mode 100644 index 00000000..21ceb7a3 --- /dev/null +++ b/doc/src/language/fn-anon.md @@ -0,0 +1,57 @@ +Anonymous Functions +=================== + +{{#include ../links.md}} + +Sometimes it gets tedious to define separate functions only to dispatch them via single [function pointers]. +This scenario is especially common when simulating object-oriented programming ([OOP]). + +```rust +// Define object +let obj = #{ + data: 42, + increment: Fn("inc_obj"), // use function pointers to + decrement: Fn("dec_obj"), // refer to method functions + print: Fn("print_obj") +}; + +// Define method functions one-by-one +fn inc_obj(x) { this.data += x; } +fn dec_obj(x) { this.data -= x; } +fn print_obj() { print(this.data); } +``` + +The above can be replaced by using _anonymous functions_ which have the same syntax as Rust's closures +(but they are **NOT** closures, merely syntactic sugar): + +```rust +let obj = #{ + data: 42, + increment: |x| this.data += x, // one-liner + decrement: |x| this.data -= x, + print_obj: || { print(this.data); } // full function body +}; +``` + +The anonymous functions will be hoisted into separate functions in the global namespace. +The above is equivalent to: + +```rust +let obj = #{ + data: 42, + increment: Fn("anon_fn_1000"), + decrement: Fn("anon_fn_1001"), + print: Fn("anon_fn_1002") +}; + +fn anon_fn_1000(x) { this.data += x; } +fn anon_fn_1001(x) { this.data -= x; } +fn anon_fn_1002() { print this.data; } +``` + +WARNING - NOT Closures +---------------------- + +Remember: anonymous functions, though having the same syntax as Rust _closures_, are themselves +**not** closures. In particular, they do not capture their running environment. They are more like +Rust's function pointers. diff --git a/doc/src/language/fn-ptr.md b/doc/src/language/fn-ptr.md index 1552a46a..833cfaa2 100644 --- a/doc/src/language/fn-ptr.md +++ b/doc/src/language/fn-ptr.md @@ -164,58 +164,3 @@ Beware that this only works for _method-call_ style. Normal function-call style the `this` pointer (for syntactic reasons). Therefore, obviously, binding the `this` pointer is unsupported under [`no_object`]. - - -Anonymous Functions -------------------- - -Sometimes it gets tedious to define separate functions only to dispatch them via single function pointers. -This scenario is especially common when simulating object-oriented programming ([OOP]). - -```rust -// Define object -let obj = #{ - data: 42, - increment: Fn("inc_obj"), // use function pointers to - decrement: Fn("dec_obj"), // refer to method functions - print: Fn("print_obj") -}; - -// Define method functions one-by-one -fn inc_obj(x) { this.data += x; } -fn dec_obj(x) { this.data -= x; } -fn print_obj() { print(this.data); } -``` - -The above can be replaced by using _anonymous functions_ which have the same syntax as Rust's closures -(but they are **NOT** closures, merely syntactic sugar): - -```rust -let obj = #{ - data: 42, - increment: |x| this.data += x, // one-liner - decrement: |x| this.data -= x, - print_obj: || { print(this.data); } // full function body -}; -``` - -The anonymous functions will be expanded into separate functions in the global namespace: - -```rust -let obj = #{ - data: 42, - increment: Fn("anon_fn_1000"), - decrement: Fn("anon_fn_1001"), - print: Fn("anon_fn_1002") -}; - -fn anon_fn_1000(x) { this.data += x; } -fn anon_fn_1001(x) { this.data -= x; } -fn anon_fn_1002() { print this.data; } -``` - -### WARNING - NOT Closures - -Remember: anonymous functions, though having the same syntax as Rust _closures_, are themselves -**not** closures. In particular, they do not capture their running environment. They are more like -Rust's function pointers. diff --git a/doc/src/links.md b/doc/src/links.md index 4cdb83e8..a26a34e8 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -78,8 +78,8 @@ [function pointers]: {{rootUrl}}/language/fn-ptr.md [function namespace]: {{rootUrl}}/language/fn-namespaces.md [function namespaces]: {{rootUrl}}/language/fn-namespaces.md -[anonymous function]: {{rootUrl}}/language/fn-ptr.md#anonymous-functions -[anonymous functions]: {{rootUrl}}/language/fn-ptr.md#anonymous-functions +[anonymous function]: {{rootUrl}}/language/fn-anon.md +[anonymous functions]: {{rootUrl}}/language/fn-anon.md [`Module`]: {{rootUrl}}/language/modules/index.md [module]: {{rootUrl}}/language/modules/index.md From 3b9422ea3c93fa42f1e493e39a80d1e57d7fa0e1 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 19 Jul 2020 21:36:16 +0800 Subject: [PATCH 05/11] Adddefault_features = false for no_std. --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index fe72ee21..f6ff2b03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ optional = true [dependencies.core-error] version = "0.0.0" +default_features = false features = ["alloc"] optional = true From c6caef7285b878df8c3248ebc551de0a2fbff943 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 19 Jul 2020 22:44:00 +0800 Subject: [PATCH 06/11] Set default features of serde to false. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f6ff2b03..46c208c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,9 +67,9 @@ features = ["compile-time-rng"] optional = true [dependencies.serde] -package = "serde" version = "1.0.111" -features = ["derive"] +default_features = false +features = ["derive", "alloc"] optional = true [target.'cfg(target_arch = "wasm32")'.dependencies] From 700060b6e70f44a552051135511d9586625ca2af Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 19 Jul 2020 23:32:16 +0800 Subject: [PATCH 07/11] Remove no_std example. --- doc/src/start/examples/rust.md | 1 - examples/no_std.rs | 23 ----------------------- 2 files changed, 24 deletions(-) delete mode 100644 examples/no_std.rs diff --git a/doc/src/start/examples/rust.md b/doc/src/start/examples/rust.md index b7b6a5a5..3784778d 100644 --- a/doc/src/start/examples/rust.md +++ b/doc/src/start/examples/rust.md @@ -10,7 +10,6 @@ A number of examples can be found in the `examples` folder: | [`arrays_and_structs`](https://github.com/jonathandturner/rhai/tree/master/examples/arrays_and_structs.rs) | Shows how to register a custom Rust type and using [arrays] on it. | | [`custom_types_and_methods`](https://github.com/jonathandturner/rhai/tree/master/examples/custom_types_and_methods.rs) | Shows how to register a custom Rust type and methods for it. | | [`hello`](https://github.com/jonathandturner/rhai/tree/master/examples/hello.rs) | Simple example that evaluates an expression and prints the result. | -| [`no_std`](https://github.com/jonathandturner/rhai/tree/master/examples/no_std.rs) | Example to test out `no-std` builds.
The [`no_std`] feature is required to build in `no-std`. | | [`reuse_scope`](https://github.com/jonathandturner/rhai/tree/master/examples/reuse_scope.rs) | Evaluates two pieces of code in separate runs, but using a common [`Scope`]. | | [`rhai_runner`](https://github.com/jonathandturner/rhai/tree/master/examples/rhai_runner.rs) | Runs each filename passed to it as a Rhai script. | | [`serde`](https://github.com/jonathandturner/rhai/tree/master/examples/serde.rs) | Example to serialize and deserialize Rust types with [`serde`](https://crates.io/crates/serde).
The [`serde`] feature is required to run. | diff --git a/examples/no_std.rs b/examples/no_std.rs deleted file mode 100644 index a6902f72..00000000 --- a/examples/no_std.rs +++ /dev/null @@ -1,23 +0,0 @@ -#![cfg_attr(feature = "no_std", no_std)] - -use rhai::{Engine, EvalAltResult, INT}; - -#[cfg(feature = "no_std")] -extern crate alloc; - -#[cfg(feature = "no_std")] -use alloc::boxed::Box; - -fn main() -> Result<(), Box> { - let engine = Engine::new(); - - let result = engine.eval::("40 + 2")?; - - #[cfg(not(feature = "no_std"))] - println!("Answer: {}", result); - - #[cfg(feature = "no_std")] - assert_eq!(result, 42); - - Ok(()) -} From a5fa8322e915e405d5f3120f7ac6d1caf6a96697 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 20 Jul 2020 20:23:35 +0800 Subject: [PATCH 08/11] Avoid pulling in std for no-std. --- Cargo.toml | 1 + src/packages/arithmetic.rs | 3 +++ src/packages/math_basic.rs | 3 +++ src/parser.rs | 1 + 4 files changed, 8 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 46c208c3..bf4e0dc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ codegen-units = 1 [dependencies.libm] version = "0.2.1" +default_features = false optional = true [dependencies.core-error] diff --git a/src/packages/arithmetic.rs b/src/packages/arithmetic.rs index 7a548ff0..8aeb1590 100644 --- a/src/packages/arithmetic.rs +++ b/src/packages/arithmetic.rs @@ -7,6 +7,9 @@ use crate::token::Position; #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; +#[cfg(not(feature = "no_float"))] +use num_traits::*; + use num_traits::{ identities::Zero, CheckedAdd, CheckedDiv, CheckedMul, CheckedNeg, CheckedRem, CheckedShl, CheckedShr, CheckedSub, diff --git a/src/packages/math_basic.rs b/src/packages/math_basic.rs index e7ce43b7..ca0bc374 100644 --- a/src/packages/math_basic.rs +++ b/src/packages/math_basic.rs @@ -6,6 +6,9 @@ use crate::token::Position; #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; +#[cfg(not(feature = "no_float"))] +use num_traits::*; + use crate::stdlib::{boxed::Box, format, i32, i64}; #[cfg(feature = "only_i32")] diff --git a/src/parser.rs b/src/parser.rs index 610bc6e2..ce0eb686 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -608,6 +608,7 @@ impl Hash for CustomExpr { #[derive(Debug, PartialEq, PartialOrd, Clone)] pub struct FloatWrapper(pub FLOAT, pub Position); +#[cfg(not(feature = "no_float"))] impl Hash for FloatWrapper { fn hash(&self, state: &mut H) { state.write(&self.0.to_le_bytes()); From e8b6d0143dd271ea843b35de7ecb12e5bd7d895b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 20 Jul 2020 23:23:12 +0800 Subject: [PATCH 09/11] Small fixups. --- src/any.rs | 1 - src/engine.rs | 2 +- src/module.rs | 1 + src/packages/arithmetic.rs | 1 + src/packages/math_basic.rs | 1 + tests/call_fn.rs | 3 +-- 6 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/any.rs b/src/any.rs index 4a330cff..5182da08 100644 --- a/src/any.rs +++ b/src/any.rs @@ -18,7 +18,6 @@ use crate::stdlib::{ boxed::Box, collections::HashMap, fmt, - hash::Hash, string::String, vec::Vec, }; diff --git a/src/engine.rs b/src/engine.rs index 16205a2f..0a5ec70a 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -710,7 +710,7 @@ impl Engine { /// 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); + args[0] = this_pointer; } } diff --git a/src/module.rs b/src/module.rs index 3ee25995..7216c248 100644 --- a/src/module.rs +++ b/src/module.rs @@ -30,6 +30,7 @@ use crate::stdlib::{ }; #[cfg(not(feature = "no_std"))] +#[cfg(feature = "sync")] use crate::stdlib::sync::RwLock; /// Return type of module-level Rust function. diff --git a/src/packages/arithmetic.rs b/src/packages/arithmetic.rs index 8aeb1590..5e43c05b 100644 --- a/src/packages/arithmetic.rs +++ b/src/packages/arithmetic.rs @@ -8,6 +8,7 @@ use crate::token::Position; use crate::parser::FLOAT; #[cfg(not(feature = "no_float"))] +#[cfg(feature = "no_std")] use num_traits::*; use num_traits::{ diff --git a/src/packages/math_basic.rs b/src/packages/math_basic.rs index ca0bc374..aed7e5b7 100644 --- a/src/packages/math_basic.rs +++ b/src/packages/math_basic.rs @@ -7,6 +7,7 @@ use crate::token::Position; use crate::parser::FLOAT; #[cfg(not(feature = "no_float"))] +#[cfg(feature = "no_std")] use num_traits::*; use crate::stdlib::{boxed::Box, format, i32, i64}; diff --git a/tests/call_fn.rs b/tests/call_fn.rs index a04bd614..97767df7 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -1,7 +1,6 @@ #![cfg(not(feature = "no_function"))] use rhai::{ - Dynamic, Engine, EvalAltResult, FnPtr, Func, ImmutableString, Module, ParseError, - ParseErrorType, Scope, INT, + Dynamic, Engine, EvalAltResult, FnPtr, Func, Module, ParseError, ParseErrorType, Scope, INT, }; #[test] From ec3074106e1fd39b39ba5c7e711ef52cbb4d30b0 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 21 Jul 2020 20:58:53 +0800 Subject: [PATCH 10/11] Display type name when printing Dynamic values that have no built-in format. --- src/any.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/any.rs b/src/any.rs index 5182da08..8ff20298 100644 --- a/src/any.rs +++ b/src/any.rs @@ -243,12 +243,15 @@ impl fmt::Display for Dynamic { #[cfg(not(feature = "no_index"))] Union::Array(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_object"))] - Union::Map(value) => write!(f, "#{:?}", value), + Union::Map(value) => { + f.write_str("#")?; + fmt::Debug::fmt(value, f) + } Union::FnPtr(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_std"))] Union::Variant(value) if value.is::() => write!(f, ""), - Union::Variant(_) => write!(f, "?"), + Union::Variant(value) => write!(f, "{}", (*value).type_name()), } } } @@ -266,12 +269,15 @@ impl fmt::Debug for Dynamic { #[cfg(not(feature = "no_index"))] Union::Array(value) => fmt::Debug::fmt(value, f), #[cfg(not(feature = "no_object"))] - Union::Map(value) => write!(f, "#{:?}", value), + Union::Map(value) => { + f.write_str("#")?; + fmt::Debug::fmt(value, f) + } Union::FnPtr(value) => fmt::Display::fmt(value, f), #[cfg(not(feature = "no_std"))] Union::Variant(value) if value.is::() => write!(f, ""), - Union::Variant(_) => write!(f, ""), + Union::Variant(value) => write!(f, "{}", (*value).type_name()), } } } From e6c3f8134ddabdbfc047589b3e161ed2bb7d30e5 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Tue, 21 Jul 2020 20:59:12 +0800 Subject: [PATCH 11/11] Avoid copying property name for map property access. --- src/engine.rs | 40 +++++++++++++++------------------------- src/optimize.rs | 2 +- src/parser.rs | 6 +++--- 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 0a5ec70a..1466e015 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -188,7 +188,7 @@ impl Target<'_> { } } /// Update the value of the `Target`. - /// Position in `EvalAltResult` is None and must be set afterwards. + /// Position in `EvalAltResult` is `None` and must be set afterwards. pub fn set_value(&mut self, new_val: Dynamic) -> Result<(), Box> { match self { Self::Ref(r) => **r = new_val, @@ -419,12 +419,9 @@ pub fn make_getter(id: &str) -> String { fn extract_prop_from_getter(fn_name: &str) -> Option<&str> { #[cfg(not(feature = "no_object"))] if fn_name.starts_with(FN_GET) { - Some(&fn_name[FN_GET.len()..]) - } else { - None + return Some(&fn_name[FN_GET.len()..]); } - #[cfg(feature = "no_object")] None } @@ -437,12 +434,9 @@ pub fn make_setter(id: &str) -> String { fn extract_prop_from_setter(fn_name: &str) -> Option<&str> { #[cfg(not(feature = "no_object"))] if fn_name.starts_with(FN_SET) { - Some(&fn_name[FN_SET.len()..]) - } else { - None + return Some(&fn_name[FN_SET.len()..]); } - #[cfg(feature = "no_object")] None } @@ -454,7 +448,7 @@ fn default_print(s: &str) { } /// Search for a module within an imports stack. -/// Position in `EvalAltResult` is None and must be set afterwards. +/// Position in `EvalAltResult` is `None` and must be set afterwards. fn search_imports<'s>( mods: &'s Imports, state: &mut State, @@ -487,7 +481,7 @@ fn search_imports<'s>( } /// Search for a module within an imports stack. -/// Position in `EvalAltResult` is None and must be set afterwards. +/// Position in `EvalAltResult` is `None` and must be set afterwards. fn search_imports_mut<'s>( mods: &'s mut Imports, state: &mut State, @@ -637,7 +631,7 @@ impl Engine { } /// Universal method for calling functions either registered with the `Engine` or written in Rhai. - /// Position in `EvalAltResult` is None and must be set afterwards. + /// Position in `EvalAltResult` is `None` and must be set afterwards. /// /// ## WARNING /// @@ -878,7 +872,7 @@ impl Engine { } /// Call a script-defined function. - /// Position in `EvalAltResult` is None and must be set afterwards. + /// Position in `EvalAltResult` is `None` and must be set afterwards. /// /// ## WARNING /// @@ -960,7 +954,7 @@ impl Engine { } /// Perform an actual function call, taking care of special functions - /// Position in `EvalAltResult` is None and must be set afterwards. + /// Position in `EvalAltResult` is `None` and must be set afterwards. /// /// ## WARNING /// @@ -1023,7 +1017,7 @@ impl Engine { } /// Evaluate a text string as a script - used primarily for 'eval'. - /// Position in `EvalAltResult` is None and must be set afterwards. + /// Position in `EvalAltResult` is `None` and must be set afterwards. fn eval_script_expr( &self, scope: &mut Scope, @@ -1161,7 +1155,7 @@ impl Engine { } /// Chain-evaluate a dot/index chain. - /// Position in `EvalAltResult` is None and must be set afterwards. + /// Position in `EvalAltResult` is `None` and must be set afterwards. fn eval_dot_index_chain_helper( &self, state: &mut State, @@ -1210,14 +1204,14 @@ impl Engine { } // xxx[rhs] = new_val _ if new_val.is_some() => { + let mut new_val = new_val.unwrap(); let mut idx_val2 = idx_val.clone(); match self.get_indexed_mut(state, lib, target, idx_val, pos, true, level) { // Indexed value is an owned value - the only possibility is an indexer // Try to call an index setter Ok(obj_ptr) if obj_ptr.is_value() => { - let args = - &mut [target.as_mut(), &mut idx_val2, &mut new_val.unwrap()]; + let args = &mut [target.as_mut(), &mut idx_val2, &mut new_val]; self.exec_fn_call( state, lib, FN_IDX_SET, true, 0, args, is_ref, true, None, @@ -1236,17 +1230,13 @@ impl Engine { // Indexed value is a reference - update directly Ok(ref mut obj_ptr) => { obj_ptr - .set_value(new_val.unwrap()) + .set_value(new_val) .map_err(|err| err.new_position(rhs.position()))?; } Err(err) => match *err { // No index getter - try to call an index setter EvalAltResult::ErrorIndexingType(_, _) => { - let args = &mut [ - target.as_mut(), - &mut idx_val2, - &mut new_val.unwrap(), - ]; + let args = &mut [target.as_mut(), &mut idx_val2, &mut new_val]; self.exec_fn_call( state, lib, FN_IDX_SET, true, 0, args, is_ref, true, None, @@ -2632,7 +2622,7 @@ impl Engine { } /// Check if the number of operations stay within limit. - /// Position in `EvalAltResult` is None and must be set afterwards. + /// Position in `EvalAltResult` is `None` and must be set afterwards. fn inc_operations(&self, state: &mut State) -> Result<(), Box> { state.operations += 1; diff --git a/src/optimize.rs b/src/optimize.rs index cbfbffe0..945040ea 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -400,7 +400,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { // All other items can be thrown away. state.set_dirty(); let pos = m.1; - m.0.into_iter().find(|((name, _), _)| name.as_str() == prop) + m.0.into_iter().find(|((name, _), _)| name.as_str() == prop.as_str()) .map(|(_, expr)| expr.set_position(pos)) .unwrap_or_else(|| Expr::Unit(pos)) } diff --git a/src/parser.rs b/src/parser.rs index ce0eb686..b3ed0756 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -643,7 +643,7 @@ pub enum Expr { )>, ), /// Property access. - Property(Box<((String, String, String), Position)>), + Property(Box<((ImmutableString, String, String), Position)>), /// { stmt } Stmt(Box<(Stmt, Position)>), /// Wrapped expression - should not be optimized away. @@ -937,7 +937,7 @@ impl Expr { let (name, pos) = x.0; let getter = make_getter(&name); let setter = make_setter(&name); - Self::Property(Box::new(((name.clone(), getter, setter), pos))) + Self::Property(Box::new(((name.into(), getter, setter), pos))) } _ => self, } @@ -1834,7 +1834,7 @@ fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result