From 439053b153edcc28633e099213558f8123a28b2c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sat, 30 May 2020 22:25:01 +0800 Subject: [PATCH 01/12] Refine write-up on functions overloading. --- README.md | 71 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 97c65279..a7b129f0 100644 --- a/README.md +++ b/README.md @@ -704,11 +704,16 @@ let x = (42_i64).into(); // 'into()' works for standard t let y = Dynamic::from(String::from("hello!")); // remember &str is not supported by Rhai ``` +Functions registered with the [`Engine`] can be _overloaded_ as long as the _signature_ is unique, +i.e. different functions can have the same name as long as their parameters are of different types +and/or different number. +New definitions _overwrite_ previous definitions of the same name and same number/types of parameters. + Generic functions ----------------- Rust generic functions can be used in Rhai, but separate instances for each concrete type must be registered separately. -This is essentially function overloading (Rhai does not natively support generics). +This essentially overloads the function with different parameter types (Rhai does not natively support generics). ```rust use std::fmt::Display; @@ -729,8 +734,8 @@ fn main() } ``` -This example shows how to register multiple functions (or, in this case, multiple overloaded versions of the same function) -under the same name. This enables function overloading based on the number and types of parameters. +The above example shows how to register multiple functions (or, in this case, multiple overloaded versions of the same function) +under the same name. Fallible functions ------------------ @@ -773,7 +778,8 @@ fn main() Overriding built-in functions ---------------------------- -Any similarly-named function defined in a script overrides any built-in function. +Any similarly-named function defined in a script overrides any built-in function and any registered +native Rust function of the same name and number of parameters. ```rust // Override the built-in function 'to_int' @@ -784,11 +790,13 @@ fn to_int(num) { print(to_int(123)); // what happens? ``` +A registered function, in turn, overrides any built-in function of the same name and number/types of parameters. + Operator overloading -------------------- In Rhai, a lot of functionalities are actually implemented as functions, including basic operations such as arithmetic calculations. -For example, in the expression "`a + b`", the `+` operator is _not_ built-in, but calls a function named "`+`" instead! +For example, in the expression "`a + b`", the `+` operator is _not_ built in, but calls a function named "`+`" instead! ```rust let x = a + b; @@ -801,7 +809,7 @@ overriding them has no effect at all. Operator functions cannot be defined as a script function (because operators syntax are not valid function names). However, operator functions _can_ be registered to the [`Engine`] via the methods `Engine::register_fn`, `Engine::register_result_fn` etc. -When a custom operator function is registered with the same name as an operator, it _overloads_ (or overrides) the built-in version. +When a custom operator function is registered with the same name as an operator, it overrides the built-in version. ```rust use rhai::{Engine, EvalAltResult, RegisterFn}; @@ -828,7 +836,7 @@ let result: i64 = engine.eval("1 + 1.0"); // prints 2.0 (normally an e ``` Use operator overloading for custom types (described below) only. -Be very careful when overloading built-in operators because script authors expect standard operators to behave in a +Be very careful when overriding built-in operators because script authors expect standard operators to behave in a consistent and predictable manner, and will be annoyed if a calculation for '`+`' turns into a subtraction, for example. Operator overloading also impacts script optimization when using [`OptimizationLevel::Full`]. @@ -915,7 +923,7 @@ engine.register_fn("update", TestStruct::update); // registers 'update(&mut Te engine.register_fn("new_ts", TestStruct::new); // registers 'new()' ``` -The custom type is then ready for us in scripts. Scripts can see the functions and methods registered earlier. +The custom type is then ready for use in scripts. Scripts can see the functions and methods registered earlier. Get the evaluation result back out from script-land just as before, this time casting to the custom type: ```rust @@ -1745,10 +1753,10 @@ The Rust type of a timestamp is `std::time::Instant`. [`type_of()`] a timestamp The following methods (defined in the [`BasicTimePackage`](#packages) but excluded if using a [raw `Engine`]) operate on timestamps: -| Function | Parameter(s) | Description | -| ------------------ | ---------------------------------- | -------------------------------------------------------- | -| `elapsed` property | _none_ | returns the number of seconds since the timestamp | -| `-` operator | later timestamp, earlier timestamp | returns the number of seconds between the two timestamps | +| Function | Parameter(s) | Description | +| ----------------------------- | ---------------------------------- | -------------------------------------------------------- | +| `elapsed` method and property | _none_ | returns the number of seconds since the timestamp | +| `-` operator | later timestamp, earlier timestamp | returns the number of seconds between the two timestamps | ### Examples @@ -1777,15 +1785,19 @@ set of types (see [built-in operators](#built-in-operators)). "42" == 42; // false ``` -Comparing two values of _different_ data types, or of unknown data types, always results in `false`. +Comparing two values of _different_ data types, or of unknown data types, always results in `false`, +except for '`!=`' (not equals) which results in `true`. This is in line with intuition. ```rust -42 == 42.0; // false - i64 is different from f64 -42 > "42"; // false - i64 is different from string -42 <= "42"; // false again +42 == 42.0; // false - i64 cannot be compared with f64 +42 != 42.0; // true - i64 cannot be compared with f64 + +42 > "42"; // false - i64 cannot be compared with string +42 <= "42"; // false - i64 cannot be compared with string let ts = new_ts(); // custom type -ts == 42; // false - types are not the same +ts == 42; // false - types cannot be compared +ts != 42; // true - types cannot be compared ``` Boolean operators @@ -2073,8 +2085,8 @@ This is similar to Rust and many other modern languages. ### Function overloading -Functions can be _overloaded_ and are resolved purely upon the function's _name_ and the _number_ of parameters -(but not parameter _types_, since all parameters are the same type - [`Dynamic`]). +Functions defined in script can be _overloaded_ by _arity_ (i.e. they are resolved purely upon the function's _name_ +and _number_ of parameters, but not parameter _types_ since all parameters are the same type - [`Dynamic`]). New definitions _overwrite_ previous definitions of the same name and number of parameters. ```rust @@ -2346,19 +2358,20 @@ so that it does not consume more resources that it is allowed to. The most important resources to watch out for are: -* **Memory**: A malignant script may continuously grow an [array] or [object map] until all memory is consumed. +* **Memory**: A malicous script may continuously grow an [array] or [object map] until all memory is consumed. It may also create a large [array] or [object map] literal that exhausts all memory during parsing. -* **CPU**: A malignant script may run an infinite tight loop that consumes all CPU cycles. -* **Time**: A malignant script may run indefinitely, thereby blocking the calling system which is waiting for a result. -* **Stack**: A malignant script may attempt an infinite recursive call that exhausts the call stack. +* **CPU**: A malicous script may run an infinite tight loop that consumes all CPU cycles. +* **Time**: A malicous script may run indefinitely, thereby blocking the calling system which is waiting for a result. +* **Stack**: A malicous script may attempt an infinite recursive call that exhausts the call stack. Alternatively, it may create a degenerated deep expression with so many levels that the parser exhausts the call stack when parsing the expression; or even deeply-nested statement blocks, if nested deep enough. -* **Overflows**: A malignant script may deliberately cause numeric over-flows and/or under-flows, divide by zero, and/or +* **Overflows**: A malicous script may deliberately cause numeric over-flows and/or under-flows, divide by zero, and/or create bad floating-point representations, in order to crash the system. -* **Files**: A malignant script may continuously [`import`] an external module within an infinite loop, +* **Files**: A malicous script may continuously [`import`] an external module within an infinite loop, thereby putting heavy load on the file-system (or even the network if the file is not local). + Furthermore, the module script may simply [`import`] itself in an infinite recursion. Even when modules are not created from files, they still typically consume a lot of resources to load. -* **Data**: A malignant script may attempt to read from and/or write to data that it does not own. If this happens, +* **Data**: A malicous script may attempt to read from and/or write to data that it does not own. If this happens, it is a severe security breach and may put the entire system at risk. ### Maximum number of operations @@ -2434,7 +2447,7 @@ Rhai by default limits function calls to a maximum depth of 128 levels (16 level This limit may be changed via the `Engine::set_max_call_levels` method. When setting this limit, care must be also taken to the evaluation depth of each _statement_ -within the function. It is entirely possible for a malignant script to embed an recursive call deep +within the function. It is entirely possible for a malicous script to embed an recursive call deep inside a nested expression or statement block (see [maximum statement depth](#maximum-statement-depth)). The limit can be disabled via the [`unchecked`] feature for higher performance @@ -2479,7 +2492,7 @@ engine.set_max_expr_depths(50, 5); // allow nesting up to 50 layers of Beware that there may be multiple layers for a simple language construct, even though it may correspond to only one AST node. That is because the Rhai _parser_ internally runs a recursive chain of function calls -and it is important that a malignant script does not panic the parser in the first place. +and it is important that a malicous script does not panic the parser in the first place. Functions are placed under stricter limits because of the multiplicative effect of recursion. A script can effectively call itself while deep inside an expression chain within the function body, @@ -2492,7 +2505,7 @@ Make sure that `C x ( 5 + F ) + S` layered calls do not cause a stack overflow, * `S` = maximum statement depth at global level. A script exceeding the maximum nesting depths will terminate with a parsing error. -The malignant `AST` will not be able to get past parsing in the first place. +The malicous `AST` will not be able to get past parsing in the first place. This check can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well). From c9de37e8d1a930df585d1129d89fc746bde1b1ba Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 31 May 2020 00:02:23 +0800 Subject: [PATCH 02/12] Hash functions only once via custom hasher. --- src/engine.rs | 70 +++++++++++++++++++++------------------------------ src/module.rs | 14 +++++++---- src/parser.rs | 67 ++++++++++++++++++++---------------------------- src/utils.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 130 insertions(+), 87 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 2c761c7e..3b740b71 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -12,7 +12,7 @@ use crate::r#unsafe::{unsafe_cast_var_name_to_lifetime, unsafe_mut_cast_to_lifet use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; use crate::token::Position; -use crate::utils::StaticVec; +use crate::utils::{StaticVec, StraightHasherBuilder}; #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; @@ -196,7 +196,7 @@ impl State { /// /// The key of the `HashMap` is a `u64` hash calculated by the function `calc_fn_hash`. #[derive(Debug, Clone, Default)] -pub struct FunctionsLib(HashMap>); +pub struct FunctionsLib(HashMap, StraightHasherBuilder>); impl FunctionsLib { /// Create a new `FunctionsLib` from a collection of `FnDef`. @@ -261,7 +261,7 @@ impl From)>> for FunctionsLib { } impl Deref for FunctionsLib { - type Target = HashMap>; + type Target = HashMap, StraightHasherBuilder>; fn deref(&self) -> &Self::Target { &self.0 @@ -269,7 +269,7 @@ impl Deref for FunctionsLib { } impl DerefMut for FunctionsLib { - fn deref_mut(&mut self) -> &mut HashMap> { + fn deref_mut(&mut self) -> &mut HashMap, StraightHasherBuilder> { &mut self.0 } } @@ -952,21 +952,24 @@ impl Engine { let mut idx_val = idx_values.pop(); if is_index { + let pos = rhs.position(); + match rhs { - // xxx[idx].dot_rhs... | xxx[idx][dot_rhs]... + // xxx[idx].expr... | xxx[idx][expr]... Expr::Dot(x) | Expr::Index(x) => { + let (idx, expr, pos) = x.as_ref(); let is_idx = matches!(rhs, Expr::Index(_)); - let pos = x.0.position(); - let this_ptr = &mut self - .get_indexed_mut(state, lib, obj, is_ref, idx_val, pos, op_pos, false)?; + let idx_pos = idx.position(); + let this_ptr = &mut self.get_indexed_mut( + state, lib, obj, is_ref, idx_val, idx_pos, op_pos, false, + )?; self.eval_dot_index_chain_helper( - state, lib, this_ptr, &x.1, idx_values, is_idx, x.2, level, new_val, + state, lib, this_ptr, expr, idx_values, is_idx, *pos, level, new_val, ) } // xxx[rhs] = new_val _ if new_val.is_some() => { - let pos = rhs.position(); let this_ptr = &mut self .get_indexed_mut(state, lib, obj, is_ref, idx_val, pos, op_pos, true)?; @@ -975,16 +978,7 @@ impl Engine { } // xxx[rhs] _ => self - .get_indexed_mut( - state, - lib, - obj, - is_ref, - idx_val, - rhs.position(), - op_pos, - false, - ) + .get_indexed_mut(state, lib, obj, is_ref, idx_val, pos, op_pos, false) .map(|v| (v.clone_into_dynamic(), false)), } } else { @@ -1050,57 +1044,51 @@ impl Engine { .map(|(v, _)| (v, false)) } #[cfg(not(feature = "no_object"))] - // {xxx:map}.idx_lhs[idx_expr] | {xxx:map}.dot_lhs.rhs + // {xxx:map}.prop[expr] | {xxx:map}.prop.expr Expr::Index(x) | Expr::Dot(x) if obj.is::() => { + let (prop, expr, pos) = x.as_ref(); let is_idx = matches!(rhs, Expr::Index(_)); - let mut val = if let Expr::Property(p) = &x.0 { + let mut val = if let Expr::Property(p) = prop { let ((prop, _, _), _) = p.as_ref(); let index = prop.clone().into(); - self.get_indexed_mut(state, lib, obj, is_ref, index, x.2, op_pos, false)? + self.get_indexed_mut(state, lib, obj, is_ref, index, *pos, op_pos, false)? } else { - // Syntax error - return Err(Box::new(EvalAltResult::ErrorDotExpr( - "".into(), - rhs.position(), - ))); + unreachable!(); }; self.eval_dot_index_chain_helper( - state, lib, &mut val, &x.1, idx_values, is_idx, x.2, level, new_val, + state, lib, &mut val, expr, idx_values, is_idx, *pos, level, new_val, ) } - // xxx.idx_lhs[idx_expr] | xxx.dot_lhs.rhs + // xxx.prop[expr] | xxx.prop.expr Expr::Index(x) | Expr::Dot(x) => { - let is_idx = matches!(rhs, Expr::Index(_)); + let (prop, expr, pos) = x.as_ref(); + let is_idx = matches!(expr, Expr::Index(_)); let args = &mut [obj, &mut Default::default()]; - let (mut val, updated) = if let Expr::Property(p) = &x.0 { + let (mut val, updated) = if let Expr::Property(p) = prop { let ((_, getter, _), _) = p.as_ref(); let args = &mut args[..1]; - self.exec_fn_call(state, lib, getter, true, 0, args, is_ref, None, x.2, 0)? + self.exec_fn_call(state, lib, getter, true, 0, args, is_ref, None, *pos, 0)? } else { - // Syntax error - return Err(Box::new(EvalAltResult::ErrorDotExpr( - "".into(), - rhs.position(), - ))); + unreachable!(); }; let val = &mut val; let target = &mut val.into(); let (result, may_be_changed) = self.eval_dot_index_chain_helper( - state, lib, target, &x.1, idx_values, is_idx, x.2, level, new_val, + state, lib, target, expr, idx_values, is_idx, *pos, level, new_val, )?; // Feed the value back via a setter just in case it has been updated if updated || may_be_changed { - if let Expr::Property(p) = &x.0 { + if let Expr::Property(p) = prop { let ((_, _, setter), _) = p.as_ref(); // Re-use args because the first &mut parameter will not be consumed args[1] = val; self.exec_fn_call( - state, lib, setter, true, 0, args, is_ref, None, x.2, 0, + state, lib, setter, true, 0, args, is_ref, None, *pos, 0, ) .or_else(|err| match *err { // If there is no setter, no need to feed it back because the property is read-only diff --git a/src/module.rs b/src/module.rs index 498739b7..9e16e276 100644 --- a/src/module.rs +++ b/src/module.rs @@ -12,7 +12,7 @@ use crate::parser::{ use crate::result::EvalAltResult; use crate::scope::{Entry as ScopeEntry, EntryType as ScopeEntryType, Scope}; use crate::token::{Position, Token}; -use crate::utils::StaticVec; +use crate::utils::{StaticVec, StraightHasherBuilder}; use crate::stdlib::{ any::TypeId, @@ -44,10 +44,14 @@ pub struct Module { variables: HashMap, /// Flattened collection of all module variables, including those in sub-modules. - all_variables: HashMap, + all_variables: HashMap, /// External Rust functions. - functions: HashMap, CallableFunction)>, + functions: HashMap< + u64, + (String, FnAccess, StaticVec, CallableFunction), + StraightHasherBuilder, + >, /// Script-defined functions. lib: FunctionsLib, @@ -57,7 +61,7 @@ pub struct Module { /// Flattened collection of all external Rust functions, native or scripted, /// including those in sub-modules. - all_functions: HashMap, + all_functions: HashMap, } impl fmt::Debug for Module { @@ -101,7 +105,7 @@ impl Module { /// ``` pub fn new_with_capacity(capacity: usize) -> Self { Self { - functions: HashMap::with_capacity(capacity), + functions: HashMap::with_capacity_and_hasher(capacity, StraightHasherBuilder), ..Default::default() } } diff --git a/src/parser.rs b/src/parser.rs index 688dc7b1..bf668759 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -7,7 +7,7 @@ use crate::error::{LexError, ParseError, ParseErrorType}; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::scope::{EntryType as ScopeEntryType, Scope}; use crate::token::{Position, Token, TokenIterator}; -use crate::utils::StaticVec; +use crate::utils::{StaticVec, StraightHasherBuilder}; #[cfg(not(feature = "no_module"))] use crate::module::ModuleRef; @@ -1496,22 +1496,21 @@ fn parse_op_assignment_stmt<'a>( } /// Make a dot expression. -fn make_dot_expr( - lhs: Expr, - rhs: Expr, - op_pos: Position, - is_index: bool, -) -> Result { +fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { Ok(match (lhs, rhs) { - // idx_lhs[idx_rhs].rhs + // idx_lhs[idx_expr].rhs // Attach dot chain to the bottom level of indexing chain (Expr::Index(x), rhs) => { - Expr::Index(Box::new((x.0, make_dot_expr(x.1, rhs, op_pos, true)?, x.2))) + let (idx_lhs, idx_expr, pos) = *x; + Expr::Index(Box::new(( + idx_lhs, + make_dot_expr(idx_expr, rhs, op_pos)?, + pos, + ))) } // lhs.id (lhs, Expr::Variable(x)) if x.1.is_none() => { let (name, pos) = x.0; - let lhs = if is_index { lhs.into_property() } else { lhs }; let getter = make_getter(&name); let setter = make_setter(&name); @@ -1519,11 +1518,6 @@ fn make_dot_expr( Expr::Dot(Box::new((lhs, rhs, op_pos))) } - (lhs, Expr::Property(x)) => { - let lhs = if is_index { lhs.into_property() } else { lhs }; - let rhs = Expr::Property(x); - Expr::Dot(Box::new((lhs, rhs, op_pos))) - } // lhs.module::id - syntax error (_, Expr::Variable(x)) if x.1.is_some() => { #[cfg(feature = "no_module")] @@ -1531,34 +1525,30 @@ fn make_dot_expr( #[cfg(not(feature = "no_module"))] return Err(PERR::PropertyExpected.into_err(x.1.unwrap().get(0).1)); } + // lhs.prop + (lhs, prop @ Expr::Property(_)) => Expr::Dot(Box::new((lhs, prop, op_pos))), // lhs.dot_lhs.dot_rhs (lhs, Expr::Dot(x)) => { let (dot_lhs, dot_rhs, pos) = *x; Expr::Dot(Box::new(( lhs, - Expr::Dot(Box::new(( - dot_lhs.into_property(), - dot_rhs.into_property(), - pos, - ))), + Expr::Dot(Box::new((dot_lhs.into_property(), dot_rhs, pos))), op_pos, ))) } // lhs.idx_lhs[idx_rhs] (lhs, Expr::Index(x)) => { - let (idx_lhs, idx_rhs, pos) = *x; + let (dot_lhs, dot_rhs, pos) = *x; Expr::Dot(Box::new(( lhs, - Expr::Index(Box::new(( - idx_lhs.into_property(), - idx_rhs.into_property(), - pos, - ))), + Expr::Index(Box::new((dot_lhs.into_property(), dot_rhs, pos))), op_pos, ))) } + // lhs.func() + (lhs, func @ Expr::FnCall(_)) => Expr::Dot(Box::new((lhs, func, op_pos))), // lhs.rhs - (lhs, rhs) => Expr::Dot(Box::new((lhs, rhs.into_property(), op_pos))), + _ => unreachable!(), }) } @@ -1822,7 +1812,7 @@ fn parse_binary_op<'a>( _ => (), } - make_dot_expr(current_lhs, rhs, pos, false)? + make_dot_expr(current_lhs, rhs, pos)? } token => return Err(PERR::UnknownOperator(token.into()).into_err(pos)), @@ -2493,22 +2483,20 @@ pub fn parse_global_expr<'a>( fn parse_global_level<'a>( input: &mut Peekable>, max_expr_depth: (usize, usize), -) -> Result<(Vec, HashMap), ParseError> { +) -> Result<(Vec, Vec), ParseError> { let mut statements = Vec::::new(); - let mut functions = HashMap::::new(); + let mut functions = HashMap::with_hasher(StraightHasherBuilder); let mut state = ParseState::new(max_expr_depth.0); while !input.peek().unwrap().0.is_eof() { // Collect all the function definitions #[cfg(not(feature = "no_function"))] { - let mut access = FnAccess::Public; - let mut must_be_fn = false; - - if match_token(input, Token::Private)? { - access = FnAccess::Private; - must_be_fn = true; - } + let (access, must_be_fn) = if match_token(input, Token::Private)? { + (FnAccess::Private, true) + } else { + (FnAccess::Public, false) + }; match input.peek().unwrap() { (Token::Fn, _) => { @@ -2565,7 +2553,7 @@ fn parse_global_level<'a>( } } - Ok((statements, functions)) + Ok((statements, functions.into_iter().map(|(_, v)| v).collect())) } /// Run the parser on an input stream, returning an AST. @@ -2576,9 +2564,8 @@ pub fn parse<'a>( optimization_level: OptimizationLevel, max_expr_depth: (usize, usize), ) -> Result { - let (statements, functions) = parse_global_level(input, max_expr_depth)?; + let (statements, lib) = parse_global_level(input, max_expr_depth)?; - let lib = functions.into_iter().map(|(_, v)| v).collect(); Ok( // Optimize AST optimize_into_ast(engine, scope, statements, lib, optimization_level), diff --git a/src/utils.rs b/src/utils.rs index c396df19..ccd3d6be 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -11,7 +11,7 @@ use crate::stdlib::{ borrow::Borrow, boxed::Box, fmt, - hash::{Hash, Hasher}, + hash::{BuildHasher, Hash, Hasher}, iter::FromIterator, mem, mem::MaybeUninit, @@ -27,6 +27,48 @@ use crate::stdlib::collections::hash_map::DefaultHasher; #[cfg(feature = "no_std")] use ahash::AHasher; +/// A hasher that only takes one single `u64` and returns it as a hash key. +/// +/// # Panics +/// +/// Panics when hashing any data type other than a `u64`. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Default)] +pub struct StraightHasher(u64); + +impl Hasher for StraightHasher { + #[inline(always)] + fn finish(&self) -> u64 { + self.0 + } + #[inline] + fn write(&mut self, bytes: &[u8]) { + let mut key = [0_u8; 8]; + key.copy_from_slice(&bytes[..8]); // Panics if fewer than 8 bytes + self.0 = u64::from_le_bytes(key); + } +} + +impl StraightHasher { + /// Create a `StraightHasher`. + #[inline(always)] + pub fn new() -> Self { + Self(0) + } +} + +/// A hash builder for `StraightHasher`. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Default)] +pub struct StraightHasherBuilder; + +impl BuildHasher for StraightHasherBuilder { + type Hasher = StraightHasher; + + #[inline(always)] + fn build_hasher(&self) -> Self::Hasher { + StraightHasher::new() + } +} + /// Calculate a `u64` hash key from a module-qualified function name and parameter types. /// /// Module names are passed in via `&str` references from an iterator. @@ -108,6 +150,7 @@ pub struct StaticVec { const MAX_STATIC_VEC: usize = 4; impl Drop for StaticVec { + #[inline(always)] fn drop(&mut self) { self.clear(); } @@ -174,6 +217,7 @@ impl FromIterator for StaticVec { impl StaticVec { /// Create a new `StaticVec`. + #[inline(always)] pub fn new() -> Self { Default::default() } @@ -189,6 +233,7 @@ impl StaticVec { self.len = 0; } /// Extract a `MaybeUninit` into a concrete initialized type. + #[inline(always)] fn extract(value: MaybeUninit) -> T { unsafe { value.assume_init() } } @@ -250,6 +295,7 @@ impl StaticVec { ); } /// Is data stored in fixed-size storage? + #[inline(always)] fn is_fixed_storage(&self) -> bool { self.len <= MAX_STATIC_VEC } @@ -359,10 +405,12 @@ impl StaticVec { result } /// Get the number of items in this `StaticVec`. + #[inline(always)] pub fn len(&self) -> usize { self.len } /// Is this `StaticVec` empty? + #[inline(always)] pub fn is_empty(&self) -> bool { self.len == 0 } @@ -605,41 +653,48 @@ pub struct ImmutableString(Shared); impl Deref for ImmutableString { type Target = String; + #[inline(always)] fn deref(&self) -> &Self::Target { &self.0 } } impl AsRef for ImmutableString { + #[inline(always)] fn as_ref(&self) -> &String { &self.0 } } impl Borrow for ImmutableString { + #[inline(always)] fn borrow(&self) -> &str { self.0.as_str() } } impl From<&str> for ImmutableString { + #[inline(always)] fn from(value: &str) -> Self { Self(value.to_string().into()) } } impl From for ImmutableString { + #[inline(always)] fn from(value: String) -> Self { Self(value.into()) } } impl From> for ImmutableString { + #[inline(always)] fn from(value: Box) -> Self { Self(value.into()) } } impl From for String { + #[inline(always)] fn from(value: ImmutableString) -> Self { value.into_owned() } @@ -648,42 +703,49 @@ impl From for String { impl FromStr for ImmutableString { type Err = (); + #[inline(always)] fn from_str(s: &str) -> Result { Ok(Self(s.to_string().into())) } } impl FromIterator for ImmutableString { + #[inline(always)] fn from_iter>(iter: T) -> Self { Self(iter.into_iter().collect::().into()) } } impl<'a> FromIterator<&'a char> for ImmutableString { + #[inline(always)] fn from_iter>(iter: T) -> Self { Self(iter.into_iter().cloned().collect::().into()) } } impl<'a> FromIterator<&'a str> for ImmutableString { + #[inline(always)] fn from_iter>(iter: T) -> Self { Self(iter.into_iter().collect::().into()) } } impl<'a> FromIterator for ImmutableString { + #[inline(always)] fn from_iter>(iter: T) -> Self { Self(iter.into_iter().collect::().into()) } } impl fmt::Display for ImmutableString { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(self.0.as_str(), f) } } impl fmt::Debug for ImmutableString { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(self.0.as_str(), f) } @@ -818,6 +880,7 @@ impl Add for &ImmutableString { } impl AddAssign for ImmutableString { + #[inline(always)] fn add_assign(&mut self, rhs: char) { self.make_mut().push(rhs); } @@ -832,6 +895,7 @@ impl ImmutableString { } /// Make sure that the `ImmutableString` is unique (i.e. no other outstanding references). /// Then return a mutable reference to the `String`. + #[inline(always)] pub fn make_mut(&mut self) -> &mut String { shared_make_mut(&mut self.0) } From 13c49387efc0342c585be0ef48835588c05bf4d1 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 31 May 2020 12:36:31 +0800 Subject: [PATCH 03/12] Add OptimizationLevel::is_simple --- src/optimize.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/optimize.rs b/src/optimize.rs index f8b0d1bb..20ccfa3c 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -37,6 +37,10 @@ impl OptimizationLevel { pub fn is_none(self) -> bool { self == Self::None } + /// Is the `OptimizationLevel` Simple. + pub fn is_simple(self) -> bool { + self == Self::Simple + } /// Is the `OptimizationLevel` Full. pub fn is_full(self) -> bool { self == Self::Full From 5f727335a6b878b43d518a756794078062e391c4 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 31 May 2020 12:36:42 +0800 Subject: [PATCH 04/12] Add type info. --- src/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.rs b/src/parser.rs index bf668759..2a20422e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2485,7 +2485,7 @@ fn parse_global_level<'a>( max_expr_depth: (usize, usize), ) -> Result<(Vec, Vec), ParseError> { let mut statements = Vec::::new(); - let mut functions = HashMap::with_hasher(StraightHasherBuilder); + let mut functions = HashMap::::with_hasher(StraightHasherBuilder); let mut state = ParseState::new(max_expr_depth.0); while !input.peek().unwrap().0.is_eof() { From 76d792011f72d239f81c22012638b95391c87bf5 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 31 May 2020 12:37:19 +0800 Subject: [PATCH 05/12] Add Engine::call_fn_dynamic. --- README.md | 10 ++++++++ RELEASES.md | 3 +++ src/api.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a7b129f0..50792756 100644 --- a/README.md +++ b/README.md @@ -341,6 +341,16 @@ let result: i64 = engine.call_fn(&mut scope, &ast, "hello", () )?; let result: () = engine.call_fn(&mut scope, &ast, "hidden", ())?; ``` +For more control, construct all arguments as `Dynamic` values and use `Engine::call_fn_dynamic`: + +```rust +let result: Dynamic = engine.call_fn_dynamic(&mut scope, &ast, "hello", + &mut [ String::from("abc").into(), 123_i64.into() ])?; +``` + +However, beware that `Engine::call_fn_dynamic` _consumes_ its arguments, meaning that all arguments passed to it +will be replaced by `()` afterwards. To re-use the arguments, clone them beforehand and pass in the clone. + ### Creating Rust anonymous functions from Rhai script [`Func`]: #creating-rust-anonymous-functions-from-rhai-script diff --git a/RELEASES.md b/RELEASES.md index 286aac4a..d0a50a9a 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -41,6 +41,7 @@ New features * Set limit on maximum level of nesting expressions and statements to avoid panics during parsing. * New `EvalPackage` to disable `eval`. * `Module::set_getter_fn`, `Module::set_setter_fn` and `Module:set_indexer_fn` to register getter/setter/indexer functions. +* `Engine::call_fn_dynamic` for more control in calling script functions. Speed enhancements ------------------ @@ -60,6 +61,8 @@ Speed enhancements excessive cloning. For example, if `a` is a large array, getting its length in this manner: `len(a)` used to result in a full clone of `a` before taking the length and throwing the copy away. Now, `a` is simply passed by reference, avoiding the cloning altogether. +* A custom hasher simply passes through `u64` keys without hashing to avoid function call hash keys + (which as by themselves `u64`) being hashed twice. Version 0.14.1 diff --git a/src/api.rs b/src/api.rs index 8a30a480..1d593023 100644 --- a/src/api.rs +++ b/src/api.rs @@ -997,6 +997,7 @@ impl Engine { } /// Call a script function defined in an `AST` with multiple arguments. + /// Arguments are passed as a tuple. /// /// # Example /// @@ -1040,6 +1041,67 @@ impl Engine { args: A, ) -> Result> { let mut arg_values = args.into_vec(); + let result = self.call_fn_dynamic(scope, ast, name, arg_values.as_mut())?; + + let return_type = self.map_type_name(result.type_name()); + + return result.try_cast().ok_or_else(|| { + Box::new(EvalAltResult::ErrorMismatchOutputType( + return_type.into(), + Position::none(), + )) + }); + } + + /// Call a script function defined in an `AST` with multiple `Dynamic` arguments. + /// + /// ## WARNING + /// + /// All the arguments are _consumed_, meaning that they're replaced by `()`. + /// This is to avoid unnecessarily cloning the arguments. + /// Do you use the arguments after this call. If you need them afterwards, + /// clone them _before_ calling this function. + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), Box> { + /// # #[cfg(not(feature = "no_function"))] + /// # { + /// use rhai::{Engine, Scope}; + /// + /// let engine = Engine::new(); + /// + /// let ast = engine.compile(r" + /// fn add(x, y) { len(x) + y + foo } + /// fn add1(x) { len(x) + 1 + foo } + /// fn bar() { foo/2 } + /// ")?; + /// + /// let mut scope = Scope::new(); + /// scope.push("foo", 42_i64); + /// + /// // Call the script-defined function + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add", &mut [ String::from("abc").into(), 123_i64.into() ])?; + /// assert_eq!(result.cast::(), 168); + /// + /// let result = engine.call_fn_dynamic(&mut scope, &ast, "add1", &mut [ String::from("abc").into() ])?; + /// assert_eq!(result.cast::(), 46); + /// + /// let result= engine.call_fn_dynamic(&mut scope, &ast, "bar", &mut [])?; + /// assert_eq!(result.cast::(), 21); + /// # } + /// # Ok(()) + /// # } + /// ``` + #[cfg(not(feature = "no_function"))] + pub fn call_fn_dynamic( + &self, + scope: &mut Scope, + ast: &AST, + name: &str, + arg_values: &mut [Dynamic], + ) -> Result> { let mut args: StaticVec<_> = arg_values.iter_mut().collect(); let lib = ast.lib(); let pos = Position::none(); @@ -1051,16 +1113,7 @@ impl Engine { let mut state = State::new(); let args = args.as_mut(); - let result = self.call_script_fn(scope, &mut state, &lib, name, fn_def, args, pos, 0)?; - - let return_type = self.map_type_name(result.type_name()); - - return result.try_cast().ok_or_else(|| { - Box::new(EvalAltResult::ErrorMismatchOutputType( - return_type.into(), - pos, - )) - }); + self.call_script_fn(scope, &mut state, &lib, name, fn_def, args, pos, 0) } /// Optimize the `AST` with constants defined in an external Scope. From d7d49a5196f969ed611079cb5a8ea5f8a267deeb Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 31 May 2020 14:27:16 +0800 Subject: [PATCH 06/12] Fix bug in chained dot/index expression. --- src/engine.rs | 4 ++-- tests/maps.rs | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 3b740b71..0fdd0467 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1064,7 +1064,7 @@ impl Engine { // xxx.prop[expr] | xxx.prop.expr Expr::Index(x) | Expr::Dot(x) => { let (prop, expr, pos) = x.as_ref(); - let is_idx = matches!(expr, Expr::Index(_)); + let is_idx = matches!(rhs, Expr::Index(_)); let args = &mut [obj, &mut Default::default()]; let (mut val, updated) = if let Expr::Property(p) = prop { @@ -1102,7 +1102,7 @@ impl Engine { } // Syntax error _ => Err(Box::new(EvalAltResult::ErrorDotExpr( - "".into(), + format!("{:?}", rhs), rhs.position(), ))), } diff --git a/tests/maps.rs b/tests/maps.rs index 64c5a4c2..a7aef8dd 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -21,6 +21,10 @@ fn test_map_indexing() -> Result<(), Box> { )?, 'o' ); + assert_eq!( + engine.eval::(r#"let a = [#{s:"hello"}]; a[0].s[2] = 'X'; a[0].s"#)?, + "heXlo" + ); } assert_eq!( From 840afe74bb574763f408c45726fbdfa7dc134fcb Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 31 May 2020 15:51:26 +0800 Subject: [PATCH 07/12] Simplify eval_dot_index_chain. --- src/engine.rs | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 0fdd0467..03521f4f 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -441,7 +441,6 @@ fn search_scope<'s, 'a>( Expr::Variable(x) => x.as_ref(), _ => unreachable!(), }; - let index = if state.always_search { None } else { *index }; #[cfg(not(feature = "no_module"))] { @@ -470,6 +469,8 @@ fn search_scope<'s, 'a>( } } + let index = if state.always_search { None } else { *index }; + let index = if let Some(index) = index { scope.len() - index.get() } else { @@ -1102,7 +1103,7 @@ impl Engine { } // Syntax error _ => Err(Box::new(EvalAltResult::ErrorDotExpr( - format!("{:?}", rhs), + "".into(), rhs.position(), ))), } @@ -1115,13 +1116,16 @@ impl Engine { scope: &mut Scope, state: &mut State, lib: &FunctionsLib, - dot_lhs: &Expr, - dot_rhs: &Expr, - is_index: bool, - op_pos: Position, + expr: &Expr, level: usize, new_val: Option, ) -> Result> { + let ((dot_lhs, dot_rhs, op_pos), is_index) = match expr { + Expr::Index(x) => (x.as_ref(), true), + Expr::Dot(x) => (x.as_ref(), false), + _ => unreachable!(), + }; + let idx_values = &mut StaticVec::new(); self.eval_indexed_chain(scope, state, lib, dot_rhs, idx_values, 0, level)?; @@ -1146,7 +1150,7 @@ impl Engine { let this_ptr = &mut target.into(); self.eval_dot_index_chain_helper( - state, lib, this_ptr, dot_rhs, idx_values, is_index, op_pos, level, new_val, + state, lib, this_ptr, dot_rhs, idx_values, is_index, *op_pos, level, new_val, ) .map(|(v, _)| v) } @@ -1161,7 +1165,7 @@ impl Engine { let val = self.eval_expr(scope, state, lib, expr, level)?; let this_ptr = &mut val.into(); self.eval_dot_index_chain_helper( - state, lib, this_ptr, dot_rhs, idx_values, is_index, op_pos, level, new_val, + state, lib, this_ptr, dot_rhs, idx_values, is_index, *op_pos, level, new_val, ) .map(|(v, _)| v) } @@ -1477,14 +1481,14 @@ impl Engine { Expr::Variable(_) => unreachable!(), // idx_lhs[idx_expr] op= rhs #[cfg(not(feature = "no_index"))] - Expr::Index(x) => self.eval_dot_index_chain( - scope, state, lib, &x.0, &x.1, true, x.2, level, new_val, - ), + Expr::Index(_) => { + self.eval_dot_index_chain(scope, state, lib, lhs_expr, level, new_val) + } // dot_lhs.dot_rhs op= rhs #[cfg(not(feature = "no_object"))] - Expr::Dot(x) => self.eval_dot_index_chain( - scope, state, lib, &x.0, &x.1, false, *op_pos, level, new_val, - ), + Expr::Dot(_) => { + self.eval_dot_index_chain(scope, state, lib, lhs_expr, level, new_val) + } // Error assignment to constant expr if expr.is_constant() => { Err(Box::new(EvalAltResult::ErrorAssignmentToConstant( @@ -1501,15 +1505,11 @@ impl Engine { // lhs[idx_expr] #[cfg(not(feature = "no_index"))] - Expr::Index(x) => { - self.eval_dot_index_chain(scope, state, lib, &x.0, &x.1, true, x.2, level, None) - } + Expr::Index(_) => self.eval_dot_index_chain(scope, state, lib, expr, level, None), // lhs.dot_rhs #[cfg(not(feature = "no_object"))] - Expr::Dot(x) => { - self.eval_dot_index_chain(scope, state, lib, &x.0, &x.1, false, x.2, level, None) - } + Expr::Dot(_) => self.eval_dot_index_chain(scope, state, lib, expr, level, None), #[cfg(not(feature = "no_index"))] Expr::Array(x) => Ok(Dynamic(Union::Array(Box::new( From 697bb39a7f30db37f4bb16b2bca5b8d877327476 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 31 May 2020 15:55:02 +0800 Subject: [PATCH 08/12] Add writeup on Rhai usage scenarios. --- README.md | 20 ++++++++++++++++---- RELEASES.md | 6 +++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 50792756..a127d857 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Features including [getters/setters](#getters-and-setters), [methods](#members-and-methods) and [indexers](#indexers). * Freely pass Rust variables/constants into a script via an external [`Scope`]. * Easily [call a script-defined function](#calling-rhai-functions-from-rust) from Rust. -* Low compile-time overhead (~0.6 sec debug/~3 sec release for `rhai_runner` sample app). -* Fairly efficient evaluation (1 million iterations in 0.75 sec on my 5 year old laptop). +* Fairly low compile-time overhead. +* Fairly efficient evaluation (1 million iterations in 0.25 sec on a single core, 2.3 GHz Linux VM). * Relatively little `unsafe` code (yes there are some for performance reasons, and most `unsafe` code is limited to one single source file, all with names starting with `"unsafe_"`). * Re-entrant scripting [`Engine`] can be made `Send + Sync` (via the [`sync`] feature). @@ -47,10 +47,22 @@ It doesn't attempt to be a new language. For example: * No classes. Well, Rust doesn't either. On the other hand... * No traits... so it is also not Rust. Do your Rusty stuff in Rust. -* No structures - define your types in Rust instead; Rhai can seamlessly work with _any Rust type_. +* No structures/records - define your types in Rust instead; Rhai can seamlessly work with _any Rust type_. + There is, however, a built-in [object map] type which is adequate for most uses. * No first-class functions - Code your functions in Rust instead, and register them with Rhai. +* No garbage collection - this should be expected, so... * No closures - do your closure magic in Rust instead; [turn a Rhai scripted function into a Rust closure](#calling-rhai-functions-from-rust). -* It is best to expose an API in Rhai for scripts to call. All your core functionalities should be in Rust. +* No byte-codes/JIT - Rhai has an AST-walking interpreter which will not win any speed races. The purpose of Rhai is not + to be extremely _fast_, but to make it as easy as possible to integrate with native Rust programs. + +Due to this intended usage, Rhai deliberately keeps the language simple and small by omitting advanced language features +such as classes, inheritance, first-class functions, closures, concurrency, byte-codes, JIT etc. +Avoid the temptation to write full-fledge program logic entirely in Rhai - that use case is best fulfilled by +more complete languages such as JS or Lua. + +Therefore, in actual practice, it is usually best to expose a Rust API into Rhai for scripts to call. +All your core functionalities should be in Rust. +This is similar to some dynamic languages where most of the core functionalities reside in a C/C++ standard library. Installation ------------ diff --git a/RELEASES.md b/RELEASES.md index d0a50a9a..1208c720 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -13,13 +13,13 @@ Bug fixes --------- * Indexing with an index or dot expression now works property (it compiled wrongly before). - For example, `let s = "hello"; s[s.len-1] = 'x';` now works property instead of an error. + For example, `let s = "hello"; s[s.len-1] = 'x';` now works property instead of causing a runtime error. Breaking changes ---------------- * `Engine::compile_XXX` functions now return `ParseError` instead of `Box`. -* The `RegisterDynamicFn` trait is merged into the `RegisterResutlFn` trait which now always returns +* The `RegisterDynamicFn` trait is merged into the `RegisterResultFn` trait which now always returns `Result>`. * Default maximum limit on levels of nested function calls is fine-tuned and set to a different value. * Some operator functions are now built in (see _Speed enhancements_ below), so they are available even @@ -62,7 +62,7 @@ Speed enhancements in a full clone of `a` before taking the length and throwing the copy away. Now, `a` is simply passed by reference, avoiding the cloning altogether. * A custom hasher simply passes through `u64` keys without hashing to avoid function call hash keys - (which as by themselves `u64`) being hashed twice. + (which are by themselves `u64`) being hashed twice. Version 0.14.1 From 7fa05f3886eb842de7e569ea885a6413483d477b Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 31 May 2020 15:57:53 +0800 Subject: [PATCH 09/12] Do not print to avoid skewing the run timing. --- scripts/mat_mul.rhai | 2 ++ scripts/primes.rhai | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/mat_mul.rhai b/scripts/mat_mul.rhai index b93a1a5e..54bca636 100644 --- a/scripts/mat_mul.rhai +++ b/scripts/mat_mul.rhai @@ -62,8 +62,10 @@ let a = mat_gen(SIZE); let b = mat_gen(SIZE); let c = mat_mul(a, b); +/* for i in range(0, SIZE) { print(c[i]); } +*/ print("Finished. Run time = " + now.elapsed + " seconds."); diff --git a/scripts/primes.rhai b/scripts/primes.rhai index 7f66976f..fc4acc7e 100644 --- a/scripts/primes.rhai +++ b/scripts/primes.rhai @@ -15,7 +15,7 @@ let total_primes_found = 0; for p in range(2, MAX_NUMBER_TO_CHECK) { if !prime_mask[p] { continue; } - print(p); + //print(p); total_primes_found += 1; From 331513f5e08e8ce138bbb7ee038bfb8347d1074c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 31 May 2020 22:47:39 +0800 Subject: [PATCH 10/12] Increase to prime numbers <= 1 million. --- scripts/primes.rhai | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/primes.rhai b/scripts/primes.rhai index fc4acc7e..25fa0690 100644 --- a/scripts/primes.rhai +++ b/scripts/primes.rhai @@ -2,7 +2,7 @@ let now = timestamp(); -const MAX_NUMBER_TO_CHECK = 100_000; // 9592 primes <= 100000 +const MAX_NUMBER_TO_CHECK = 1_000_000; // 9592 primes <= 100000 let prime_mask = []; prime_mask.pad(MAX_NUMBER_TO_CHECK, true); @@ -28,6 +28,6 @@ for p in range(2, MAX_NUMBER_TO_CHECK) { print("Total " + total_primes_found + " primes <= " + MAX_NUMBER_TO_CHECK); print("Run time = " + now.elapsed + " seconds."); -if total_primes_found != 9_592 { - print("The answer is WRONG! Should be 9,592!"); +if total_primes_found != 78_498 { + print("The answer is WRONG! Should be 78,498!"); } \ No newline at end of file From c6e5f672c937ea245d0fe70dfa2660b1b22268f5 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Sun, 31 May 2020 23:44:49 +0800 Subject: [PATCH 11/12] More benchmarks and examples. --- README.md | 63 +++++++++++++----------- benches/eval_array.rs | 24 +++++++++ scripts/for2.rhai | 22 +++++++++ scripts/strings_map.rhai | 103 +++++++++++++++++++++++++++++++++++++++ src/optimize.rs | 16 +++--- 5 files changed, 189 insertions(+), 39 deletions(-) create mode 100644 scripts/for2.rhai create mode 100644 scripts/strings_map.rhai diff --git a/README.md b/README.md index a127d857..f894ea7b 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Features to do checked arithmetic operations); for [`no-std`](#optional-features) builds, a number of additional dependencies are pulled in to provide for functionalities that used to be in `std`. -**Note:** Currently, the version is 0.15.0, so the language and API's may change before they stabilize. +**Note:** Currently, the version is `0.15.0`, so the language and API's may change before they stabilize. What Rhai doesn't do -------------------- @@ -67,7 +67,7 @@ This is similar to some dynamic languages where most of the core functionalities Installation ------------ -Install the Rhai crate by adding this line to `dependencies`: +Install the Rhai crate on [`crates.io`](https::/crates.io/crates/rhai/) by adding this line to `dependencies`: ```toml [dependencies] @@ -191,33 +191,35 @@ A number of examples can be found in the `examples` folder: Examples can be run with the following command: ```bash -cargo run --example name +cargo run --example {example_name} ``` The `repl` example is a particularly good one as it allows one to interactively try out Rhai's language features in a standard REPL (**R**ead-**E**val-**P**rint **L**oop). -Example Scripts +Example scripts --------------- There are also a number of examples scripts that showcase Rhai's features, all in the `scripts` folder: -| Language feature scripts | Description | -| ---------------------------------------------------- | ------------------------------------------------------------- | -| [`array.rhai`](scripts/array.rhai) | [arrays] in Rhai | -| [`assignment.rhai`](scripts/assignment.rhai) | variable declarations | -| [`comments.rhai`](scripts/comments.rhai) | just comments | -| [`for1.rhai`](scripts/for1.rhai) | for loops | -| [`function_decl1.rhai`](scripts/function_decl1.rhai) | a function without parameters | -| [`function_decl2.rhai`](scripts/function_decl2.rhai) | a function with two parameters | -| [`function_decl3.rhai`](scripts/function_decl3.rhai) | a function with many parameters | -| [`if1.rhai`](scripts/if1.rhai) | if example | -| [`loop.rhai`](scripts/loop.rhai) | endless loop in Rhai, this example emulates a do..while cycle | -| [`op1.rhai`](scripts/op1.rhai) | just a simple addition | -| [`op2.rhai`](scripts/op2.rhai) | simple addition and multiplication | -| [`op3.rhai`](scripts/op3.rhai) | change evaluation order with parenthesis | -| [`string.rhai`](scripts/string.rhai) | [string] operations | -| [`while.rhai`](scripts/while.rhai) | while loop | +| Language feature scripts | Description | +| ---------------------------------------------------- | ----------------------------------------------------------------------------- | +| [`array.rhai`](scripts/array.rhai) | [arrays] in Rhai | +| [`assignment.rhai`](scripts/assignment.rhai) | variable declarations | +| [`comments.rhai`](scripts/comments.rhai) | just comments | +| [`for1.rhai`](scripts/for1.rhai) | [`for`](#for-loop) loops | +| [`for2.rhai`](scripts/for2.rhai) | [`for`](#for-loop) loops on [arrays] | +| [`function_decl1.rhai`](scripts/function_decl1.rhai) | a [function] without parameters | +| [`function_decl2.rhai`](scripts/function_decl2.rhai) | a [function] with two parameters | +| [`function_decl3.rhai`](scripts/function_decl3.rhai) | a [function] with many parameters | +| [`if1.rhai`](scripts/if1.rhai) | [`if`](#if-statement) example | +| [`loop.rhai`](scripts/loop.rhai) | count-down [`loop`](#infinite-loop) in Rhai, emulating a `do` .. `while` loop | +| [`op1.rhai`](scripts/op1.rhai) | just simple addition | +| [`op2.rhai`](scripts/op2.rhai) | simple addition and multiplication | +| [`op3.rhai`](scripts/op3.rhai) | change evaluation order with parenthesis | +| [`string.rhai`](scripts/string.rhai) | [string] operations | +| [`strings_map.rhai`](scripts/strings_map.rhai) | [string] and [object map] operations | +| [`while.rhai`](scripts/while.rhai) | [`while`](#while-loop) loop | | Example scripts | Description | | -------------------------------------------- | ---------------------------------------------------------------------------------- | @@ -247,6 +249,7 @@ fn main() -> Result<(), Box> let engine = Engine::new(); let result = engine.eval::("40 + 2")?; + // ^^^^^^^ cast the result to an 'i64', this is required println!("Answer: {}", result); // prints 42 @@ -303,6 +306,8 @@ let ast = engine.compile_file("hello_world.rhai".into())?; ### Calling Rhai functions from Rust +[`private`]: #calling-rhai-functions-from-rust + Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust via `Engine::call_fn`. Functions declared with `private` are hidden and cannot be called from Rust (see also [modules]). @@ -1870,8 +1875,8 @@ my_str += 12345; my_str == "abcABC12345" ``` -`if` statements ---------------- +`if` statement +-------------- ```rust if foo(x) { @@ -1906,8 +1911,8 @@ let x = if decision { 42 }; // no else branch defaults to '()' x == (); ``` -`while` loops -------------- +`while` loop +------------ ```rust let x = 10; @@ -1934,8 +1939,8 @@ loop { } ``` -`for` loops ------------ +`for` loop +---------- Iterating through a range or an [array] is provided by the `for` ... `in` loop. @@ -2206,8 +2211,8 @@ Modules can be disabled via the [`no_module`] feature. A _module_ is a single script (or pre-compiled `AST`) containing global variables and functions. The `export` statement, which can only be at global level, exposes selected variables as members of a module. Variables not exported are _private_ and invisible to the outside. -On the other hand, all functions are automatically exported, _unless_ it is explicitly opt-out with the `private` prefix. -Functions declared `private` are invisible to the outside. +On the other hand, all functions are automatically exported, _unless_ it is explicitly opt-out with the [`private`] prefix. +Functions declared [`private`] are invisible to the outside. Everything exported from a module is **constant** (**read-only**). @@ -2301,7 +2306,7 @@ engine.eval_expression_with_scope::(&scope, "question::inc(question::answer It is easy to convert a pre-compiled `AST` into a module: just use `Module::eval_ast_as_new`. Don't forget the `export` statement, otherwise there will be no variables exposed by the module -other than non-`private` functions (unless that's intentional). +other than non-[`private`] functions (unless that's intentional). ```rust use rhai::{Engine, Module}; diff --git a/benches/eval_array.rs b/benches/eval_array.rs index d97e1164..0689bf4f 100644 --- a/benches/eval_array.rs +++ b/benches/eval_array.rs @@ -61,3 +61,27 @@ fn bench_eval_array_large_set(bench: &mut Bencher) { bench.iter(|| engine.consume_ast(&ast).unwrap()); } + +#[bench] +fn bench_eval_array_loop(bench: &mut Bencher) { + let script = r#" + let list = []; + + for i in range(0, 10_000) { + list.push(i); + } + + let sum = 0; + + for i in list { + sum += i; + } + "#; + + let mut engine = Engine::new(); + engine.set_optimization_level(OptimizationLevel::None); + + let ast = engine.compile(script).unwrap(); + + bench.iter(|| engine.consume_ast(&ast).unwrap()); +} diff --git a/scripts/for2.rhai b/scripts/for2.rhai new file mode 100644 index 00000000..8547e7d5 --- /dev/null +++ b/scripts/for2.rhai @@ -0,0 +1,22 @@ +const MAX = 1_000_000; + +print("Iterating an array with " + MAX + " items..."); + +print("Ready... Go!"); + +let now = timestamp(); + +let list = []; + +for i in range(0, MAX) { + list.push(i); +} + +let sum = 0; + +for i in list { + sum += i; +} + +print("Sum = " + sum); +print("Finished. Run time = " + now.elapsed + " seconds."); diff --git a/scripts/strings_map.rhai b/scripts/strings_map.rhai new file mode 100644 index 00000000..04cd81db --- /dev/null +++ b/scripts/strings_map.rhai @@ -0,0 +1,103 @@ +print("Ready... Go!"); + +let now = timestamp(); + +let adverbs = [ "moderately", "really", "slightly", "very" ]; + +let adjectives = [ + "abandoned", "able", "absolute", "academic", "acceptable", "acclaimed", + "accomplished", "accurate", "aching", "acidic", "acrobatic", "active", + "actual", "adept", "admirable", "admired", "adolescent", "adorable", "adored", + "advanced", "adventurous", "affectionate", "afraid", "aged", "aggravating", + "aggressive", "agile", "agitated", "agonizing", "agreeable", "ajar", + "alarmed", "alarming", "alert", "alienated", "alive", "all", "altruistic", + "amazing", "ambitious", "ample", "amused", "amusing", "anchored", "ancient", + "angelic", "angry", "anguished", "animated", "annual", "another", "antique", + "anxious", "any", "apprehensive", "appropriate", "apt", "arctic", "arid", + "aromatic", "artistic", "ashamed", "assured", "astonishing", "athletic", + "attached", "attentive", "attractive", "austere", "authentic", "authorized", + "automatic", "avaricious", "average", "aware", "awesome", "awful", "awkward", + "babyish", "back", "bad", "baggy", "bare", "barren", "basic", "beautiful", + "belated", "beloved", "beneficial", "best", "better", "bewitched", "big", + "big-hearted", "biodegradable", "bite-sized", "bitter", "black", + "black-and-white", "bland", "blank", "blaring", "bleak", "blind", "blissful", + "blond", "blue", "blushing", "bogus", "boiling", "bold", "bony", "boring", + "bossy", "both", "bouncy", "bountiful", "bowed", "brave", "breakable", + "brief", "bright", "brilliant", "brisk", "broken", "bronze", "brown", + "bruised", "bubbly", "bulky", "bumpy", "buoyant", "burdensome", "burly", + "bustling", "busy", "buttery", "buzzing", "calculating", "calm", "candid", + "canine", "capital", "carefree", "careful", "careless", "caring", "cautious", + "cavernous", "celebrated", "charming", "cheap", "cheerful", "cheery", "chief", + "chilly", "chubby", "circular", "classic", "clean", "clear", "clear-cut", + "clever", "close", "closed", "cloudy", "clueless", "clumsy", "cluttered", + "coarse", "cold", "colorful", "colorless", "colossal", "comfortable", + "common", "compassionate", "competent", "complete", "complex", "complicated", + "composed", "concerned", "concrete", "confused", "conscious", "considerate", + "constant", "content", "conventional", "cooked", "cool", "cooperative", + "coordinated", "corny", "corrupt", "costly", "courageous", "courteous", + "crafty" +]; + +let animals = [ + "aardvark", "african buffalo", "albatross", "alligator", "alpaca", "ant", + "anteater", "antelope", "ape", "armadillo", "baboon", "badger", "barracuda", + "bat", "bear", "beaver", "bee", "bison", "black panther", "blue jay", "boar", + "butterfly", "camel", "capybara", "carduelis", "caribou", "cassowary", "cat", + "caterpillar", "cattle", "chamois", "cheetah", "chicken", "chimpanzee", + "chinchilla", "chough", "clam", "cobra", "cockroach", "cod", "cormorant", + "coyote", "crab", "crane", "crocodile", "crow", "curlew", "deer", "dinosaur", + "dog", "dolphin", "domestic pig", "donkey", "dotterel", "dove", "dragonfly", + "duck", "dugong", "dunlin", "eagle", "echidna", "eel", "elephant seal", + "elephant", "elk", "emu", "falcon", "ferret", "finch", "fish", "flamingo", + "fly", "fox", "frog", "gaur", "gazelle", "gerbil", "giant panda", "giraffe", + "gnat", "goat", "goldfish", "goose", "gorilla", "goshawk", "grasshopper", + "grouse", "guanaco", "guinea fowl", "guinea pig", "gull", "hamster", "hare", + "hawk", "hedgehog", "heron", "herring", "hippopotamus", "hornet", "horse", + "human", "hummingbird", "hyena", "ibex", "ibis", "jackal", "jaguar", "jay", + "jellyfish", "kangaroo", "kingfisher", "koala", "komodo dragon", "kookabura", + "kouprey", "kudu", "lapwing", "lark", "lemur", "leopard", "lion", "llama", + "lobster", "locust", "loris", "louse", "lyrebird", "magpie", "mallard", + "manatee", "mandrill", "mantis", "marten", "meerkat", "mink", "mole", + "mongoose", "monkey", "moose", "mosquito", "mouse", "mule", "narwhal", "newt", + "nightingale", "octopus", "okapi", "opossum", "oryx", "ostrich", "otter", + "owl", "oyster", "parrot", "partridge", "peafowl", "pelican", "penguin", + "pheasant", "pigeon", "pinniped", "polar bear", "pony", "porcupine", + "porpoise", "prairie dog", "quail", "quelea", "quetzal", "rabbit", "raccoon", + "ram", "rat", "raven", "red deer", "red panda", "reindeer", "rhinoceros", + "rook", "salamander", "salmon", "sand dollar", "sandpiper", "sardine", + "scorpion", "sea lion", "sea urchin", "seahorse", "shark", "sheep", "shrew", + "skunk", "snail", "snake", "sparrow", "spider", "spoonbill", "squid", + "wallaby", "wildebeest" +]; + +let keys = []; + +for animal in animals { + for adjective in adjectives { + for adverb in adverbs { + keys.push(adverb + " " + adjective + " " + animal) + } + } +} + +let map = #{}; + +let i = 0; + +for key in keys { + map[key] = i; + i += 1; +} + +let sum = 0; + +for key in keys { + sum += map[key]; +} + +for key in keys { + map.remove(key); +} + +print("Sum = " + sum); +print("Finished. Run time = " + now.elapsed + " seconds."); diff --git a/src/optimize.rs b/src/optimize.rs index 20ccfa3c..78613ace 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -385,10 +385,10 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { // ( stmt ) stmt => Expr::Stmt(Box::new((stmt, x.1))), }, - // id = expr + // id op= expr Expr::Assignment(x) => match x.2 { - //id = id2 op= expr2 - Expr::Assignment(x2) if x.1 == "=" => match (x.0, x2.0) { + //id = id2 op= rhs + Expr::Assignment(x2) if x.1.is_empty() => match (x.0, &x2.0) { // var = var op= expr2 -> var op= expr2 (Expr::Variable(a), Expr::Variable(b)) if a.1.is_none() && b.1.is_none() && a.0 == b.0 && a.3 == b.3 => @@ -397,14 +397,10 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { state.set_dirty(); Expr::Assignment(Box::new((Expr::Variable(a), x2.1, optimize_expr(x2.2, state), x.3))) } - // id1 = id2 op= expr2 - (id1, id2) => { - Expr::Assignment(Box::new(( - id1, x.1, Expr::Assignment(Box::new((id2, x2.1, optimize_expr(x2.2, state), x2.3))), x.3, - ))) - } + // expr1 = expr2 op= rhs + (expr1, _) => Expr::Assignment(Box::new((expr1, x.1, optimize_expr(Expr::Assignment(x2), state), x.3))), }, - // id op= expr + // expr = rhs expr => Expr::Assignment(Box::new((x.0, x.1, optimize_expr(expr, state), x.3))), }, From cec6748ac6271cdb6469c7f93765cd31bdd2d067 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Mon, 1 Jun 2020 10:58:14 +0800 Subject: [PATCH 12/12] Remove no_module gates to prepare for plugins. --- README.md | 2 +- src/any.rs | 14 +------- src/engine.rs | 89 +++++++++++++++++++++--------------------------- src/module.rs | 22 +++--------- src/parser.rs | 85 +++++++++++++++++---------------------------- src/scope.rs | 20 +++++++++-- src/token.rs | 8 ++--- tests/modules.rs | 1 + 8 files changed, 99 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index f894ea7b..bd0cc029 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ Optional features | `no_index` | Disable [arrays] and indexing features. | | `no_object` | Disable support for custom types and [object maps]. | | `no_function` | Disable script-defined functions. | -| `no_module` | Disable loading modules. | +| `no_module` | Disable loading external modules. | | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | By default, Rhai includes all the standard functionalities in a small, tight package. diff --git a/src/any.rs b/src/any.rs index f685c303..0b6d81d2 100644 --- a/src/any.rs +++ b/src/any.rs @@ -1,11 +1,9 @@ //! Helper module which defines the `Any` trait to to allow dynamic value handling. +use crate::module::Module; use crate::parser::{ImmutableString, INT}; use crate::r#unsafe::{unsafe_cast_box, unsafe_try_cast}; -#[cfg(not(feature = "no_module"))] -use crate::module::Module; - #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; @@ -160,7 +158,6 @@ pub enum Union { Array(Box), #[cfg(not(feature = "no_object"))] Map(Box), - #[cfg(not(feature = "no_module"))] Module(Box), Variant(Box>), } @@ -198,7 +195,6 @@ impl Dynamic { Union::Array(_) => TypeId::of::(), #[cfg(not(feature = "no_object"))] Union::Map(_) => TypeId::of::(), - #[cfg(not(feature = "no_module"))] Union::Module(_) => TypeId::of::(), Union::Variant(value) => (***value).type_id(), } @@ -218,7 +214,6 @@ impl Dynamic { Union::Array(_) => "array", #[cfg(not(feature = "no_object"))] Union::Map(_) => "map", - #[cfg(not(feature = "no_module"))] Union::Module(_) => "sub-scope", #[cfg(not(feature = "no_std"))] @@ -242,7 +237,6 @@ impl fmt::Display for Dynamic { Union::Array(value) => write!(f, "{:?}", value), #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), - #[cfg(not(feature = "no_module"))] Union::Module(value) => write!(f, "{:?}", value), #[cfg(not(feature = "no_std"))] @@ -266,7 +260,6 @@ impl fmt::Debug for Dynamic { Union::Array(value) => write!(f, "{:?}", value), #[cfg(not(feature = "no_object"))] Union::Map(value) => write!(f, "#{:?}", value), - #[cfg(not(feature = "no_module"))] Union::Module(value) => write!(f, "{:?}", value), #[cfg(not(feature = "no_std"))] @@ -290,7 +283,6 @@ impl Clone for Dynamic { Union::Array(ref value) => Self(Union::Array(value.clone())), #[cfg(not(feature = "no_object"))] Union::Map(ref value) => Self(Union::Map(value.clone())), - #[cfg(not(feature = "no_module"))] Union::Module(ref value) => Self(Union::Module(value.clone())), Union::Variant(ref value) => (***value).clone_into_dynamic(), } @@ -426,7 +418,6 @@ impl Dynamic { Union::Array(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), #[cfg(not(feature = "no_object"))] Union::Map(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), - #[cfg(not(feature = "no_module"))] Union::Module(value) => unsafe_cast_box::<_, T>(value).ok().map(|v| *v), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).ok(), } @@ -470,7 +461,6 @@ impl Dynamic { Union::Array(value) => *unsafe_cast_box::<_, T>(value).unwrap(), #[cfg(not(feature = "no_object"))] Union::Map(value) => *unsafe_cast_box::<_, T>(value).unwrap(), - #[cfg(not(feature = "no_module"))] Union::Module(value) => *unsafe_cast_box::<_, T>(value).unwrap(), Union::Variant(value) => (*value).as_box_any().downcast().map(|x| *x).unwrap(), } @@ -498,7 +488,6 @@ impl Dynamic { Union::Array(value) => (value.as_ref() as &dyn Any).downcast_ref::(), #[cfg(not(feature = "no_object"))] Union::Map(value) => (value.as_ref() as &dyn Any).downcast_ref::(), - #[cfg(not(feature = "no_module"))] Union::Module(value) => (value.as_ref() as &dyn Any).downcast_ref::(), Union::Variant(value) => value.as_ref().as_ref().as_any().downcast_ref::(), } @@ -524,7 +513,6 @@ impl Dynamic { Union::Array(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), #[cfg(not(feature = "no_object"))] Union::Map(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), - #[cfg(not(feature = "no_module"))] Union::Module(value) => (value.as_mut() as &mut dyn Any).downcast_mut::(), Union::Variant(value) => value.as_mut().as_mut_any().downcast_mut::(), } diff --git a/src/engine.rs b/src/engine.rs index 03521f4f..453ee184 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -4,7 +4,7 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; use crate::error::ParseErrorType; use crate::fn_native::{CallableFunction, FnCallArgs, Shared}; -use crate::module::Module; +use crate::module::{resolvers, Module, ModuleResolver}; use crate::optimize::OptimizationLevel; use crate::packages::{CorePackage, Package, PackageLibrary, PackagesCollection, StandardPackage}; use crate::parser::{Expr, FnAccess, FnDef, ImmutableString, ReturnType, Stmt, AST, INT}; @@ -17,9 +17,6 @@ use crate::utils::{StaticVec, StraightHasherBuilder}; #[cfg(not(feature = "no_float"))] use crate::parser::FLOAT; -#[cfg(not(feature = "no_module"))] -use crate::module::{resolvers, ModuleResolver}; - use crate::stdlib::{ any::TypeId, boxed::Box, @@ -297,7 +294,6 @@ pub struct Engine { pub(crate) packages: PackagesCollection, /// A module resolution service. - #[cfg(not(feature = "no_module"))] pub(crate) module_resolver: Option>, /// A hashmap mapping type names to pretty-print names. @@ -350,8 +346,7 @@ impl Default for Engine { #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_std"))] module_resolver: Some(Box::new(resolvers::FileModuleResolver::new())), - #[cfg(not(feature = "no_module"))] - #[cfg(feature = "no_std")] + #[cfg(any(feature = "no_module", feature = "no_std"))] module_resolver: None, type_names: HashMap::new(), @@ -442,31 +437,28 @@ fn search_scope<'s, 'a>( _ => unreachable!(), }; - #[cfg(not(feature = "no_module"))] - { - if let Some(modules) = modules.as_ref() { - let module = if let Some(index) = modules.index() { - scope - .get_mut(scope.len() - index.get()) - .0 - .downcast_mut::() - .unwrap() - } else { - let (id, root_pos) = modules.get(0); + if let Some(modules) = modules.as_ref() { + let module = if let Some(index) = modules.index() { + scope + .get_mut(scope.len() - index.get()) + .0 + .downcast_mut::() + .unwrap() + } else { + let (id, root_pos) = modules.get(0); - scope.find_module(id).ok_or_else(|| { - Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)) - })? - }; + scope + .find_module_internal(id) + .ok_or_else(|| Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)))? + }; - return Ok(( - module.get_qualified_var_mut(name, *hash_var, *pos)?, - name, - // Module variables are constant - ScopeEntryType::Constant, - *pos, - )); - } + return Ok(( + module.get_qualified_var_mut(name, *hash_var, *pos)?, + name, + // Module variables are constant + ScopeEntryType::Constant, + *pos, + )); } let index = if state.always_search { None } else { *index }; @@ -496,8 +488,6 @@ impl Engine { Self { packages: Default::default(), global_module: Default::default(), - - #[cfg(not(feature = "no_module"))] module_resolver: None, type_names: HashMap::new(), @@ -1609,7 +1599,6 @@ impl Engine { } // Module-qualified function call - #[cfg(not(feature = "no_module"))] Expr::FnCall(x) if x.1.is_some() => { let ((name, _, pos), modules, hash_fn_def, args_expr, def_val) = x.as_ref(); let modules = modules.as_ref().unwrap(); @@ -1630,7 +1619,7 @@ impl Engine { .downcast_mut::() .unwrap() } else { - scope.find_module(id).ok_or_else(|| { + scope.find_module_internal(id).ok_or_else(|| { Box::new(EvalAltResult::ErrorModuleNotFound(id.into(), *root_pos)) })? }; @@ -1919,21 +1908,18 @@ impl Engine { // Import statement Stmt::Import(x) => { - #[cfg(feature = "no_module")] - unreachable!(); + let (expr, (name, pos)) = x.as_ref(); - #[cfg(not(feature = "no_module"))] + // Guard against too many modules + if state.modules >= self.max_modules { + return Err(Box::new(EvalAltResult::ErrorTooManyModules(*pos))); + } + + if let Some(path) = self + .eval_expr(scope, state, lib, &expr, level)? + .try_cast::() { - let (expr, (name, pos)) = x.as_ref(); - - // Guard against too many modules - if state.modules >= self.max_modules { - return Err(Box::new(EvalAltResult::ErrorTooManyModules(*pos))); - } - - if let Some(path) = self - .eval_expr(scope, state, lib, &expr, level)? - .try_cast::() + #[cfg(not(feature = "no_module"))] { if let Some(resolver) = &self.module_resolver { // Use an empty scope to create a module @@ -1941,7 +1927,7 @@ impl Engine { resolver.resolve(self, Scope::new(), &path, expr.position())?; let mod_name = unsafe_cast_var_name_to_lifetime(name, &state); - scope.push_module(mod_name, module); + scope.push_module_internal(mod_name, module); state.modules += 1; @@ -1952,9 +1938,12 @@ impl Engine { expr.position(), ))) } - } else { - Err(Box::new(EvalAltResult::ErrorImportExpr(expr.position()))) } + + #[cfg(feature = "no_module")] + Ok(Default::default()) + } else { + Err(Box::new(EvalAltResult::ErrorImportExpr(expr.position()))) } } diff --git a/src/module.rs b/src/module.rs index 9e16e276..cd567a57 100644 --- a/src/module.rs +++ b/src/module.rs @@ -3,7 +3,7 @@ use crate::any::{Dynamic, Variant}; use crate::calc_fn_hash; use crate::engine::{make_getter, make_setter, Engine, FunctionsLib, FUNC_INDEXER}; -use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn}; +use crate::fn_native::{CallableFunction, FnCallArgs, IteratorFn, SendSync}; use crate::parser::{ FnAccess, FnAccess::{Private, Public}, @@ -945,23 +945,7 @@ impl ModuleRef { } /// Trait that encapsulates a module resolution service. -#[cfg(not(feature = "no_module"))] -#[cfg(not(feature = "sync"))] -pub trait ModuleResolver { - /// Resolve a module based on a path string. - fn resolve( - &self, - engine: &Engine, - scope: Scope, - path: &str, - pos: Position, - ) -> Result>; -} - -/// Trait that encapsulates a module resolution service. -#[cfg(not(feature = "no_module"))] -#[cfg(feature = "sync")] -pub trait ModuleResolver: Send + Sync { +pub trait ModuleResolver: SendSync { /// Resolve a module based on a path string. fn resolve( &self, @@ -979,6 +963,8 @@ pub mod resolvers { pub use super::file::FileModuleResolver; pub use super::stat::StaticModuleResolver; } +#[cfg(feature = "no_module")] +pub mod resolvers {} /// Script file-based module resolver. #[cfg(not(feature = "no_module"))] diff --git a/src/parser.rs b/src/parser.rs index 2a20422e..8005acbc 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4,18 +4,12 @@ use crate::any::{Dynamic, Union}; use crate::calc_fn_hash; use crate::engine::{make_getter, make_setter, Engine, FunctionsLib}; use crate::error::{LexError, ParseError, ParseErrorType}; +use crate::module::ModuleRef; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::scope::{EntryType as ScopeEntryType, Scope}; use crate::token::{Position, Token, TokenIterator}; use crate::utils::{StaticVec, StraightHasherBuilder}; -#[cfg(not(feature = "no_module"))] -use crate::module::ModuleRef; - -#[cfg(feature = "no_module")] -#[derive(Debug, Eq, PartialEq, Clone, Hash, Copy, Default)] -pub struct ModuleRef; - use crate::stdlib::{ borrow::Cow, boxed::Box, @@ -644,7 +638,6 @@ impl Expr { Self::Variable(_) => match token { Token::LeftBracket | Token::LeftParen => true, - #[cfg(not(feature = "no_module"))] Token::DoubleColon => true, _ => false, }, @@ -761,27 +754,21 @@ fn parse_call_expr<'a>( Token::RightParen => { eat_token(input, Token::RightParen); - #[cfg(not(feature = "no_module"))] - let hash_fn_def = { - if let Some(modules) = modules.as_mut() { - modules.set_index(state.find_module(&modules.get(0).0)); + let hash_fn_def = if let Some(modules) = modules.as_mut() { + modules.set_index(state.find_module(&modules.get(0).0)); - // Rust functions are indexed in two steps: - // 1) Calculate a hash in a similar manner to script-defined functions, - // i.e. qualifiers + function name + number of arguments. - // 2) Calculate a second hash with no qualifiers, empty function name, - // zero number of arguments, and the actual list of argument `TypeId`'s. - // 3) The final hash is the XOR of the two hashes. - let qualifiers = modules.iter().map(|(m, _)| m.as_str()); - calc_fn_hash(qualifiers, &id, 0, empty()) - } else { - // Qualifiers (none) + function name + no parameters. - calc_fn_hash(empty(), &id, 0, empty()) - } + // Rust functions are indexed in two steps: + // 1) Calculate a hash in a similar manner to script-defined functions, + // i.e. qualifiers + function name + number of arguments. + // 2) Calculate a second hash with no qualifiers, empty function name, + // zero number of arguments, and the actual list of argument `TypeId`'s. + // 3) The final hash is the XOR of the two hashes. + let qualifiers = modules.iter().map(|(m, _)| m.as_str()); + calc_fn_hash(qualifiers, &id, 0, empty()) + } else { + // Qualifiers (none) + function name + no parameters. + calc_fn_hash(empty(), &id, 0, empty()) }; - // Qualifiers (none) + function name + no parameters. - #[cfg(feature = "no_module")] - let hash_fn_def = calc_fn_hash(empty(), &id, 0, empty()); return Ok(Expr::FnCall(Box::new(( (id.into(), false, begin), @@ -803,27 +790,21 @@ fn parse_call_expr<'a>( (Token::RightParen, _) => { eat_token(input, Token::RightParen); - #[cfg(not(feature = "no_module"))] - let hash_fn_def = { - if let Some(modules) = modules.as_mut() { - modules.set_index(state.find_module(&modules.get(0).0)); + let hash_fn_def = if let Some(modules) = modules.as_mut() { + modules.set_index(state.find_module(&modules.get(0).0)); - // Rust functions are indexed in two steps: - // 1) Calculate a hash in a similar manner to script-defined functions, - // i.e. qualifiers + function name + number of arguments. - // 2) Calculate a second hash with no qualifiers, empty function name, - // zero number of arguments, and the actual list of argument `TypeId`'s. - // 3) The final hash is the XOR of the two hashes. - let qualifiers = modules.iter().map(|(m, _)| m.as_str()); - calc_fn_hash(qualifiers, &id, args.len(), empty()) - } else { - // Qualifiers (none) + function name + number of arguments. - calc_fn_hash(empty(), &id, args.len(), empty()) - } + // Rust functions are indexed in two steps: + // 1) Calculate a hash in a similar manner to script-defined functions, + // i.e. qualifiers + function name + number of arguments. + // 2) Calculate a second hash with no qualifiers, empty function name, + // zero number of arguments, and the actual list of argument `TypeId`'s. + // 3) The final hash is the XOR of the two hashes. + let qualifiers = modules.iter().map(|(m, _)| m.as_str()); + calc_fn_hash(qualifiers, &id, args.len(), empty()) + } else { + // Qualifiers (none) + function name + number of arguments. + calc_fn_hash(empty(), &id, args.len(), empty()) }; - // Qualifiers (none) + function name + number of arguments. - #[cfg(feature = "no_module")] - let hash_fn_def = calc_fn_hash(empty(), &id, args.len(), empty()); return Ok(Expr::FnCall(Box::new(( (id.into(), false, begin), @@ -1265,7 +1246,6 @@ fn parse_primary<'a>( } (Expr::Property(_), _) => unreachable!(), // module access - #[cfg(not(feature = "no_module"))] (Expr::Variable(x), Token::DoubleColon) => match input.next().unwrap() { (Token::Identifier(id2), pos2) => { let ((name, pos), mut modules, _, index) = *x; @@ -1293,7 +1273,6 @@ fn parse_primary<'a>( match &mut root_expr { // Cache the hash key for module-qualified variables - #[cfg(not(feature = "no_module"))] Expr::Variable(x) if x.1.is_some() => { let ((name, _), modules, hash, _) = x.as_mut(); let modules = modules.as_mut().unwrap(); @@ -1520,9 +1499,6 @@ fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { - #[cfg(feature = "no_module")] - unreachable!(); - #[cfg(not(feature = "no_module"))] return Err(PERR::PropertyExpected.into_err(x.1.unwrap().get(0).1)); } // lhs.prop @@ -2113,6 +2089,7 @@ fn parse_import<'a>( } /// Parse an export statement. +#[cfg(not(feature = "no_module"))] fn parse_export<'a>( input: &mut Peekable>, state: &mut ParseState, @@ -2294,7 +2271,9 @@ fn parse_stmt<'a>( Token::LeftBrace => parse_block(input, state, breakable, level + 1, allow_stmt_expr), // fn ... + #[cfg(not(feature = "no_function"))] Token::Fn if !is_global => Err(PERR::WrongFnDefinition.into_err(*pos)), + #[cfg(not(feature = "no_function"))] Token::Fn => unreachable!(), Token::If => parse_if(input, state, breakable, level + 1, allow_stmt_expr), @@ -2343,8 +2322,6 @@ fn parse_stmt<'a>( Token::Let => parse_let(input, state, Normal, level + 1, allow_stmt_expr), Token::Const => parse_let(input, state, Constant, level + 1, allow_stmt_expr), - - #[cfg(not(feature = "no_module"))] Token::Import => parse_import(input, state, level + 1, allow_stmt_expr), #[cfg(not(feature = "no_module"))] @@ -2358,6 +2335,7 @@ fn parse_stmt<'a>( } /// Parse a function definition. +#[cfg(not(feature = "no_function"))] fn parse_fn<'a>( input: &mut Peekable>, state: &mut ParseState, @@ -2499,6 +2477,7 @@ fn parse_global_level<'a>( }; match input.peek().unwrap() { + #[cfg(not(feature = "no_function"))] (Token::Fn, _) => { let mut state = ParseState::new(max_expr_depth.1); let func = parse_fn(input, &mut state, access, 0, true)?; diff --git a/src/scope.rs b/src/scope.rs index 1b9c8b1d..50a5694f 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,12 +1,10 @@ //! Module that defines the `Scope` type representing a function call-stack scope. use crate::any::{Dynamic, Union, Variant}; +use crate::module::Module; use crate::parser::{map_dynamic_to_expr, Expr}; use crate::token::Position; -#[cfg(not(feature = "no_module"))] -use crate::module::Module; - use crate::stdlib::{borrow::Cow, boxed::Box, iter, string::String, vec::Vec}; /// Type of an entry in the Scope. @@ -178,6 +176,17 @@ impl<'a> Scope<'a> { /// Modules are used for accessing member variables, functions and plugins under a namespace. #[cfg(not(feature = "no_module"))] pub fn push_module>>(&mut self, name: K, mut value: Module) { + self.push_module_internal(name, value); + } + + /// Add (push) a new module to the Scope. + /// + /// Modules are used for accessing member variables, functions and plugins under a namespace. + pub(crate) fn push_module_internal>>( + &mut self, + name: K, + mut value: Module, + ) { value.index_all_sub_modules(); self.push_dynamic_value( @@ -350,6 +359,11 @@ impl<'a> Scope<'a> { /// Find a module in the Scope, starting from the last entry. #[cfg(not(feature = "no_module"))] pub fn find_module(&mut self, name: &str) -> Option<&mut Module> { + self.find_module_internal(name) + } + + /// Find a module in the Scope, starting from the last entry. + pub(crate) fn find_module_internal(&mut self, name: &str) -> Option<&mut Module> { let index = self.get_module_index(name)?; self.get_mut(index).0.downcast_mut::() } diff --git a/src/token.rs b/src/token.rs index dda005a9..9e6613b0 100644 --- a/src/token.rs +++ b/src/token.rs @@ -181,6 +181,7 @@ pub enum Token { XOr, Ampersand, And, + #[cfg(not(feature = "no_function"))] Fn, Continue, Break, @@ -199,6 +200,7 @@ pub enum Token { PowerOfAssign, Private, Import, + #[cfg(not(feature = "no_module"))] Export, As, LexError(Box), @@ -260,6 +262,7 @@ impl Token { Or => "||", Ampersand => "&", And => "&&", + #[cfg(not(feature = "no_function"))] Fn => "fn", Continue => "continue", Break => "break", @@ -283,6 +286,7 @@ impl Token { PowerOfAssign => "~=", Private => "private", Import => "import", + #[cfg(not(feature = "no_module"))] Export => "export", As => "as", EOF => "{EOF}", @@ -754,12 +758,9 @@ impl<'a> TokenIterator<'a> { "for" => Token::For, "in" => Token::In, "private" => Token::Private, - - #[cfg(not(feature = "no_module"))] "import" => Token::Import, #[cfg(not(feature = "no_module"))] "export" => Token::Export, - #[cfg(not(feature = "no_module"))] "as" => Token::As, #[cfg(not(feature = "no_function"))] @@ -916,7 +917,6 @@ impl<'a> TokenIterator<'a> { } ('=', _) => return Some((Token::Equals, pos)), - #[cfg(not(feature = "no_module"))] (':', ':') => { self.eat_next(); return Some((Token::DoubleColon, pos)); diff --git a/tests/modules.rs b/tests/modules.rs index 61273f53..a063e1bd 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -195,6 +195,7 @@ fn test_module_from_ast() -> Result<(), Box> { let module = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?; let mut scope = Scope::new(); + scope.push_module("testing", module); assert_eq!(