diff --git a/RELEASES.md b/RELEASES.md index c9c1d81e..ffdca9a9 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -2,6 +2,29 @@ Rhai Release Notes ================== +Version 0.19.4 +============== + +This version adds a low-level API for more flexibility when defining custom syntax. + +Bug fixes +--------- + +* Fixes `Send + Sync` for `EvalAltResult` under the `sync` feature. Bug introduced with `0.19.3`. + +Breaking changes +---------------- + +* Custom syntax can no longer start with a keyword (even a _reserved_ one), even if it has been disabled. That is to avoid breaking scripts later when the keyword is no longer disabled. +* `EvalAltResult::ErrorAssignmentToUnknownLHS` is moved to `ParseError::AssignmentToInvalidLHS`. `ParseError::AssignmentToCopy` is removed. + +New features +------------ + +* Low-level API for custom syntax allowing more flexibility in designing the syntax. +* `Module::fill_with` to poly-fill a module with another. + + Version 0.19.3 ============== diff --git a/doc/src/engine/custom-syntax.md b/doc/src/engine/custom-syntax.md index 84463935..61cfae84 100644 --- a/doc/src/engine/custom-syntax.md +++ b/doc/src/engine/custom-syntax.md @@ -54,16 +54,14 @@ These symbol types can be used: * `$ident$` - any [variable] name. -### The First Symbol Must be a Keyword +### The First Symbol Must be an Identifier There is no specific limit on the combination and sequencing of each symbol type, except the _first_ symbol which must be a custom keyword that follows the naming rules of [variables]. -The first symbol also cannot be a reserved [keyword], unless that keyword -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 also cannot be a normal or reserved [keyword]. +In other words, any valid identifier that is not a [keyword] will work fine. ### The First Symbol Must be Unique @@ -118,14 +116,19 @@ The function signature of an implementation is: where: -* `context: &mut EvalContext` - mutable reference to the current evaluation _context_, exposing the following: - * `context.scope: &mut Scope` - mutable reference to the current [`Scope`]; variables can be added to/removed from it. - * `context.engine(): &Engine` - reference to the current [`Engine`]. - * `context.iter_namespaces(): impl Iterator` - iterator of the namespaces (as [modules]) containing all script-defined functions. - * `context.this_ptr(): Option<&Dynamic>` - reference to the current bound [`this`] pointer, if any. - * `context.call_level(): usize` - the current nesting level of function calls. +| Parameter | Type | Description | +| ----------------------------- | :-----------------------------: | ------------------------------------------------------------------------------------- | +| `context` | `&mut EvalContext` | mutable reference to the current evaluation _context_ | +| - `context.scope` | `&mut Scope` | mutable reference to the current [`Scope`]; variables can be added to/removed from it | +| - `context.engine()` | `&Engine` | reference to the current [`Engine`] | +| - `context.iter_namespaces()` | `impl Iterator` | iterator of the namespaces (as [modules]) containing all script-defined functions | +| - `context.this_ptr()` | `Option<&Dynamic>` | reference to the current bound [`this`] pointer, if any | +| - `context.call_level()` | `usize` | the current nesting level of function calls | +| `inputs` | `&[Expression]` | a list of input expression trees | -* `inputs: &[Expression]` - a list of input expression trees. +### Return Value + +Return value is the result of evaluating the custom syntax expression. ### Access Arguments @@ -215,9 +218,9 @@ fn implementation_func( Ok(().into()) } -// Register the custom syntax (sample): do |x| -> { x += 1 } while x < 0; +// Register the custom syntax (sample): exec |x| -> { x += 1 } while x < 0; engine.register_custom_syntax( - &[ "do", "|", "$ident$", "|", "->", "$block$", "while", "$expr$" ], // the custom syntax + &[ "exec", "|", "$ident$", "|", "->", "$block$", "while", "$expr$" ], // the custom syntax 1, // the number of new variables declared within this custom syntax implementation_func )?; @@ -252,3 +255,88 @@ Make sure there are _lots_ of examples for users to follow. Step Six - Profit! ------------------ + + +Really Advanced - Low Level Custom Syntax API +-------------------------------------------- + +Sometimes it is desirable to have multiple custom syntax starting with the +same symbol. This is especially common for _command-style_ syntax where the +second symbol calls a particular command: + +```rust +// The following simulates a command-style syntax, all starting with 'perform'. +perform hello world; // A fixed sequence of symbols +perform action 42; // Perform a system action with a parameter +perform update system; // Update the system +perform check all; // Check all system settings +perform cleanup; // Clean up the system +perform add something; // Add something to the system +perform remove something; // Delete something from the system +``` + +For even more flexibility, there is a _low level_ API for custom syntax that +allows the registration of an entire mini-parser. + +Use `Engine::register_custom_syntax_raw` to register a custom syntax _parser_ +together with the implementation function: + +```rust +engine.register_custom_syntax_raw( + "perform", + |stream| match stream.len() { + // perform ... + 1 => Ok(Some("$ident$".to_string())), + // perform command ... + 2 => match stream[1].as_str() { + "action" => Ok(Some("$expr$".to_string())), + "hello" => Ok(Some("world".to_string())), + "update" | "check" | "add" | "remove" => Ok(Some("$ident$".to_string())), + "cleanup" => Ok(None), + cmd => Err(ParseError(Box::new(ParseErrorType::BadInput( + format!("Improper command: {}", cmd))), + Position::none(), + )), + }, + // perform command arg ... + 3 => match (stream[1].as_str(), stream[2].as_str()) { + ("action", _) => Ok(None), + ("hello", "world") => Ok(None), + ("update", arg) if arg == "system" => Ok(None), + ("update", arg) if arg == "client" => Ok(None), + ("check", arg) => Ok(None), + ("add", arg) => Ok(None), + ("remove", arg) => Ok(None), + (cmd, arg) => Err(ParseError(Box::new(ParseErrorType::BadInput( + format!("Invalid argument for command {}: {}", cmd, arg))), + Position::none(), + )), + }, + _ => unreachable!(), + }, + 0, // the number of new variables declared within this custom syntax + implementation_func +); +``` + +### Function Signature + +The custom syntax parser has the following signature: + +> `Fn(stream: &[String]) -> Result, ParseError>` + +where: + +| Parameter | Type | Description | +| --------- | :---------: | -------------------------------------------------------------------------------------------------- | +| `stream` | `&[String]` | a slice of symbols that have been parsed so far, possibly containing `"$expr$"` and/or `"$block$"` | + +### Return Value + +The return value is `Result, ParseError>` where: + +| Value | Description | +| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Ok(None)` | parsing complete and there are no more symbols to match | +| `Ok(Some(symbol))` | next symbol to match, which can also be `"$expr$"`, `"$ident$"` or `"$block$"` | +| `Err(ParseError)` | error that is reflected back to the [`Engine`].
Normally this is `ParseError(ParseErrorType::BadInput(message), Position::none())` to indicate that there is a syntax error, but it can be any `ParseError`. | diff --git a/doc/src/engine/var.md b/doc/src/engine/var.md index 270f122e..dd661f8b 100644 --- a/doc/src/engine/var.md +++ b/doc/src/engine/var.md @@ -16,7 +16,7 @@ To do so, provide a closure to the [`Engine`] via the `Engine::on_var` method: let mut engine = Engine::new(); // Register a variable resolver. -engine.on_var(|name, index, scope, context| { +engine.on_var(|name, index, context| { match name { "MYSTIC_NUMBER" => Ok(Some((42 as INT).into())), // Override a variable - make it not found even if it exists! @@ -24,7 +24,7 @@ engine.on_var(|name, index, scope, context| { EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::none()) )), // Silently maps 'chameleon' into 'innocent'. - "chameleon" => scope.get_value("innocent").map(Some).ok_or_else(|| Box::new( + "chameleon" => context.scope.get_value("innocent").map(Some).ok_or_else(|| Box::new( EvalAltResult::ErrorVariableNotFound(name.to_string(), Position::none()) )), // Return Ok(None) to continue with the normal variable resolution process. @@ -67,27 +67,23 @@ The function signature passed to `Engine::on_var` takes the following form: where: -* `name: &str` - variable name. - -* `index: usize` - an offset from the bottom of the current [`Scope`] that the variable is supposed to reside. - Offsets start from 1, with 1 meaning the last variable in the current [`Scope`]. Essentially the correct variable is at position `scope.len() - index`. - - If `index` is zero, then there is no pre-calculated offset position and a search through the current [`Scope`] must be performed. - -* `context: &EvalContext` - reference to the current evaluation _context_, which exposes the following fields: - * `context.scope: &Scope` - reference to the current [`Scope`] containing all variables up to the current evaluation position. - * `context.engine(): &Engine` - reference to the current [`Engine`]. - * `context.iter_namespaces(): impl Iterator` - iterator of the namespaces (as [modules]) containing all script-defined functions. - * `context.this_ptr(): Option<&Dynamic>` - reference to the current bound [`this`] pointer, if any. - * `context.call_level(): usize` - the current nesting level of function calls. +| Parameter | Type | Description | +| ----------------------------- | :-----------------------------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | `&str` | variable name | +| `index` | `usize` | an offset from the bottom of the current [`Scope`] that the variable is supposed to reside.
Offsets start from 1, with 1 meaning the last variable in the current [`Scope`]. Essentially the correct variable is at position `scope.len() - index`.
If `index` is zero, then there is no pre-calculated offset position and a search through the current [`Scope`] must be performed. | +| `context` | `&EvalContext` | reference to the current evaluation _context_ | +| - `context.scope` | `&Scope` | reference to the current [`Scope`] containing all variables up to the current evaluation position | +| - `context.engine()` | `&Engine` | reference to the current [`Engine`] | +| - `context.iter_namespaces()` | `impl Iterator` | iterator of the namespaces (as [modules]) containing all script-defined functions | +| - `context.this_ptr()` | `Option<&Dynamic>` | reference to the current bound [`this`] pointer, if any | +| - `context.call_level()` | `usize` | the current nesting level of function calls | ### Return Value The return value is `Result, Box>` where: -* `Ok(None)` - normal variable resolution process should continue, meaning to continue searching through the [`Scope`]. - -* `Ok(Some(Dynamic))` - wrapped [`Dynamic`] is taken as the value of the variable, which is treated as a constant. - -* `Err(Box)` - error is reflected back to the [`Engine`]. - Normally this is `EvalAltResult::ErrorVariableNotFound` to indicate that the variable does not exist, but it can be any error. +| Value | Description | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `Ok(None)` | normal variable resolution process should continue, i.e. continue searching through the [`Scope`] | +| `Ok(Some(Dynamic))` | value of the variable, treated as a constant | +| `Err(Box)` | error that is reflected back to the [`Engine`].
Normally this is `EvalAltResult::ErrorVariableNotFound(var_name, Position::none())` to indicate that the variable does not exist, but it can be any `EvalAltResult`. | diff --git a/doc/src/rust/register-raw.md b/doc/src/rust/register-raw.md index 8e848734..38f9b4b1 100644 --- a/doc/src/rust/register-raw.md +++ b/doc/src/rust/register-raw.md @@ -65,16 +65,17 @@ The function signature passed to `Engine::register_raw_fn` takes the following f where: -* `T: Clone` - return type of the function. +| Parameter | Type | Description | +| ----------------------------- | :-----------------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `T` | `impl Clone` | return type of the function | +| `context` | `NativeCallContext` | the current _native call context_ | +| - `context.engine()` | `&Engine` | the current [`Engine`], with all configurations and settings.
This is sometimes useful for calling a script-defined function within the same evaluation context using [`Engine::call_fn`][`call_fn`], or calling a [function pointer]. | +| - `context.iter_namespaces()` | `impl Iterator` | iterator of the namespaces (as [modules]) containing all script-defined functions | +| `args` | `&mut [&mut Dynamic]` | a slice containing `&mut` references to [`Dynamic`] values.
The slice is guaranteed to contain enough arguments _of the correct types_. | -* `context: NativeCallContext` - the current _native call context_, which exposes the following: +### Return value - * `context.engine(): &Engine` - the current [`Engine`], with all configurations and settings. - This is sometimes useful for calling a script-defined function within the same evaluation context using [`Engine::call_fn`][`call_fn`], or calling a [function pointer]. - * `context.iter_namespaces(): impl Iterator` - iterator of the namespaces (as [modules]) containing all script-defined functions. - -* `args: &mut [&mut Dynamic]` - a slice containing `&mut` references to [`Dynamic`] values. - The slice is guaranteed to contain enough arguments _of the correct types_. +The return value is the result of the function call. Remember, in Rhai, all arguments _except_ the _first_ one are always passed by _value_ (i.e. cloned). Therefore, it is unnecessary to ever mutate any argument except the first one, as all mutations @@ -116,16 +117,16 @@ let mut engine = Engine::new(); engine.register_raw_fn( "bar", &[ - std::any::TypeId::of::(), // parameter types + std::any::TypeId::of::(), // parameter types std::any::TypeId::of::(), std::any::TypeId::of::(), ], |context, args| { // 'args' is guaranteed to contain enough arguments of the correct types - let fp = std::mem::take(args[1]).cast::(); // 2nd argument - function pointer - let value = args[2].clone(); // 3rd argument - function argument - let this_ptr = args.get_mut(0).unwrap(); // 1st argument - this pointer + let fp = std::mem::take(args[1]).cast::(); // 2nd argument - function pointer + let value = args[2].clone(); // 3rd argument - function argument + let this_ptr = args.get_mut(0).unwrap(); // 1st argument - this pointer // Use 'FnPtr::call_dynamic' to call the function pointer. // Beware, private script-defined functions will not be found. @@ -133,13 +134,14 @@ engine.register_raw_fn( }, ); -let result = engine.eval::(r#" +let result = engine.eval::( + r#" fn foo(x) { this += x; } // script-defined function 'foo' let x = 41; // object x.bar(Fn("foo"), 1); // pass 'foo' as function pointer x -"#)?; + "#)?; ``` @@ -156,7 +158,8 @@ Shared values are implemented as `Rc>` (`Arc>` If the value is _not_ a shared value, or if running under [`no_closure`] where there is no [capturing][automatic currying], this API de-sugars to a simple `Dynamic::downcast_ref` and -`Dynamic::downcast_mut`. +`Dynamic::downcast_mut`. In other words, there is no locking and reference counting overhead +for the vast majority of non-shared values. If the value is a shared value, then it is first locked and the returned lock guard then allows access to the underlying value in the specified type. diff --git a/examples/repl.rs b/examples/repl.rs index f6999b6a..bd0691ef 100644 --- a/examples/repl.rs +++ b/examples/repl.rs @@ -120,16 +120,17 @@ fn main() { *value.read_lock::().unwrap(), ) }); + println!(); continue; } "astu" => { // print the last un-optimized AST - println!("{:#?}", &ast_u); + println!("{:#?}\n", &ast_u); continue; } "ast" => { // print the last AST - println!("{:#?}", &ast); + println!("{:#?}\n", &ast); continue; } _ => (), diff --git a/src/api.rs b/src/api.rs index cfb1199a..bfa0abfa 100644 --- a/src/api.rs +++ b/src/api.rs @@ -164,14 +164,8 @@ impl Engine { #[cfg(not(feature = "no_object"))] #[inline(always)] pub fn register_type_with_name(&mut self, name: &str) -> &mut Self { - if self.type_names.is_none() { - self.type_names = Some(Default::default()); - } // Add the pretty-print type name into the map - self.type_names - .as_mut() - .unwrap() - .insert(type_name::().into(), name.into()); + self.type_names.insert(type_name::().into(), name.into()); self } diff --git a/src/engine.rs b/src/engine.rs index e7de12bd..2bc062f4 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -6,7 +6,7 @@ use crate::fn_native::{Callback, FnPtr, OnVarCallback}; use crate::module::{Module, ModuleRef}; use crate::optimize::OptimizationLevel; use crate::packages::{Package, PackagesCollection, StandardPackage}; -use crate::parser::{Expr, ReturnType, Stmt}; +use crate::parser::{BinaryExpr, Expr, ReturnType, Stmt}; use crate::r#unsafe::unsafe_cast_var_name_to_lifetime; use crate::result::EvalAltResult; use crate::scope::{EntryType as ScopeEntryType, Scope}; @@ -302,26 +302,19 @@ impl<'a> Target<'a> { #[cfg(not(feature = "no_index"))] Self::StringChar(_, _, ch) => { let char_value = ch.clone(); - self.set_value((char_value, Position::none()), Position::none()) - .unwrap(); + self.set_value((char_value, Position::none())).unwrap(); } } } /// Update the value of the `Target`. #[cfg(any(not(feature = "no_object"), not(feature = "no_index")))] - pub fn set_value( - &mut self, - new_val: (Dynamic, Position), - target_pos: Position, - ) -> Result<(), Box> { + pub fn set_value(&mut self, new_val: (Dynamic, Position)) -> Result<(), Box> { match self { Self::Ref(r) => **r = new_val.0, #[cfg(not(feature = "no_closure"))] #[cfg(not(feature = "no_object"))] Self::LockGuard((r, _)) => **r = new_val.0, - Self::Value(_) => { - return EvalAltResult::ErrorAssignmentToUnknownLHS(target_pos).into(); - } + Self::Value(_) => unreachable!(), #[cfg(not(feature = "no_index"))] Self::StringChar(string, index, _) if string.is::() => { let mut s = string.write_lock::().unwrap(); @@ -484,6 +477,14 @@ impl<'e, 'x, 'px, 'a, 's, 'm, 'pm, 't, 'pt> EvalContext<'e, 'x, 'px, 'a, 's, 'm, /// Rhai main scripting engine. /// +/// # Thread Safety +/// +/// `Engine` is re-entrant. +/// +/// Currently, `Engine` is neither `Send` nor `Sync`. Use the `sync` feature to make it `Send + Sync`. +/// +/// # Example +/// /// ``` /// # fn main() -> Result<(), Box> { /// use rhai::Engine; @@ -496,11 +497,9 @@ impl<'e, 'x, 'px, 'a, 's, 'm, 'pm, 't, 'pt> EvalContext<'e, 'x, 'px, 'a, 's, 'm, /// # Ok(()) /// # } /// ``` -/// -/// Currently, `Engine` is neither `Send` nor `Sync`. Use the `sync` feature to make it `Send + Sync`. pub struct Engine { /// A unique ID identifying this scripting `Engine`. - pub id: Option, + pub id: String, /// A module containing all functions directly loaded into the Engine. pub(crate) global_module: Module, @@ -512,14 +511,14 @@ pub struct Engine { pub(crate) module_resolver: Option>, /// A hashmap mapping type names to pretty-print names. - pub(crate) type_names: Option>, + pub(crate) type_names: HashMap, /// A hashset containing symbols to disable. - pub(crate) disabled_symbols: Option>, + pub(crate) disabled_symbols: HashSet, /// A hashset containing custom keywords and precedence to recognize. - pub(crate) custom_keywords: Option>, + pub(crate) custom_keywords: HashMap>, /// Custom syntax. - pub(crate) custom_syntax: Option>, + pub(crate) custom_syntax: HashMap, /// Callback closure for resolving variable access. pub(crate) resolve_var: Option, @@ -541,9 +540,10 @@ pub struct Engine { impl fmt::Debug for Engine { #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.id.as_ref() { - Some(id) => write!(f, "Engine({})", id), - None => f.write_str("Engine"), + if !self.id.is_empty() { + write!(f, "Engine({})", self.id) + } else { + f.write_str("Engine") } } } @@ -645,7 +645,7 @@ impl Engine { pub fn new() -> Self { // Create the new scripting Engine let mut engine = Self { - id: None, + id: Default::default(), packages: Default::default(), global_module: Default::default(), @@ -658,10 +658,10 @@ impl Engine { #[cfg(any(feature = "no_std", target_arch = "wasm32",))] module_resolver: None, - type_names: None, - disabled_symbols: None, - custom_keywords: None, - custom_syntax: None, + type_names: Default::default(), + disabled_symbols: Default::default(), + custom_keywords: Default::default(), + custom_syntax: Default::default(), // variable resolver resolve_var: None, @@ -707,7 +707,7 @@ impl Engine { #[inline(always)] pub fn new_raw() -> Self { Self { - id: None, + id: Default::default(), packages: Default::default(), global_module: Default::default(), @@ -715,10 +715,10 @@ impl Engine { #[cfg(not(feature = "no_module"))] module_resolver: None, - type_names: None, - disabled_symbols: None, - custom_keywords: None, - custom_syntax: None, + type_names: Default::default(), + disabled_symbols: Default::default(), + custom_keywords: Default::default(), + custom_syntax: Default::default(), resolve_var: None, @@ -894,18 +894,17 @@ impl Engine { match rhs { // xxx[idx].expr... | xxx[idx][expr]... Expr::Dot(x) | Expr::Index(x) => { - let (idx, expr, pos) = x.as_ref(); - let idx_pos = idx.position(); + let idx_pos = x.lhs.position(); let idx_val = idx_val.as_value(); let obj_ptr = &mut self.get_indexed_mut( state, lib, target, idx_val, idx_pos, false, true, level, )?; self.eval_dot_index_chain_helper( - state, lib, this_ptr, obj_ptr, expr, idx_values, next_chain, level, + state, lib, this_ptr, obj_ptr, &x.rhs, idx_values, next_chain, level, new_val, ) - .map_err(|err| err.fill_position(*pos)) + .map_err(|err| err.fill_position(x.pos)) } // xxx[rhs] = new_val _ if new_val.is_some() => { @@ -918,7 +917,7 @@ impl Engine { { // Indexed value is a reference - update directly Ok(ref mut obj_ptr) => { - obj_ptr.set_value(new_val.unwrap(), rhs.position())?; + obj_ptr.set_value(new_val.unwrap())?; None } Err(err) => match *err { @@ -986,7 +985,7 @@ impl Engine { let mut val = self .get_indexed_mut(state, lib, target, index, *pos, true, false, level)?; - val.set_value(new_val.unwrap(), rhs.position())?; + val.set_value(new_val.unwrap())?; Ok((Default::default(), true)) } // {xxx:map}.id @@ -1024,9 +1023,7 @@ impl Engine { } // {xxx:map}.sub_lhs[expr] | {xxx:map}.sub_lhs.expr Expr::Index(x) | Expr::Dot(x) if target.is::() => { - let (sub_lhs, expr, pos) = x.as_ref(); - - let mut val = match sub_lhs { + let mut val = match &x.lhs { Expr::Property(p) => { let ((prop, _, _), pos) = p.as_ref(); let index = prop.clone().into(); @@ -1054,16 +1051,14 @@ impl Engine { }; self.eval_dot_index_chain_helper( - state, lib, this_ptr, &mut val, expr, idx_values, next_chain, level, + state, lib, this_ptr, &mut val, &x.rhs, idx_values, next_chain, level, new_val, ) - .map_err(|err| err.fill_position(*pos)) + .map_err(|err| err.fill_position(x.pos)) } // xxx.sub_lhs[expr] | xxx.sub_lhs.expr Expr::Index(x) | Expr::Dot(x) => { - let (sub_lhs, expr, _) = x.as_ref(); - - match sub_lhs { + match &x.lhs { // xxx.prop[expr] | xxx.prop.expr Expr::Property(p) => { let ((_, getter, setter), pos) = p.as_ref(); @@ -1084,13 +1079,13 @@ impl Engine { lib, this_ptr, &mut val.into(), - expr, + &x.rhs, idx_values, next_chain, level, new_val, ) - .map_err(|err| err.fill_position(*pos))?; + .map_err(|err| err.fill_position(x.pos))?; // Feed the value back via a setter just in case it has been updated if updated || may_be_changed { @@ -1106,7 +1101,7 @@ impl Engine { EvalAltResult::ErrorDotExpr(_, _) => { Ok(Default::default()) } - _ => Err(err.fill_position(*pos)), + _ => Err(err.fill_position(x.pos)), }, )?; } @@ -1114,8 +1109,8 @@ impl Engine { Ok((result, may_be_changed)) } // xxx.fn_name(arg_expr_list)[expr] | xxx.fn_name(arg_expr_list).expr - Expr::FnCall(x) if x.1.is_none() => { - let ((name, native, _, pos), _, hash, _, def_val) = x.as_ref(); + Expr::FnCall(f) if f.1.is_none() => { + let ((name, native, _, pos), _, hash, _, def_val) = f.as_ref(); let def_val = def_val.map(Into::::into); let args = idx_val.as_fn_call_args(); let (mut val, _) = self @@ -1128,7 +1123,7 @@ impl Engine { let target = &mut val.into(); self.eval_dot_index_chain_helper( - state, lib, this_ptr, target, expr, idx_values, next_chain, + state, lib, this_ptr, target, &x.rhs, idx_values, next_chain, level, new_val, ) .map_err(|err| err.fill_position(*pos)) @@ -1161,7 +1156,14 @@ impl Engine { level: usize, new_val: Option<(Dynamic, Position)>, ) -> Result> { - let ((dot_lhs, dot_rhs, op_pos), chain_type) = match expr { + let ( + BinaryExpr { + lhs: dot_lhs, + rhs: dot_rhs, + pos: op_pos, + }, + chain_type, + ) = match expr { Expr::Index(x) => (x.as_ref(), ChainType::Index), Expr::Dot(x) => (x.as_ref(), ChainType::Dot), _ => unreachable!(), @@ -1210,9 +1212,7 @@ impl Engine { .map_err(|err| err.fill_position(*op_pos)) } // {expr}.??? = ??? or {expr}[???] = ??? - expr if new_val.is_some() => { - return EvalAltResult::ErrorAssignmentToUnknownLHS(expr.position()).into(); - } + _ if new_val.is_some() => unreachable!(), // {expr}.??? or {expr}[???] expr => { let val = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; @@ -1259,7 +1259,7 @@ impl Engine { Expr::FnCall(_) => unreachable!(), Expr::Property(_) => idx_values.push(IndexChainValue::None), Expr::Index(x) | Expr::Dot(x) => { - let (lhs, rhs, _) = x.as_ref(); + let BinaryExpr { lhs, rhs, .. } = x.as_ref(); // Evaluate in left-to-right order let lhs_val = match lhs { @@ -1511,9 +1511,146 @@ impl Engine { // Statement block Expr::Stmt(x) => self.eval_stmt(scope, mods, state, lib, this_ptr, &x.0, level), + // lhs[idx_expr] + #[cfg(not(feature = "no_index"))] + Expr::Index(_) => { + self.eval_dot_index_chain(scope, mods, state, lib, this_ptr, expr, level, None) + } + + // lhs.dot_rhs + #[cfg(not(feature = "no_object"))] + Expr::Dot(_) => { + self.eval_dot_index_chain(scope, mods, state, lib, this_ptr, expr, level, None) + } + + #[cfg(not(feature = "no_index"))] + Expr::Array(x) => Ok(Dynamic(Union::Array(Box::new( + x.0.iter() + .map(|item| self.eval_expr(scope, mods, state, lib, this_ptr, item, level)) + .collect::, _>>()?, + )))), + + #[cfg(not(feature = "no_object"))] + Expr::Map(x) => Ok(Dynamic(Union::Map(Box::new( + x.0.iter() + .map(|((key, _), expr)| { + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) + .map(|val| (key.clone(), val)) + }) + .collect::, _>>()?, + )))), + + // Normal function call + Expr::FnCall(x) if x.1.is_none() => { + let ((name, native, cap_scope, pos), _, hash, args_expr, def_val) = x.as_ref(); + let def_val = def_val.map(Into::::into); + self.make_function_call( + scope, mods, state, lib, this_ptr, name, args_expr, &def_val, *hash, *native, + false, *cap_scope, level, + ) + .map_err(|err| err.fill_position(*pos)) + } + + // Module-qualified function call + Expr::FnCall(x) if x.1.is_some() => { + let ((name, _, _, pos), modules, hash, args_expr, def_val) = x.as_ref(); + self.make_qualified_function_call( + scope, mods, state, lib, this_ptr, modules, name, args_expr, *def_val, *hash, + level, + ) + .map_err(|err| err.fill_position(*pos)) + } + + Expr::In(x) => { + self.eval_in_expr(scope, mods, state, lib, this_ptr, &x.lhs, &x.rhs, level) + } + + Expr::And(x) => { + Ok((self + .eval_expr(scope, mods, state, lib, this_ptr, &x.lhs, level)? + .as_bool() + .map_err(|err| self.make_type_mismatch_err::(err, x.lhs.position()))? + && // Short-circuit using && + self + .eval_expr(scope, mods, state, lib, this_ptr, &x.rhs, level)? + .as_bool() + .map_err(|err| self.make_type_mismatch_err::(err, x.rhs.position()))?) + .into()) + } + + Expr::Or(x) => { + Ok((self + .eval_expr(scope, mods, state, lib, this_ptr, &x.lhs, level)? + .as_bool() + .map_err(|err| self.make_type_mismatch_err::(err, x.lhs.position()))? + || // Short-circuit using || + self + .eval_expr(scope, mods, state, lib, this_ptr, &x.rhs, level)? + .as_bool() + .map_err(|err| self.make_type_mismatch_err::(err, x.rhs.position()))?) + .into()) + } + + Expr::True(_) => Ok(true.into()), + Expr::False(_) => Ok(false.into()), + Expr::Unit(_) => Ok(().into()), + + Expr::Custom(custom) => { + let func = custom.func(); + let expressions = custom + .keywords() + .iter() + .map(Into::into) + .collect::>(); + let mut context = EvalContext { + engine: self, + scope, + mods, + state, + lib, + this_ptr, + level, + }; + func(&mut context, &expressions) + } + + _ => unreachable!(), + }; + + self.check_data_size(result) + .map_err(|err| err.fill_position(expr.position())) + } + + /// Evaluate a statement + /// + /// + /// # Safety + /// + /// This method uses some unsafe code, mainly for avoiding cloning of local variable names via + /// direct lifetime casting. + pub(crate) fn eval_stmt( + &self, + scope: &mut Scope, + mods: &mut Imports, + state: &mut State, + lib: &[&Module], + this_ptr: &mut Option<&mut Dynamic>, + stmt: &Stmt, + level: usize, + ) -> Result> { + self.inc_operations(state) + .map_err(|err| err.fill_position(stmt.position()))?; + + let result = match stmt { + // No-op + Stmt::Noop(_) => Ok(Default::default()), + + // Expression as statement + Stmt::Expr(expr) => self.eval_expr(scope, mods, state, lib, this_ptr, expr, level), + // var op= rhs - Expr::Assignment(x) if x.0.get_variable_access(false).is_some() => { - let (lhs_expr, op, rhs_expr, op_pos) = x.as_ref(); + Stmt::Assignment(x, op_pos) if x.0.get_variable_access(false).is_some() => { + let (lhs_expr, op, rhs_expr) = x.as_ref(); let mut rhs_val = self .eval_expr(scope, mods, state, lib, this_ptr, rhs_expr, level)? .flatten(); @@ -1611,8 +1748,8 @@ impl Engine { } // lhs op= rhs - Expr::Assignment(x) => { - let (lhs_expr, op, rhs_expr, op_pos) = x.as_ref(); + Stmt::Assignment(x, op_pos) => { + let (lhs_expr, op, rhs_expr) = x.as_ref(); let mut rhs_val = self.eval_expr(scope, mods, state, lib, this_ptr, rhs_expr, level)?; @@ -1657,157 +1794,18 @@ impl Engine { )?; Ok(Default::default()) } - // Constant expression (should be caught during parsing) - expr if expr.is_constant() => unreachable!(), - // Syntax error - expr => EvalAltResult::ErrorAssignmentToUnknownLHS(expr.position()).into(), + // Non-lvalue expression (should be caught during parsing) + _ => unreachable!(), } } - // lhs[idx_expr] - #[cfg(not(feature = "no_index"))] - Expr::Index(_) => { - self.eval_dot_index_chain(scope, mods, state, lib, this_ptr, expr, level, None) - } - - // lhs.dot_rhs - #[cfg(not(feature = "no_object"))] - Expr::Dot(_) => { - self.eval_dot_index_chain(scope, mods, state, lib, this_ptr, expr, level, None) - } - - #[cfg(not(feature = "no_index"))] - Expr::Array(x) => Ok(Dynamic(Union::Array(Box::new( - x.0.iter() - .map(|item| self.eval_expr(scope, mods, state, lib, this_ptr, item, level)) - .collect::, _>>()?, - )))), - - #[cfg(not(feature = "no_object"))] - Expr::Map(x) => Ok(Dynamic(Union::Map(Box::new( - x.0.iter() - .map(|((key, _), expr)| { - self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) - .map(|val| (key.clone(), val)) - }) - .collect::, _>>()?, - )))), - - // Normal function call - Expr::FnCall(x) if x.1.is_none() => { - let ((name, native, cap_scope, pos), _, hash, args_expr, def_val) = x.as_ref(); - let def_val = def_val.map(Into::::into); - self.make_function_call( - scope, mods, state, lib, this_ptr, name, args_expr, &def_val, *hash, *native, - false, *cap_scope, level, - ) - .map_err(|err| err.fill_position(*pos)) - } - - // Module-qualified function call - Expr::FnCall(x) if x.1.is_some() => { - let ((name, _, _, pos), modules, hash, args_expr, def_val) = x.as_ref(); - self.make_qualified_function_call( - scope, mods, state, lib, this_ptr, modules, name, args_expr, *def_val, *hash, - level, - ) - .map_err(|err| err.fill_position(*pos)) - } - - Expr::In(x) => self.eval_in_expr(scope, mods, state, lib, this_ptr, &x.0, &x.1, level), - - Expr::And(x) => { - let (lhs, rhs, _) = x.as_ref(); - Ok((self - .eval_expr(scope, mods, state, lib, this_ptr, lhs, level)? - .as_bool() - .map_err(|err| self.make_type_mismatch_err::(err, lhs.position()))? - && // Short-circuit using && - self - .eval_expr(scope, mods, state, lib, this_ptr, rhs, level)? - .as_bool() - .map_err(|err| self.make_type_mismatch_err::(err, rhs.position()))?) - .into()) - } - - Expr::Or(x) => { - let (lhs, rhs, _) = x.as_ref(); - Ok((self - .eval_expr(scope, mods, state, lib, this_ptr, lhs, level)? - .as_bool() - .map_err(|err| self.make_type_mismatch_err::(err, lhs.position()))? - || // Short-circuit using || - self - .eval_expr(scope, mods, state, lib, this_ptr, rhs, level)? - .as_bool() - .map_err(|err| self.make_type_mismatch_err::(err, rhs.position()))?) - .into()) - } - - Expr::True(_) => Ok(true.into()), - Expr::False(_) => Ok(false.into()), - Expr::Unit(_) => Ok(().into()), - - Expr::Custom(x) => { - let func = (x.0).func(); - let expressions = (x.0) - .keywords() - .iter() - .map(Into::into) - .collect::>(); - let mut context = EvalContext { - engine: self, - scope, - mods, - state, - lib, - this_ptr, - level, - }; - func(&mut context, &expressions) - } - - _ => unreachable!(), - }; - - self.check_data_size(result) - .map_err(|err| err.fill_position(expr.position())) - } - - /// Evaluate a statement - /// - /// - /// # Safety - /// - /// This method uses some unsafe code, mainly for avoiding cloning of local variable names via - /// direct lifetime casting. - pub(crate) fn eval_stmt( - &self, - scope: &mut Scope, - mods: &mut Imports, - state: &mut State, - lib: &[&Module], - this_ptr: &mut Option<&mut Dynamic>, - stmt: &Stmt, - level: usize, - ) -> Result> { - self.inc_operations(state) - .map_err(|err| err.fill_position(stmt.position()))?; - - let result = match stmt { - // No-op - Stmt::Noop(_) => Ok(Default::default()), - - // Expression as statement - Stmt::Expr(expr) => self.eval_expr(scope, mods, state, lib, this_ptr, expr, level), - // Block scope - Stmt::Block(x) => { + Stmt::Block(statements, _) => { let prev_scope_len = scope.len(); let prev_mods_len = mods.len(); state.scope_level += 1; - let result = x.0.iter().try_fold(Default::default(), |_, stmt| { + let result = statements.iter().try_fold(Default::default(), |_, stmt| { self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level) }); @@ -1823,9 +1821,8 @@ impl Engine { } // If-else statement - Stmt::IfThenElse(x) => { - let (expr, if_block, else_block, _) = x.as_ref(); - + Stmt::IfThenElse(expr, x, _) => { + let (if_block, else_block) = x.as_ref(); self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)? .as_bool() .map_err(|err| self.make_type_mismatch_err::(err, expr.position())) @@ -1841,9 +1838,7 @@ impl Engine { } // While loop - Stmt::While(x) => loop { - let (expr, body, _) = x.as_ref(); - + Stmt::While(expr, body, _) => loop { match self .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? .as_bool() @@ -1866,8 +1861,8 @@ impl Engine { }, // Loop statement - Stmt::Loop(x) => loop { - match self.eval_stmt(scope, mods, state, lib, this_ptr, &x.0, level) { + Stmt::Loop(block, _) => loop { + match self.eval_stmt(scope, mods, state, lib, this_ptr, block, level) { Ok(_) => (), Err(err) => match *err { EvalAltResult::LoopBreak(false, _) => (), @@ -1878,8 +1873,8 @@ impl Engine { }, // For loop - Stmt::For(x) => { - let (name, expr, stmt, _) = x.as_ref(); + Stmt::For(expr, x, _) => { + let (name, stmt) = x.as_ref(); let iter_obj = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; let iter_type = iter_obj.type_id(); @@ -1922,7 +1917,7 @@ impl Engine { scope.rewind(scope.len() - 1); Ok(Default::default()) } else { - EvalAltResult::ErrorFor(x.1.position()).into() + EvalAltResult::ErrorFor(expr.position()).into() } } @@ -1934,17 +1929,17 @@ impl Engine { // Try/Catch statement Stmt::TryCatch(x) => { - let ((body, _), var_def, (catch_body, _)) = x.as_ref(); + let ((try_body, _), var_def, (catch_body, _)) = x.as_ref(); let result = self - .eval_stmt(scope, mods, state, lib, this_ptr, body, level) + .eval_stmt(scope, mods, state, lib, this_ptr, try_body, level) .map(|_| ().into()); match result { Ok(_) => result, Err(err) => match *err { mut err @ EvalAltResult::ErrorRuntime(_, _) | mut err - if err.catchable() => + if err.is_catchable() => { let value = if let EvalAltResult::ErrorRuntime(ref x, _) = err { x.clone() @@ -1985,40 +1980,33 @@ impl Engine { } // Return value - Stmt::ReturnWithVal(x) if x.1.is_some() && (x.0).0 == ReturnType::Return => { - let expr = x.1.as_ref().unwrap(); - EvalAltResult::Return( - self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?, - (x.0).1, - ) - .into() - } + Stmt::ReturnWithVal((ReturnType::Return, pos), Some(expr), _) => EvalAltResult::Return( + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?, + *pos, + ) + .into(), // Empty return - Stmt::ReturnWithVal(x) if (x.0).0 == ReturnType::Return => { - EvalAltResult::Return(Default::default(), (x.0).1).into() + Stmt::ReturnWithVal((ReturnType::Return, pos), None, _) => { + EvalAltResult::Return(Default::default(), *pos).into() } // Throw value - Stmt::ReturnWithVal(x) if x.1.is_some() && (x.0).0 == ReturnType::Exception => { - let expr = x.1.as_ref().unwrap(); + Stmt::ReturnWithVal((ReturnType::Exception, pos), Some(expr), _) => { let val = self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)?; - EvalAltResult::ErrorRuntime(val, (x.0).1).into() + EvalAltResult::ErrorRuntime(val, *pos).into() } // Empty throw - Stmt::ReturnWithVal(x) if (x.0).0 == ReturnType::Exception => { - EvalAltResult::ErrorRuntime(().into(), (x.0).1).into() + Stmt::ReturnWithVal((ReturnType::Exception, pos), None, _) => { + EvalAltResult::ErrorRuntime(().into(), *pos).into() } - Stmt::ReturnWithVal(_) => unreachable!(), - // Let/const statement - Stmt::Let(x) | Stmt::Const(x) => { - let ((var_name, _), expr, _) = x.as_ref(); + Stmt::Let(var_def, expr, _) | Stmt::Const(var_def, expr, _) => { let entry_type = match stmt { - Stmt::Let(_) => ScopeEntryType::Normal, - Stmt::Const(_) => ScopeEntryType::Constant, + Stmt::Let(_, _, _) => ScopeEntryType::Normal, + Stmt::Const(_, _, _) => ScopeEntryType::Constant, _ => unreachable!(), }; @@ -2028,16 +2016,14 @@ impl Engine { } else { ().into() }; - let var_name = unsafe_cast_var_name_to_lifetime(var_name, &state); + let var_name = unsafe_cast_var_name_to_lifetime(&var_def.0, &state); scope.push_dynamic_value(var_name, entry_type, val, false); Ok(Default::default()) } // Import statement #[cfg(not(feature = "no_module"))] - Stmt::Import(x) => { - let (expr, alias, _pos) = x.as_ref(); - + Stmt::Import(expr, alias, _pos) => { // Guard against too many modules #[cfg(not(feature = "unchecked"))] if state.modules >= self.max_modules() { @@ -2051,9 +2037,9 @@ impl Engine { if let Some(resolver) = &self.module_resolver { let mut module = resolver.resolve(self, &path, expr.position())?; - if let Some((name, _)) = alias { + if let Some(name_def) = alias { module.index_all_sub_modules(); - mods.push((name.clone(), module)); + mods.push((name_def.0.clone(), module)); } state.modules += 1; @@ -2072,8 +2058,8 @@ impl Engine { // Export statement #[cfg(not(feature = "no_module"))] - Stmt::Export(x) => { - for ((id, id_pos), rename) in x.0.iter() { + Stmt::Export(list, _) => { + for ((id, id_pos), rename) in list.iter() { // Mark scope variables as public if let Some(index) = scope.get_index(id).map(|(i, _)| i) { let alias = rename.as_ref().map(|(n, _)| n).unwrap_or_else(|| id); @@ -2087,9 +2073,7 @@ impl Engine { // Share statement #[cfg(not(feature = "no_closure"))] - Stmt::Share(x) => { - let (var_name, _) = x.as_ref(); - + Stmt::Share(var_name, _) => { match scope.get_index(var_name) { Some((index, ScopeEntryType::Normal)) => { let (val, _) = scope.get_mut(index); @@ -2274,14 +2258,14 @@ impl Engine { #[inline(always)] pub(crate) fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str { self.type_names - .as_ref() - .and_then(|t| t.get(name).map(String::as_str)) + .get(name) + .map(String::as_str) .unwrap_or_else(|| map_std_type_name(name)) } /// Make a Box>. #[inline(always)] - pub fn make_type_mismatch_err(&self, typ: &str, pos: Position) -> Box { + pub(crate) fn make_type_mismatch_err(&self, typ: &str, pos: Position) -> Box { EvalAltResult::ErrorMismatchDataType( typ.into(), self.map_type_name(type_name::()).into(), diff --git a/src/error.rs b/src/error.rs index 0d680b85..ca0f9d71 100644 --- a/src/error.rs +++ b/src/error.rs @@ -138,10 +138,11 @@ pub enum ParseErrorType { /// /// Never appears under the `no_module` feature. WrongExport, - /// Assignment to a copy of a value. - AssignmentToCopy, /// Assignment to an a constant variable. Wrapped value is the constant variable name. AssignmentToConstant(String), + /// Assignment to an inappropriate LHS (left-hand-side) expression. + /// Wrapped value is the error message (if any). + AssignmentToInvalidLHS(String), /// Expression exceeding the maximum levels of complexity. /// /// Never appears under the `unchecked` feature. @@ -183,8 +184,8 @@ impl ParseErrorType { Self::WrongFnDefinition => "Function definitions must be at global level and cannot be inside a block or another function", Self::DuplicatedExport(_) => "Duplicated variable/function in export statement", Self::WrongExport => "Export statement can only appear at global level", - Self::AssignmentToCopy => "Only a copy of the value is change with this assignment", Self::AssignmentToConstant(_) => "Cannot assign to a constant value", + Self::AssignmentToInvalidLHS(_) => "Expression cannot be assigned to", Self::ExprTooDeep => "Expression exceeds maximum complexity", Self::LiteralTooLarge(_, _) => "Literal exceeds maximum limit", Self::LoopBreak => "Break statement should only be used inside a loop" @@ -233,6 +234,10 @@ impl fmt::Display for ParseErrorType { Self::AssignmentToConstant(s) if s.is_empty() => f.write_str(self.desc()), Self::AssignmentToConstant(s) => write!(f, "Cannot assign to constant '{}'", s), + + Self::AssignmentToInvalidLHS(s) if s.is_empty() => f.write_str(self.desc()), + Self::AssignmentToInvalidLHS(s) => f.write_str(s), + Self::LiteralTooLarge(typ, max) => { write!(f, "{} exceeds the maximum limit ({})", typ, max) } diff --git a/src/fn_call.rs b/src/fn_call.rs index fda09ff2..efd563b6 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -387,9 +387,7 @@ impl Engine { let unified_lib = if let Some(ref env_lib) = fn_def.lib { lib_merged = Default::default(); lib_merged.push(env_lib.as_ref()); - if !lib.is_empty() { - lib_merged.extend(lib.iter().cloned()); - } + lib_merged.extend(lib.iter().cloned()); lib_merged.as_ref() } else { lib @@ -411,6 +409,9 @@ impl Engine { ) .into() } + // System errors are passed straight-through + err if err.is_system_exception() => Err(Box::new(err)), + // Other errors are wrapped in `ErrorInFunctionCall` _ => EvalAltResult::ErrorInFunctionCall( fn_def.name.to_string(), err, @@ -671,6 +672,11 @@ impl Engine { ) -> Result> { self.inc_operations(state)?; + let script = script.trim(); + if script.is_empty() { + return Ok(().into()); + } + // Check for stack overflow #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "unchecked"))] @@ -969,11 +975,7 @@ impl Engine { let var_name = var_name.as_str().map_err(|err| { self.make_type_mismatch_err::(err, args_expr[0].position()) })?; - if var_name.is_empty() { - return Ok(false.into()); - } else { - return Ok(scope.contains(var_name).into()); - } + return Ok(scope.contains(var_name).into()); } } @@ -1001,12 +1003,13 @@ impl Engine { self.make_type_mismatch_err::(err, args_expr[1].position()) })?; - if fn_name.is_empty() || num_params < 0 { - return Ok(false.into()); + return Ok(if num_params < 0 { + false } else { let hash = calc_fn_hash(empty(), fn_name, num_params as usize, empty()); - return Ok(lib.iter().any(|&m| m.contains_fn(hash, false)).into()); + lib.iter().any(|&m| m.contains_fn(hash, false)) } + .into()); } } @@ -1022,12 +1025,9 @@ impl Engine { let script = script.as_str().map_err(|typ| { self.make_type_mismatch_err::(typ, args_expr[0].position()) })?; - let result = if !script.is_empty() { - self.eval_script_expr(scope, mods, state, lib, script, level + 1) - .map_err(|err| err.fill_position(args_expr[0].position())) - } else { - Ok(().into()) - }; + let result = self + .eval_script_expr(scope, mods, state, lib, script, level + 1) + .map_err(|err| err.fill_position(args_expr[0].position())); // IMPORTANT! If the eval defines new variables in the current scope, // all variable offsets from this point on will be mis-aligned. @@ -1055,7 +1055,7 @@ impl Engine { } else { // If the first argument is a variable, and there is no curried arguments, convert to method-call style // in order to leverage potential &mut first argument and avoid cloning the value - if args_expr[0].get_variable_access(false).is_some() && curry.is_empty() { + if curry.is_empty() && args_expr[0].get_variable_access(false).is_some() { // func(x, ...) -> x.func(...) arg_values = args_expr .iter() diff --git a/src/fn_func.rs b/src/fn_func.rs index 331f7926..27e73bb0 100644 --- a/src/fn_func.rs +++ b/src/fn_func.rs @@ -19,7 +19,7 @@ pub trait Func { /// Create a Rust closure from an `AST`. /// The `Engine` and `AST` are consumed and basically embedded into the closure. /// - /// # Examples + /// # Example /// /// ``` /// # fn main() -> Result<(), Box> { @@ -50,7 +50,7 @@ pub trait Func { /// Create a Rust closure from a script. /// The `Engine` is consumed and basically embedded into the closure. /// - /// # Examples + /// # Example /// /// ``` /// # fn main() -> Result<(), Box> { diff --git a/src/module/mod.rs b/src/module/mod.rs index 4c2b3780..176441e8 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -125,7 +125,7 @@ impl AsRef for Module { impl Module { /// Create a new module. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -141,7 +141,7 @@ impl Module { /// Create a new module with a specified capacity for native Rust functions. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -160,7 +160,7 @@ impl Module { /// Is the module empty? /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -199,7 +199,7 @@ impl Module { /// Does a variable exist in the module? /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -215,7 +215,7 @@ impl Module { /// Get the value of a module variable. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -231,7 +231,7 @@ impl Module { /// Get a module variable as a `Dynamic`. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -249,7 +249,7 @@ impl Module { /// /// If there is an existing variable of the same name, it is replaced. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -327,7 +327,7 @@ impl Module { /// Does a sub-module exist in the module? /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -344,7 +344,7 @@ impl Module { /// Get a sub-module. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -361,7 +361,7 @@ impl Module { /// Get a mutable reference to a sub-module. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -380,7 +380,7 @@ impl Module { /// /// If there is an existing sub-module of the same name, it is replaced. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -402,7 +402,7 @@ impl Module { /// The `u64` hash is calculated by the function `crate::calc_fn_hash`. /// It is also returned by the `set_fn_XXX` calls. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -500,7 +500,7 @@ impl Module { /// /// To access the first mutable parameter, use `args.get_mut(0).unwrap()` /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -555,7 +555,7 @@ impl Module { /// /// If there is a similar existing Rust function, it is replaced. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -584,7 +584,7 @@ impl Module { /// /// If there is a similar existing Rust function, it is replaced. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -615,7 +615,7 @@ impl Module { /// /// If there is a similar existing Rust function, it is replaced. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -646,7 +646,7 @@ impl Module { /// /// If there is a similar existing Rust getter function, it is replaced. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Module; @@ -669,7 +669,7 @@ impl Module { /// /// If there is a similar existing Rust function, it is replaced. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Module, ImmutableString}; @@ -706,7 +706,7 @@ impl Module { /// /// If there is a similar existing Rust function, it is replaced. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Module, ImmutableString}; @@ -743,7 +743,7 @@ impl Module { /// /// If there is a similar existing setter Rust function, it is replaced. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Module, ImmutableString}; @@ -775,7 +775,7 @@ impl Module { /// Panics if the type is `Array` or `Map`. /// Indexers for arrays, object maps and strings cannot be registered. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Module, ImmutableString}; @@ -813,7 +813,7 @@ impl Module { /// /// If there is a similar existing Rust function, it is replaced. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Module, ImmutableString}; @@ -856,7 +856,7 @@ impl Module { /// /// If there is a similar existing Rust function, it is replaced. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Module, ImmutableString}; @@ -904,7 +904,7 @@ impl Module { /// Panics if the type is `Array` or `Map`. /// Indexers for arrays, object maps and strings cannot be registered. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Module, ImmutableString}; @@ -962,7 +962,7 @@ impl Module { /// Panics if the type is `Array` or `Map`. /// Indexers for arrays, object maps and strings cannot be registered. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Module, ImmutableString}; @@ -997,7 +997,7 @@ impl Module { /// /// If there is a similar existing Rust function, it is replaced. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Module, ImmutableString}; @@ -1047,7 +1047,7 @@ impl Module { /// /// If there is a similar existing Rust function, it is replaced. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Module, ImmutableString}; @@ -1125,18 +1125,10 @@ impl Module { /// The other module is consumed to merge into this module. #[inline] pub fn combine(&mut self, other: Self) -> &mut Self { - if !other.modules.is_empty() { - self.modules.extend(other.modules.into_iter()); - } - if !other.variables.is_empty() { - self.variables.extend(other.variables.into_iter()); - } - if !other.functions.is_empty() { - self.functions.extend(other.functions.into_iter()); - } - if !other.type_iterators.is_empty() { - self.type_iterators.extend(other.type_iterators.into_iter()); - } + self.modules.extend(other.modules.into_iter()); + self.variables.extend(other.variables.into_iter()); + self.functions.extend(other.functions.into_iter()); + self.type_iterators.extend(other.type_iterators.into_iter()); self.all_functions.clear(); self.all_variables.clear(); self.indexed = false; @@ -1148,20 +1140,38 @@ impl Module { /// Sub-modules are flattened onto the root module, with higher level overriding lower level. #[inline] pub fn combine_flatten(&mut self, other: Self) -> &mut Self { - if !other.modules.is_empty() { - other.modules.into_iter().for_each(|(_, m)| { - self.combine_flatten(m); - }); - } - if !other.variables.is_empty() { - self.variables.extend(other.variables.into_iter()); - } - if !other.functions.is_empty() { - self.functions.extend(other.functions.into_iter()); - } - if !other.type_iterators.is_empty() { - self.type_iterators.extend(other.type_iterators.into_iter()); - } + other.modules.into_iter().for_each(|(_, m)| { + self.combine_flatten(m); + }); + self.variables.extend(other.variables.into_iter()); + self.functions.extend(other.functions.into_iter()); + self.type_iterators.extend(other.type_iterators.into_iter()); + self.all_functions.clear(); + self.all_variables.clear(); + self.indexed = false; + self + } + + /// Poly-fill this module with another module. + /// Only items not existing in this module are added. + #[inline] + pub fn fill_with(&mut self, other: &Self) -> &mut Self { + other.modules.iter().for_each(|(k, v)| { + if !self.modules.contains_key(k) { + self.modules.insert(k.clone(), v.clone()); + } + }); + other.variables.iter().for_each(|(k, v)| { + if !self.variables.contains_key(k) { + self.variables.insert(k.clone(), v.clone()); + } + }); + other.functions.iter().for_each(|(&k, v)| { + self.functions.entry(k).or_insert_with(|| v.clone()); + }); + other.type_iterators.iter().for_each(|(&k, &v)| { + self.type_iterators.entry(k).or_insert(v); + }); self.all_functions.clear(); self.all_variables.clear(); self.indexed = false; @@ -1181,42 +1191,32 @@ impl Module { mut _filter: &mut impl FnMut(FnAccess, &str, usize) -> bool, ) -> &mut Self { #[cfg(not(feature = "no_function"))] - if !other.modules.is_empty() { - other.modules.iter().for_each(|(k, v)| { - let mut m = Self::new(); - m.merge_filtered(v, _filter); - self.modules.insert(k.clone(), m); - }); - } + other.modules.iter().for_each(|(k, v)| { + let mut m = Self::new(); + m.merge_filtered(v, _filter); + self.modules.insert(k.clone(), m); + }); #[cfg(feature = "no_function")] - if !other.modules.is_empty() { - self.modules - .extend(other.modules.iter().map(|(k, v)| (k.clone(), v.clone()))); - } - if !other.variables.is_empty() { - self.variables - .extend(other.variables.iter().map(|(k, v)| (k.clone(), v.clone()))); - } - if !other.functions.is_empty() { - self.functions.extend( - other - .functions - .iter() - .filter(|(_, (_, _, _, _, v))| match v { - #[cfg(not(feature = "no_function"))] - CallableFunction::Script(f) => { - _filter(f.access, f.name.as_str(), f.params.len()) - } - _ => true, - }) - .map(|(&k, v)| (k, v.clone())), - ); - } + self.modules + .extend(other.modules.iter().map(|(k, v)| (k.clone(), v.clone()))); - if !other.type_iterators.is_empty() { - self.type_iterators - .extend(other.type_iterators.iter().map(|(&k, v)| (k, v.clone()))); - } + self.variables + .extend(other.variables.iter().map(|(k, v)| (k.clone(), v.clone()))); + self.functions.extend( + other + .functions + .iter() + .filter(|(_, (_, _, _, _, v))| match v { + #[cfg(not(feature = "no_function"))] + CallableFunction::Script(f) => { + _filter(f.access, f.name.as_str(), f.params.len()) + } + _ => true, + }) + .map(|(&k, v)| (k, v.clone())), + ); + + self.type_iterators.extend(other.type_iterators.iter()); self.all_functions.clear(); self.all_variables.clear(); self.indexed = false; @@ -1327,7 +1327,7 @@ impl Module { /// defined in the module, are _merged_ into a _unified_ namespace before each call. /// Therefore, all functions will be found. /// - /// # Examples + /// # Example /// /// ``` /// # fn main() -> Result<(), Box> { diff --git a/src/module/resolvers/collection.rs b/src/module/resolvers/collection.rs index 6a57e1fe..ceffee33 100644 --- a/src/module/resolvers/collection.rs +++ b/src/module/resolvers/collection.rs @@ -8,7 +8,7 @@ use crate::stdlib::{boxed::Box, ops::AddAssign, vec::Vec}; /// Module resolution service that holds a collection of module resolves, /// to be searched in sequential order. /// -/// # Examples +/// # Example /// /// ``` /// use rhai::{Engine, Module}; @@ -28,7 +28,7 @@ pub struct ModuleResolversCollection(Vec>); impl ModuleResolversCollection { /// Create a new `ModuleResolversCollection`. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Engine, Module}; @@ -80,9 +80,7 @@ impl ModuleResolversCollection { /// The other `ModuleResolversCollection` is consumed. #[inline(always)] pub fn append(&mut self, other: Self) { - if !other.is_empty() { - self.0.extend(other.0.into_iter()); - } + self.0.extend(other.0.into_iter()); } } diff --git a/src/module/resolvers/file.rs b/src/module/resolvers/file.rs index 154b76d4..73640768 100644 --- a/src/module/resolvers/file.rs +++ b/src/module/resolvers/file.rs @@ -22,7 +22,7 @@ use crate::stdlib::{boxed::Box, collections::HashMap, path::PathBuf, string::Str /// plus all those defined within the same module are _merged_ into a _unified_ namespace before /// the call. Therefore, functions in a module script can cross-call each other. /// -/// # Examples +/// # Example /// /// ``` /// use rhai::Engine; @@ -53,7 +53,7 @@ impl Default for FileModuleResolver { impl FileModuleResolver { /// Create a new `FileModuleResolver` with a specific base path. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Engine; @@ -75,7 +75,7 @@ impl FileModuleResolver { /// /// The default extension is `.rhai`. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Engine; @@ -102,7 +102,7 @@ impl FileModuleResolver { /// Create a new `FileModuleResolver` with the current directory as base path. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Engine; diff --git a/src/module/resolvers/stat.rs b/src/module/resolvers/stat.rs index b530202b..4b64ad91 100644 --- a/src/module/resolvers/stat.rs +++ b/src/module/resolvers/stat.rs @@ -7,7 +7,7 @@ use crate::stdlib::{boxed::Box, collections::HashMap, ops::AddAssign, string::St /// Module resolution service that serves modules added into it. /// -/// # Examples +/// # Example /// /// ``` /// use rhai::{Engine, Module}; @@ -28,7 +28,7 @@ pub struct StaticModuleResolver(HashMap); impl StaticModuleResolver { /// Create a new `StaticModuleResolver`. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Engine, Module}; diff --git a/src/optimize.rs b/src/optimize.rs index 5c8053e4..8fe5c876 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -7,7 +7,7 @@ use crate::engine::{ }; use crate::fn_call::run_builtin_binary_op; use crate::module::Module; -use crate::parser::{map_dynamic_to_expr, Expr, ScriptFnDef, Stmt, AST}; +use crate::parser::{map_dynamic_to_expr, BinaryExpr, Expr, ScriptFnDef, Stmt, AST}; use crate::scope::{Entry as ScopeEntry, Scope}; use crate::token::{is_valid_identifier, Position}; use crate::{calc_fn_hash, StaticVec}; @@ -163,87 +163,91 @@ fn call_fn_with_constant_arguments( /// Optimize a statement. fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { match stmt { + // id op= expr + Stmt::Assignment(x, pos) => Stmt::Assignment( + Box::new((optimize_expr(x.0, state), x.1, optimize_expr(x.2, state))), + pos, + ), + // if false { if_block } -> Noop + Stmt::IfThenElse(Expr::False(pos), x, _) if x.1.is_none() => { + state.set_dirty(); + Stmt::Noop(pos) + } + // if true { if_block } -> if_block + Stmt::IfThenElse(Expr::True(_), x, _) if x.1.is_none() => optimize_stmt(x.0, state, true), // if expr { Noop } - Stmt::IfThenElse(x) if matches!(x.1, Stmt::Noop(_)) && x.2.is_none() => { + Stmt::IfThenElse(condition, x, _) if x.1.is_none() && matches!(x.0, Stmt::Noop(_)) => { state.set_dirty(); - let pos = x.0.position(); - let expr = optimize_expr(x.0, state); + let pos = condition.position(); + let expr = optimize_expr(condition, state); if preserve_result { // -> { expr, Noop } - let mut statements = StaticVec::new(); - statements.push(Stmt::Expr(Box::new(expr))); - statements.push(x.1); + let mut statements = Vec::new(); + statements.push(Stmt::Expr(expr)); + statements.push(x.0); - Stmt::Block(Box::new((statements, pos))) + Stmt::Block(statements, pos) } else { // -> expr - Stmt::Expr(Box::new(expr)) + Stmt::Expr(expr) } } // if expr { if_block } - Stmt::IfThenElse(x) if x.2.is_none() => match x.0 { - // if false { if_block } -> Noop - Expr::False(pos) => { - state.set_dirty(); - Stmt::Noop(pos) - } - // if true { if_block } -> if_block - Expr::True(_) => optimize_stmt(x.1, state, true), - // if expr { if_block } - expr => Stmt::IfThenElse(Box::new(( - optimize_expr(expr, state), - optimize_stmt(x.1, state, true), - None, - x.3, - ))), - }, + Stmt::IfThenElse(condition, x, pos) if x.1.is_none() => Stmt::IfThenElse( + optimize_expr(condition, state), + Box::new((optimize_stmt(x.0, state, true), None)), + pos, + ), + // if false { if_block } else { else_block } -> else_block + Stmt::IfThenElse(Expr::False(_), x, _) if x.1.is_some() => { + optimize_stmt(x.1.unwrap(), state, true) + } + // if true { if_block } else { else_block } -> if_block + Stmt::IfThenElse(Expr::True(_), x, _) => optimize_stmt(x.0, state, true), // if expr { if_block } else { else_block } - Stmt::IfThenElse(x) if x.2.is_some() => match x.0 { - // if false { if_block } else { else_block } -> else_block - Expr::False(_) => optimize_stmt(x.2.unwrap(), state, true), - // if true { if_block } else { else_block } -> if_block - Expr::True(_) => optimize_stmt(x.1, state, true), - // if expr { if_block } else { else_block } - expr => Stmt::IfThenElse(Box::new(( - optimize_expr(expr, state), - optimize_stmt(x.1, state, true), - match optimize_stmt(x.2.unwrap(), state, true) { + Stmt::IfThenElse(condition, x, pos) => Stmt::IfThenElse( + optimize_expr(condition, state), + Box::new(( + optimize_stmt(x.0, state, true), + match optimize_stmt(x.1.unwrap(), state, true) { Stmt::Noop(_) => None, // Noop -> no else block stmt => Some(stmt), }, - x.3, - ))), - }, + )), + pos, + ), + + // while false { block } -> Noop + Stmt::While(Expr::False(pos), _, _) => { + state.set_dirty(); + Stmt::Noop(pos) + } + // while true { block } -> loop { block } + Stmt::While(Expr::True(_), block, pos) => { + Stmt::Loop(Box::new(optimize_stmt(*block, state, false)), pos) + } // while expr { block } - Stmt::While(x) => match x.0 { - // while false { block } -> Noop - Expr::False(pos) => { - state.set_dirty(); - Stmt::Noop(pos) - } - // while true { block } -> loop { block } - Expr::True(_) => Stmt::Loop(Box::new((optimize_stmt(x.1, state, false), x.2))), - // while expr { block } - expr => match optimize_stmt(x.1, state, false) { + Stmt::While(condition, block, pos) => { + match optimize_stmt(*block, state, false) { // while expr { break; } -> { expr; } Stmt::Break(pos) => { // Only a single break statement - turn into running the guard expression once state.set_dirty(); - let mut statements = StaticVec::new(); - statements.push(Stmt::Expr(Box::new(optimize_expr(expr, state)))); + let mut statements = Vec::new(); + statements.push(Stmt::Expr(optimize_expr(condition, state))); if preserve_result { statements.push(Stmt::Noop(pos)) } - Stmt::Block(Box::new((statements, pos))) + Stmt::Block(statements, pos) } // while expr { block } - stmt => Stmt::While(Box::new((optimize_expr(expr, state), stmt, x.2))), - }, - }, + stmt => Stmt::While(optimize_expr(condition, state), Box::new(stmt), pos), + } + } // loop { block } - Stmt::Loop(x) => match optimize_stmt(x.0, state, false) { + Stmt::Loop(block, pos) => match optimize_stmt(*block, state, false) { // loop { break; } -> Noop Stmt::Break(pos) => { // Only a single break statement @@ -251,59 +255,52 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { Stmt::Noop(pos) } // loop { block } - stmt => Stmt::Loop(Box::new((stmt, x.1))), + stmt => Stmt::Loop(Box::new(stmt), pos), }, // for id in expr { block } - Stmt::For(x) => Stmt::For(Box::new(( - x.0, - optimize_expr(x.1, state), - optimize_stmt(x.2, state, false), - x.3, - ))), + Stmt::For(iterable, x, pos) => { + let (var_name, block) = *x; + Stmt::For( + optimize_expr(iterable, state), + Box::new((var_name, optimize_stmt(block, state, false))), + pos, + ) + } // let id = expr; - Stmt::Let(x) if x.1.is_some() => Stmt::Let(Box::new(( - x.0, - Some(optimize_expr(x.1.unwrap(), state)), - x.2, - ))), + Stmt::Let(name, Some(expr), pos) => Stmt::Let(name, Some(optimize_expr(expr, state)), pos), // let id; - stmt @ Stmt::Let(_) => stmt, + stmt @ Stmt::Let(_, None, _) => stmt, // import expr as var; #[cfg(not(feature = "no_module"))] - Stmt::Import(x) => Stmt::Import(Box::new((optimize_expr(x.0, state), x.1, x.2))), + Stmt::Import(expr, alias, pos) => Stmt::Import(optimize_expr(expr, state), alias, pos), // { block } - Stmt::Block(x) => { - let orig_len = x.0.len(); // Original number of statements in the block, for change detection + Stmt::Block(statements, pos) => { + let orig_len = statements.len(); // Original number of statements in the block, for change detection let orig_constants_len = state.constants.len(); // Original number of constants in the state, for restore later - let pos = x.1; // Optimize each statement in the block - let mut result: Vec<_> = - x.0.into_iter() - .map(|stmt| match stmt { - // Add constant literals into the state - Stmt::Const(mut v) => { - if let Some(expr) = v.1 { - let expr = optimize_expr(expr, state); - - if expr.is_literal() { - state.set_dirty(); - state.push_constant(&(v.0).0, expr); - Stmt::Noop(pos) // No need to keep constants - } else { - v.1 = Some(expr); - Stmt::Const(v) - } - } else { - state.set_dirty(); - state.push_constant(&(v.0).0, Expr::Unit((v.0).1)); - Stmt::Noop(pos) // No need to keep constants - } - } - // Optimize the statement - _ => optimize_stmt(stmt, state, preserve_result), - }) - .collect(); + let mut result: Vec<_> = statements + .into_iter() + .map(|stmt| match stmt { + // Add constant literals into the state + Stmt::Const(name, Some(expr), pos) if expr.is_literal() => { + state.set_dirty(); + state.push_constant(&name.0, expr); + Stmt::Noop(pos) // No need to keep constants + } + Stmt::Const(name, Some(expr), pos) if expr.is_literal() => { + let expr = optimize_expr(expr, state); + Stmt::Const(name, Some(expr), pos) + } + Stmt::Const(name, None, pos) => { + state.set_dirty(); + state.push_constant(&name.0, Expr::Unit(name.1)); + Stmt::Noop(pos) // No need to keep constants + } + // Optimize the statement + stmt => optimize_stmt(stmt, state, preserve_result), + }) + .collect(); // Remove all raw expression statements that are pure except for the very last statement let last_stmt = if preserve_result { result.pop() } else { None }; @@ -321,9 +318,11 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { while let Some(expr) = result.pop() { match expr { - Stmt::Let(x) => removed = x.1.as_ref().map(Expr::is_pure).unwrap_or(true), + Stmt::Let(_, expr, _) => { + removed = expr.as_ref().map(Expr::is_pure).unwrap_or(true) + } #[cfg(not(feature = "no_module"))] - Stmt::Import(x) => removed = x.0.is_pure(), + Stmt::Import(expr, _, _) => removed = expr.is_pure(), _ => { result.push(expr); break; @@ -341,7 +340,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { .into_iter() .rev() .enumerate() - .map(|(i, s)| optimize_stmt(s, state, i == 0)) + .map(|(i, stmt)| optimize_stmt(stmt, state, i == 0)) .rev() .collect(); } @@ -355,7 +354,7 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { } match stmt { - Stmt::ReturnWithVal(_) | Stmt::Break(_) => dead_code = true, + Stmt::ReturnWithVal(_, _, _) | Stmt::Break(_) => dead_code = true, _ => (), } @@ -370,23 +369,23 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { // Pop the stack and remove all the local constants state.restore_constants(orig_constants_len); - match result[..] { + match &result[..] { // No statements in block - change to No-op [] => { state.set_dirty(); Stmt::Noop(pos) } // Only one let statement - leave it alone - [Stmt::Let(_)] => Stmt::Block(Box::new((result.into(), pos))), + [x] if matches!(x, Stmt::Let(_, _, _)) => Stmt::Block(result, pos), // Only one import statement - leave it alone #[cfg(not(feature = "no_module"))] - [Stmt::Import(_)] => Stmt::Block(Box::new((result.into(), pos))), + [x] if matches!(x, Stmt::Import(_, _, _)) => Stmt::Block(result, pos), // Only one statement - promote [_] => { state.set_dirty(); result.remove(0) } - _ => Stmt::Block(Box::new((result.into(), pos))), + _ => Stmt::Block(result, pos), } } // try { block } catch ( var ) { block } @@ -394,25 +393,31 @@ fn optimize_stmt(stmt: Stmt, state: &mut State, preserve_result: bool) -> Stmt { // If try block is pure, there will never be any exceptions state.set_dirty(); let pos = (x.0).0.position(); - let mut statements: StaticVec<_> = Default::default(); + let mut statements: Vec<_> = Default::default(); statements.push(optimize_stmt((x.0).0, state, preserve_result)); statements.push(Stmt::Noop(pos)); - Stmt::Block(Box::new((statements, pos))) + Stmt::Block(statements, pos) } // try { block } catch ( var ) { block } - Stmt::TryCatch(x) => Stmt::TryCatch(Box::new(( - (optimize_stmt((x.0).0, state, false), (x.0).1), - x.1, - (optimize_stmt((x.2).0, state, false), (x.2).1), - ))), + Stmt::TryCatch(x) => { + let ((try_block, try_pos), var_name, (catch_block, catch_pos)) = *x; + Stmt::TryCatch(Box::new(( + (optimize_stmt(try_block, state, false), try_pos), + var_name, + (optimize_stmt(catch_block, state, false), catch_pos), + ))) + } // expr; - Stmt::Expr(expr) => Stmt::Expr(Box::new(optimize_expr(*expr, state))), + Stmt::Expr(Expr::Stmt(x)) if matches!(x.0, Stmt::Expr(_)) => { + state.set_dirty(); + optimize_stmt(x.0, state, preserve_result) + } + // expr; + Stmt::Expr(expr) => Stmt::Expr(optimize_expr(expr, state)), // return expr; - Stmt::ReturnWithVal(x) if x.1.is_some() => Stmt::ReturnWithVal(Box::new(( - x.0, - Some(optimize_expr(x.1.unwrap(), state)), - x.2, - ))), + Stmt::ReturnWithVal(ret, Some(expr), pos) => { + Stmt::ReturnWithVal(ret, Some(optimize_expr(expr, state)), pos) + } // All other statements - skip stmt => stmt, } @@ -432,27 +437,25 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { match expr { // expr - do not promote because there is a reason it is wrapped in an `Expr::Expr` Expr::Expr(x) => Expr::Expr(Box::new(optimize_expr(*x, state))), - // ( stmt ) - Expr::Stmt(x) => match optimize_stmt(x.0, state, true) { - // ( Noop ) -> () + // { stmt } + Expr::Stmt(x) => match x.0 { + // {} -> () Stmt::Noop(_) => { state.set_dirty(); Expr::Unit(x.1) } - // ( expr ) -> expr + // { expr } -> expr Stmt::Expr(expr) => { state.set_dirty(); - *expr + optimize_expr(expr, state) } - // ( stmt ) - stmt => Expr::Stmt(Box::new((stmt, x.1))), + // { stmt } + stmt => Expr::Stmt(Box::new((optimize_stmt(stmt, state, true), x.1))), }, - // id op= expr - Expr::Assignment(x) => Expr::Assignment(Box::new((x.0, x.1, optimize_expr(x.2, state), x.3))), // lhs.rhs #[cfg(not(feature = "no_object"))] - Expr::Dot(x) => match (x.0, x.1) { + Expr::Dot(x) => match (x.lhs, x.rhs) { // map.string (Expr::Map(m), Expr::Property(p)) if m.0.iter().all(|(_, x)| x.is_pure()) => { let ((prop, _, _), _) = p.as_ref(); @@ -465,12 +468,16 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { .unwrap_or_else(|| Expr::Unit(pos)) } // lhs.rhs - (lhs, rhs) => Expr::Dot(Box::new((optimize_expr(lhs, state), optimize_expr(rhs, state), x.2))) + (lhs, rhs) => Expr::Dot(Box::new(BinaryExpr { + lhs: optimize_expr(lhs, state), + rhs: optimize_expr(rhs, state), + pos: x.pos + })) } // lhs[rhs] #[cfg(not(feature = "no_index"))] - Expr::Index(x) => match (x.0, x.1) { + Expr::Index(x) => match (x.lhs, x.rhs) { // array[int] (Expr::Array(mut a), Expr::IntegerConstant(i)) if i.0 >= 0 && (i.0 as usize) < a.0.len() && a.0.iter().all(Expr::is_pure) => @@ -499,7 +506,11 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { Expr::CharConstant(Box::new((s.0.chars().nth(i.0 as usize).unwrap(), s.1))) } // lhs[rhs] - (lhs, rhs) => Expr::Index(Box::new((optimize_expr(lhs, state), optimize_expr(rhs, state), x.2))), + (lhs, rhs) => Expr::Index(Box::new(BinaryExpr { + lhs: optimize_expr(lhs, state), + rhs: optimize_expr(rhs, state), + pos: x.pos + })), }, // [ items .. ] #[cfg(not(feature = "no_index"))] @@ -512,7 +523,7 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { .into_iter().map(|((key, pos), expr)| ((key, pos), optimize_expr(expr, state))) .collect(), m.1))), // lhs in rhs - Expr::In(x) => match (x.0, x.1) { + Expr::In(x) => match (x.lhs, x.rhs) { // "xxx" in "xxxxx" (Expr::StringConstant(a), Expr::StringConstant(b)) => { state.set_dirty(); @@ -544,10 +555,14 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { } } // lhs in rhs - (lhs, rhs) => Expr::In(Box::new((optimize_expr(lhs, state), optimize_expr(rhs, state), x.2))), + (lhs, rhs) => Expr::In(Box::new(BinaryExpr { + lhs: optimize_expr(lhs, state), + rhs: optimize_expr(rhs, state), + pos: x.pos + })), }, // lhs && rhs - Expr::And(x) => match (x.0, x.1) { + Expr::And(x) => match (x.lhs, x.rhs) { // true && rhs -> rhs (Expr::True(_), rhs) => { state.set_dirty(); @@ -564,10 +579,14 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { optimize_expr(lhs, state) } // lhs && rhs - (lhs, rhs) => Expr::And(Box::new((optimize_expr(lhs, state), optimize_expr(rhs, state), x.2))), + (lhs, rhs) => Expr::And(Box::new(BinaryExpr { + lhs: optimize_expr(lhs, state), + rhs: optimize_expr(rhs, state), + pos: x.pos + })), }, // lhs || rhs - Expr::Or(x) => match (x.0, x.1) { + Expr::Or(x) => match (x.lhs, x.rhs) { // false || rhs -> rhs (Expr::False(_), rhs) => { state.set_dirty(); @@ -584,7 +603,11 @@ fn optimize_expr(expr: Expr, state: &mut State) -> Expr { optimize_expr(lhs, state) } // lhs || rhs - (lhs, rhs) => Expr::Or(Box::new((optimize_expr(lhs, state), optimize_expr(rhs, state), x.2))), + (lhs, rhs) => Expr::Or(Box::new(BinaryExpr { + lhs: optimize_expr(lhs, state), + rhs: optimize_expr(rhs, state), + pos: x.pos + })), }, // Do not call some special keywords @@ -748,35 +771,35 @@ fn optimize( .enumerate() .map(|(i, stmt)| { match stmt { - Stmt::Const(mut v) => { + Stmt::Const(var_def, Some(expr), pos) => { // Load constants - if let Some(expr) = v.1 { - let expr = optimize_expr(expr, &mut state); + let expr = optimize_expr(expr, &mut state); - if expr.is_literal() { - state.push_constant(&(v.0).0, expr.clone()); - } - - v.1 = if expr.is_unit() { - state.set_dirty(); - None - } else { - Some(expr) - }; - } else { - state.push_constant(&(v.0).0, Expr::Unit((v.0).1)); + if expr.is_literal() { + state.push_constant(&var_def.0, expr.clone()); } // Keep it in the global scope - Stmt::Const(v) + if expr.is_unit() { + state.set_dirty(); + Stmt::Const(var_def, None, pos) + } else { + Stmt::Const(var_def, Some(expr), pos) + } + } + Stmt::Const(ref var_def, None, _) => { + state.push_constant(&var_def.0, Expr::Unit(var_def.1)); + + // Keep it in the global scope + stmt } _ => { // Keep all variable declarations at this level // and always keep the last return value let keep = match stmt { - Stmt::Let(_) => true, + Stmt::Let(_, _, _) => true, #[cfg(not(feature = "no_module"))] - Stmt::Import(_) => true, + Stmt::Import(_, _, _) => true, _ => i == num_statements - 1, }; optimize_stmt(stmt, &mut state, keep) @@ -798,7 +821,7 @@ fn optimize( // Add back the last statement unless it is a lone No-op if let Some(stmt) = last_stmt { - if !result.is_empty() || !matches!(stmt, Stmt::Noop(_)) { + if !result.is_empty() || !stmt.is_noop() { result.push(stmt); } } @@ -859,16 +882,12 @@ pub fn optimize_into_ast( // {} -> Noop fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { // { return val; } -> val - Stmt::ReturnWithVal(x) - if x.1.is_some() && (x.0).0 == ReturnType::Return => - { - Stmt::Expr(Box::new(x.1.unwrap())) + Stmt::ReturnWithVal((ReturnType::Return, _), Some(expr), _) => { + Stmt::Expr(expr) } // { return; } -> () - Stmt::ReturnWithVal(x) - if x.1.is_none() && (x.0).0 == ReturnType::Return => - { - Stmt::Expr(Box::new(Expr::Unit((x.0).1))) + Stmt::ReturnWithVal((ReturnType::Return, pos), None, _) => { + Stmt::Expr(Expr::Unit(pos)) } // All others stmt => stmt, diff --git a/src/packages/iter_basic.rs b/src/packages/iter_basic.rs index 46ab0119..0d8d1e8f 100644 --- a/src/packages/iter_basic.rs +++ b/src/packages/iter_basic.rs @@ -45,6 +45,8 @@ where Ok(StepRange::(from, to, step)) } +#[cfg(not(feature = "only_i32"))] +#[cfg(not(feature = "only_i64"))] macro_rules! reg_range { ($lib:expr, $x:expr, $( $y:ty ),*) => ( $( @@ -54,6 +56,8 @@ macro_rules! reg_range { ) } +#[cfg(not(feature = "only_i32"))] +#[cfg(not(feature = "only_i64"))] macro_rules! reg_step { ($lib:expr, $x:expr, $( $y:ty ),*) => ( $( diff --git a/src/packages/mod.rs b/src/packages/mod.rs index d7c7d023..072de190 100644 --- a/src/packages/mod.rs +++ b/src/packages/mod.rs @@ -94,7 +94,7 @@ impl PackagesCollection { /// Functions can be added to the package using the standard module methods such as /// `set_fn_2`, `set_fn_3_mut`, `set_fn_0` etc. /// -/// # Examples +/// # Example /// /// ``` /// use rhai::{Dynamic, EvalAltResult}; diff --git a/src/parser.rs b/src/parser.rs index 50eb0454..4329de2a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -74,6 +74,8 @@ type FunctionsLib = HashMap; /// Compiled AST (abstract syntax tree) of a Rhai script. /// +/// # Thread Safety +/// /// Currently, `AST` is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. #[derive(Debug, Clone, Default)] pub struct AST( @@ -107,6 +109,7 @@ impl AST { } /// Get a mutable reference to the statements. + #[cfg(not(feature = "no_optimize"))] #[inline(always)] pub(crate) fn statements_mut(&mut self) -> &mut Vec { &mut self.0 @@ -398,13 +401,8 @@ impl AST { mut filter: impl FnMut(FnAccess, &str, usize) -> bool, ) -> &mut Self { let Self(ref mut statements, ref mut functions) = self; - - if !other.0.is_empty() { - statements.extend(other.0.into_iter()); - } - + statements.extend(other.0.into_iter()); functions.merge_filtered(&other.1, &mut filter); - self } @@ -745,19 +743,21 @@ pub enum Stmt { /// No-op. Noop(Position), /// if expr { stmt } else { stmt } - IfThenElse(Box<(Expr, Stmt, Option, Position)>), + IfThenElse(Expr, Box<(Stmt, Option)>, Position), /// while expr { stmt } - While(Box<(Expr, Stmt, Position)>), + While(Expr, Box, Position), /// loop { stmt } - Loop(Box<(Stmt, Position)>), + Loop(Box, Position), /// for id in expr { stmt } - For(Box<(String, Expr, Stmt, Position)>), + For(Expr, Box<(String, Stmt)>, Position), /// let id = expr - Let(Box<((String, Position), Option, Position)>), + Let(Box<(String, Position)>, Option, Position), /// const id = expr - Const(Box<((String, Position), Option, Position)>), + Const(Box<(String, Position)>, Option, Position), + /// expr op= expr + Assignment(Box<(Expr, Cow<'static, str>, Expr)>, Position), /// { stmt; ... } - Block(Box<(StaticVec, Position)>), + Block(Vec, Position), /// try { stmt; ... } catch ( var ) { stmt; ... } TryCatch( Box<( @@ -767,27 +767,25 @@ pub enum Stmt { )>, ), /// expr - Expr(Box), + Expr(Expr), /// continue Continue(Position), /// break Break(Position), /// return/throw - ReturnWithVal(Box<((ReturnType, Position), Option, Position)>), + ReturnWithVal((ReturnType, Position), Option, Position), /// import expr as var #[cfg(not(feature = "no_module"))] - Import(Box<(Expr, Option<(ImmutableString, Position)>, Position)>), + Import(Expr, Option>, Position), /// export var as var, ... #[cfg(not(feature = "no_module"))] Export( - Box<( - StaticVec<((String, Position), Option<(String, Position)>)>, - Position, - )>, + Vec<((String, Position), Option<(String, Position)>)>, + Position, ), /// Convert a variable to shared. #[cfg(not(feature = "no_closure"))] - Share(Box<(String, Position)>), + Share(String, Position), } impl Default for Stmt { @@ -798,55 +796,71 @@ impl Default for Stmt { } impl Stmt { + /// Is this statement `Noop`? + pub fn is_noop(&self) -> bool { + match self { + Self::Noop(_) => true, + _ => false, + } + } + /// Get the `Position` of this statement. pub fn position(&self) -> Position { match self { - Stmt::Noop(pos) | Stmt::Continue(pos) | Stmt::Break(pos) => *pos, - Stmt::Let(x) => (x.0).1, - Stmt::Const(x) => (x.0).1, - Stmt::Block(x) => x.1, - Stmt::IfThenElse(x) => x.3, - Stmt::Expr(x) => x.position(), - Stmt::While(x) => x.2, - Stmt::Loop(x) => x.1, - Stmt::For(x) => x.3, - Stmt::ReturnWithVal(x) => (x.0).1, - Stmt::TryCatch(x) => (x.0).1, + Self::Noop(pos) + | Self::Continue(pos) + | Self::Break(pos) + | Self::Block(_, pos) + | Self::Assignment(_, pos) + | Self::IfThenElse(_, _, pos) + | Self::While(_, _, pos) + | Self::Loop(_, pos) + | Self::For(_, _, pos) + | Self::ReturnWithVal((_, pos), _, _) => *pos, + + Self::Let(x, _, _) | Self::Const(x, _, _) => x.1, + Self::TryCatch(x) => (x.0).1, + + Self::Expr(x) => x.position(), #[cfg(not(feature = "no_module"))] - Stmt::Import(x) => x.2, + Self::Import(_, _, pos) => *pos, #[cfg(not(feature = "no_module"))] - Stmt::Export(x) => x.1, + Self::Export(_, pos) => *pos, #[cfg(not(feature = "no_closure"))] - Stmt::Share(x) => x.1, + Self::Share(_, pos) => *pos, } } /// Override the `Position` of this statement. pub fn set_position(&mut self, new_pos: Position) -> &mut Self { match self { - Stmt::Noop(pos) | Stmt::Continue(pos) | Stmt::Break(pos) => *pos = new_pos, - Stmt::Let(x) => (x.0).1 = new_pos, - Stmt::Const(x) => (x.0).1 = new_pos, - Stmt::Block(x) => x.1 = new_pos, - Stmt::IfThenElse(x) => x.3 = new_pos, - Stmt::Expr(x) => { + Self::Noop(pos) + | Self::Continue(pos) + | Self::Break(pos) + | Self::Block(_, pos) + | Self::Assignment(_, pos) + | Self::IfThenElse(_, _, pos) + | Self::While(_, _, pos) + | Self::Loop(_, pos) + | Self::For(_, _, pos) + | Self::ReturnWithVal((_, pos), _, _) => *pos = new_pos, + + Self::Let(x, _, _) | Self::Const(x, _, _) => x.1 = new_pos, + Self::TryCatch(x) => (x.0).1 = new_pos, + + Self::Expr(x) => { x.set_position(new_pos); } - Stmt::While(x) => x.2 = new_pos, - Stmt::Loop(x) => x.1 = new_pos, - Stmt::For(x) => x.3 = new_pos, - Stmt::ReturnWithVal(x) => (x.0).1 = new_pos, - Stmt::TryCatch(x) => (x.0).1 = new_pos, #[cfg(not(feature = "no_module"))] - Stmt::Import(x) => x.2 = new_pos, + Self::Import(_, _, pos) => *pos = new_pos, #[cfg(not(feature = "no_module"))] - Stmt::Export(x) => x.1 = new_pos, + Self::Export(_, pos) => *pos = new_pos, #[cfg(not(feature = "no_closure"))] - Stmt::Share(x) => x.1 = new_pos, + Self::Share(_, pos) => *pos = new_pos, } self @@ -855,55 +869,56 @@ impl Stmt { /// Is this statement self-terminated (i.e. no need for a semicolon terminator)? pub fn is_self_terminated(&self) -> bool { match self { - Stmt::IfThenElse(_) - | Stmt::While(_) - | Stmt::Loop(_) - | Stmt::For(_) - | Stmt::Block(_) - | Stmt::TryCatch(_) => true, + Self::IfThenElse(_, _, _) + | Self::While(_, _, _) + | Self::Loop(_, _) + | Self::For(_, _, _) + | Self::Block(_, _) + | Self::TryCatch(_) => true, // A No-op requires a semicolon in order to know it is an empty statement! - Stmt::Noop(_) => false, + Self::Noop(_) => false, - Stmt::Let(_) - | Stmt::Const(_) - | Stmt::Expr(_) - | Stmt::Continue(_) - | Stmt::Break(_) - | Stmt::ReturnWithVal(_) => false, + Self::Let(_, _, _) + | Self::Const(_, _, _) + | Self::Assignment(_, _) + | Self::Expr(_) + | Self::Continue(_) + | Self::Break(_) + | Self::ReturnWithVal(_, _, _) => false, #[cfg(not(feature = "no_module"))] - Stmt::Import(_) | Stmt::Export(_) => false, + Self::Import(_, _, _) | Self::Export(_, _) => false, #[cfg(not(feature = "no_closure"))] - Stmt::Share(_) => false, + Self::Share(_, _) => false, } } /// Is this statement _pure_? pub fn is_pure(&self) -> bool { match self { - Stmt::Noop(_) => true, - Stmt::Expr(expr) => expr.is_pure(), - Stmt::IfThenElse(x) if x.2.is_some() => { - x.0.is_pure() && x.1.is_pure() && x.2.as_ref().unwrap().is_pure() + Self::Noop(_) => true, + Self::Expr(expr) => expr.is_pure(), + Self::IfThenElse(condition, x, _) if x.1.is_some() => { + condition.is_pure() && x.0.is_pure() && x.1.as_ref().unwrap().is_pure() } - Stmt::IfThenElse(x) => x.1.is_pure(), - Stmt::While(x) => x.0.is_pure() && x.1.is_pure(), - Stmt::Loop(x) => x.0.is_pure(), - Stmt::For(x) => x.1.is_pure() && x.2.is_pure(), - Stmt::Let(_) | Stmt::Const(_) => false, - Stmt::Block(x) => x.0.iter().all(Stmt::is_pure), - Stmt::Continue(_) | Stmt::Break(_) | Stmt::ReturnWithVal(_) => false, - Stmt::TryCatch(x) => (x.0).0.is_pure() && (x.2).0.is_pure(), + Self::IfThenElse(condition, x, _) => condition.is_pure() && x.0.is_pure(), + Self::While(condition, block, _) => condition.is_pure() && block.is_pure(), + Self::Loop(block, _) => block.is_pure(), + Self::For(iterable, x, _) => iterable.is_pure() && x.1.is_pure(), + Self::Let(_, _, _) | Self::Const(_, _, _) | Self::Assignment(_, _) => false, + Self::Block(block, _) => block.iter().all(|stmt| stmt.is_pure()), + Self::Continue(_) | Self::Break(_) | Self::ReturnWithVal(_, _, _) => false, + Self::TryCatch(x) => (x.0).0.is_pure() && (x.2).0.is_pure(), #[cfg(not(feature = "no_module"))] - Stmt::Import(_) => false, + Self::Import(_, _, _) => false, #[cfg(not(feature = "no_module"))] - Stmt::Export(_) => false, + Self::Export(_, _) => false, #[cfg(not(feature = "no_closure"))] - Stmt::Share(_) => false, + Self::Share(_, _) => false, } } } @@ -915,19 +930,23 @@ impl Stmt { /// /// This type is volatile and may change. #[derive(Clone)] -pub struct CustomExpr(pub StaticVec, pub Shared); +pub struct CustomExpr { + keywords: StaticVec, + func: Shared, + pos: Position, +} impl fmt::Debug for CustomExpr { #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self.0, f) + fmt::Debug::fmt(&self.keywords, f) } } impl Hash for CustomExpr { #[inline(always)] fn hash(&self, state: &mut H) { - self.0.hash(state); + self.keywords.hash(state); } } @@ -935,12 +954,17 @@ impl CustomExpr { /// Get the keywords for this `CustomExpr`. #[inline(always)] pub fn keywords(&self) -> &[Expr] { - &self.0 + &self.keywords } /// Get the implementation function for this `CustomExpr`. #[inline(always)] pub fn func(&self) -> &FnCustomSyntaxEval { - self.1.as_ref() + self.func.as_ref() + } + /// Get the position of this `CustomExpr`. + #[inline(always)] + pub fn position(&self) -> Position { + self.pos } } @@ -966,6 +990,13 @@ impl Hash for FloatWrapper { } } +#[derive(Debug, Clone, Hash)] +pub struct BinaryExpr { + pub lhs: Expr, + pub rhs: Expr, + pub pos: Position, +} + /// _[INTERNALS]_ An expression sub-tree. /// Exported under the `internals` feature only. /// @@ -1015,22 +1046,20 @@ pub enum Expr { Option, // Default value is `bool` in order for `Expr` to be `Hash`. )>, ), - /// expr op= expr - Assignment(Box<(Expr, Cow<'static, str>, Expr, Position)>), /// lhs.rhs - Dot(Box<(Expr, Expr, Position)>), + Dot(Box), /// expr[expr] - Index(Box<(Expr, Expr, Position)>), + Index(Box), /// [ expr, ... ] Array(Box<(StaticVec, Position)>), /// #{ name:expr, ... } Map(Box<(StaticVec<((ImmutableString, Position), Expr)>, Position)>), /// lhs in rhs - In(Box<(Expr, Expr, Position)>), + In(Box), /// lhs && rhs - And(Box<(Expr, Expr, Position)>), + And(Box), /// lhs || rhs - Or(Box<(Expr, Expr, Position)>), + Or(Box), /// true True(Position), /// false @@ -1038,7 +1067,7 @@ pub enum Expr { /// () Unit(Position), /// Custom syntax - Custom(Box<(CustomExpr, Position)>), + Custom(Box), } impl Default for Expr { @@ -1143,15 +1172,14 @@ impl Expr { Self::Stmt(x) => x.1, Self::Variable(x) => (x.0).1, Self::FnCall(x) => (x.0).3, - Self::Assignment(x) => x.0.position(), - Self::And(x) | Self::Or(x) | Self::In(x) => x.2, + Self::And(x) | Self::Or(x) | Self::In(x) => x.pos, Self::True(pos) | Self::False(pos) | Self::Unit(pos) => *pos, - Self::Dot(x) | Self::Index(x) => x.0.position(), + Self::Dot(x) | Self::Index(x) => x.lhs.position(), - Self::Custom(x) => x.1, + Self::Custom(x) => x.pos, } } @@ -1175,16 +1203,10 @@ impl Expr { Self::Property(x) => x.1 = new_pos, Self::Stmt(x) => x.1 = new_pos, Self::FnCall(x) => (x.0).3 = new_pos, - Self::And(x) => x.2 = new_pos, - Self::Or(x) => x.2 = new_pos, - Self::In(x) => x.2 = new_pos, - Self::True(pos) => *pos = new_pos, - Self::False(pos) => *pos = new_pos, - Self::Unit(pos) => *pos = new_pos, - Self::Assignment(x) => x.3 = new_pos, - Self::Dot(x) => x.2 = new_pos, - Self::Index(x) => x.2 = new_pos, - Self::Custom(x) => x.1 = new_pos, + Self::And(x) | Self::Or(x) | Self::In(x) => x.pos = new_pos, + Self::True(pos) | Self::False(pos) | Self::Unit(pos) => *pos = new_pos, + Self::Dot(x) | Self::Index(x) => x.pos = new_pos, + Self::Custom(x) => x.pos = new_pos, } self @@ -1200,8 +1222,7 @@ impl Expr { Self::Array(x) => x.0.iter().all(Self::is_pure), Self::Index(x) | Self::And(x) | Self::Or(x) | Self::In(x) => { - let (lhs, rhs, _) = x.as_ref(); - lhs.is_pure() && rhs.is_pure() + x.lhs.is_pure() && x.rhs.is_pure() } Self::Stmt(x) => x.0.is_pure(), @@ -1244,7 +1265,7 @@ impl Expr { Self::Map(x) => x.0.iter().map(|(_, expr)| expr).all(Self::is_literal), // Check in expression - Self::In(x) => match (&x.0, &x.1) { + Self::In(x) => match (&x.lhs, &x.rhs) { (Self::StringConstant(_), Self::StringConstant(_)) | (Self::CharConstant(_), Self::StringConstant(_)) => true, _ => false, @@ -1277,7 +1298,7 @@ impl Expr { Self::Map(x) => x.0.iter().map(|(_, expr)| expr).all(Self::is_constant), // Check in expression - Self::In(x) => match (&x.0, &x.1) { + Self::In(x) => match (&x.lhs, &x.rhs) { (Self::StringConstant(_), Self::StringConstant(_)) | (Self::CharConstant(_), Self::StringConstant(_)) => true, _ => false, @@ -1303,8 +1324,7 @@ impl Expr { | Self::Or(_) | Self::True(_) | Self::False(_) - | Self::Unit(_) - | Self::Assignment(_) => false, + | Self::Unit(_) => false, Self::StringConstant(_) | Self::Stmt(_) @@ -1579,7 +1599,6 @@ fn parse_index_chain( } Expr::CharConstant(_) - | Expr::Assignment(_) | Expr::And(_) | Expr::Or(_) | Expr::In(_) @@ -1615,7 +1634,6 @@ fn parse_index_chain( } Expr::CharConstant(_) - | Expr::Assignment(_) | Expr::And(_) | Expr::Or(_) | Expr::In(_) @@ -1646,13 +1664,6 @@ fn parse_index_chain( ) .into_err(x.position())) } - // lhs[??? = ??? ] - x @ Expr::Assignment(_) => { - return Err(PERR::MalformedIndexExpr( - "Array access expects integer index, not an assignment".into(), - ) - .into_err(x.position())) - } // lhs[()] x @ Expr::Unit(_) => { return Err(PERR::MalformedIndexExpr( @@ -1693,7 +1704,11 @@ fn parse_index_chain( let idx_expr = parse_index_chain(input, state, lib, idx_expr, settings.level_up())?; // Indexing binds to right - Ok(Expr::Index(Box::new((lhs, idx_expr, prev_pos)))) + Ok(Expr::Index(Box::new(BinaryExpr { + lhs, + rhs: idx_expr, + pos: prev_pos, + }))) } // Otherwise terminate the indexing chain _ => { @@ -1701,10 +1716,18 @@ fn parse_index_chain( // Terminate with an `Expr::Expr` wrapper to prevent the last index expression // inside brackets to be mis-parsed as another level of indexing, or a // dot expression/function call to be mis-parsed as following the indexing chain. - Expr::Index(_) | Expr::Dot(_) | Expr::FnCall(_) => Ok(Expr::Index( - Box::new((lhs, Expr::Expr(Box::new(idx_expr)), settings.pos)), - )), - _ => Ok(Expr::Index(Box::new((lhs, idx_expr, settings.pos)))), + Expr::Index(_) | Expr::Dot(_) | Expr::FnCall(_) => { + Ok(Expr::Index(Box::new(BinaryExpr { + lhs, + rhs: Expr::Expr(Box::new(idx_expr)), + pos: settings.pos, + }))) + } + _ => Ok(Expr::Index(Box::new(BinaryExpr { + lhs, + rhs: idx_expr, + pos: settings.pos, + }))), } } } @@ -2218,18 +2241,18 @@ fn make_assignment_stmt<'a>( lhs: Expr, rhs: Expr, pos: Position, -) -> Result { +) -> Result { match &lhs { // var (non-indexed) = rhs Expr::Variable(x) if x.3.is_none() => { - Ok(Expr::Assignment(Box::new((lhs, fn_name.into(), rhs, pos)))) + Ok(Stmt::Assignment(Box::new((lhs, fn_name.into(), rhs)), pos)) } // var (indexed) = rhs Expr::Variable(x) => { let ((name, name_pos), _, _, index) = x.as_ref(); match state.stack[(state.stack.len() - index.unwrap().get())].1 { ScopeEntryType::Normal => { - Ok(Expr::Assignment(Box::new((lhs, fn_name.into(), rhs, pos)))) + Ok(Stmt::Assignment(Box::new((lhs, fn_name.into(), rhs)), pos)) } // Constant values cannot be assigned to ScopeEntryType::Constant => { @@ -2238,17 +2261,17 @@ fn make_assignment_stmt<'a>( } } // xxx[???] = rhs, xxx.??? = rhs - Expr::Index(x) | Expr::Dot(x) => match &x.0 { + Expr::Index(x) | Expr::Dot(x) => match &x.lhs { // var[???] (non-indexed) = rhs, var.??? (non-indexed) = rhs Expr::Variable(x) if x.3.is_none() => { - Ok(Expr::Assignment(Box::new((lhs, fn_name.into(), rhs, pos)))) + Ok(Stmt::Assignment(Box::new((lhs, fn_name.into(), rhs)), pos)) } // var[???] (indexed) = rhs, var.??? (indexed) = rhs Expr::Variable(x) => { let ((name, name_pos), _, _, index) = x.as_ref(); match state.stack[(state.stack.len() - index.unwrap().get())].1 { ScopeEntryType::Normal => { - Ok(Expr::Assignment(Box::new((lhs, fn_name.into(), rhs, pos)))) + Ok(Stmt::Assignment(Box::new((lhs, fn_name.into(), rhs)), pos)) } // Constant values cannot be assigned to ScopeEntryType::Constant => { @@ -2257,7 +2280,7 @@ fn make_assignment_stmt<'a>( } } // expr[???] = rhs, expr.??? = rhs - _ => Err(PERR::AssignmentToCopy.into_err(x.0.position())), + _ => Err(PERR::AssignmentToInvalidLHS("".to_string()).into_err(x.lhs.position())), }, // const_expr = rhs expr if expr.is_constant() => { @@ -2268,7 +2291,7 @@ fn make_assignment_stmt<'a>( Err(PERR::BadInput("Possibly a typo of '=='?".to_string()).into_err(pos)) } // expr = rhs - _ => Err(PERR::AssignmentToCopy.into_err(lhs.position())), + _ => Err(PERR::AssignmentToInvalidLHS("".to_string()).into_err(lhs.position())), } } @@ -2279,7 +2302,7 @@ fn parse_op_assignment_stmt( lib: &mut FunctionsLib, lhs: Expr, mut settings: ParseSettings, -) -> Result { +) -> Result { let (token, token_pos) = input.peek().unwrap(); settings.pos = *token_pos; @@ -2301,7 +2324,7 @@ fn parse_op_assignment_stmt( | Token::OrAssign | Token::XOrAssign => token.syntax(), - _ => return Ok(lhs), + _ => return Ok(Stmt::Expr(lhs)), }; let (_, pos) = input.next().unwrap(); @@ -2315,13 +2338,9 @@ fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { - let (idx_lhs, idx_expr, pos) = *x; - Expr::Index(Box::new(( - idx_lhs, - make_dot_expr(idx_expr, rhs, op_pos)?, - pos, - ))) + (Expr::Index(mut x), rhs) => { + x.rhs = make_dot_expr(x.rhs, rhs, op_pos)?; + Expr::Index(x) } // lhs.id (lhs, Expr::Variable(x)) if x.1.is_none() => { @@ -2331,31 +2350,47 @@ fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { return Err(PERR::PropertyExpected.into_err(x.1.unwrap()[0].1)); } // lhs.prop - (lhs, prop @ Expr::Property(_)) => Expr::Dot(Box::new((lhs, prop, op_pos))), + (lhs, prop @ Expr::Property(_)) => Expr::Dot(Box::new(BinaryExpr { + lhs, + rhs: prop, + pos: op_pos, + })), // lhs.dot_lhs.dot_rhs (lhs, Expr::Dot(x)) => { - let (dot_lhs, dot_rhs, pos) = *x; - Expr::Dot(Box::new(( + let rhs = Expr::Dot(Box::new(BinaryExpr { + lhs: x.lhs.into_property(), + rhs: x.rhs, + pos: x.pos, + })); + Expr::Dot(Box::new(BinaryExpr { lhs, - Expr::Dot(Box::new((dot_lhs.into_property(), dot_rhs, pos))), - op_pos, - ))) + rhs, + pos: op_pos, + })) } // lhs.idx_lhs[idx_rhs] (lhs, Expr::Index(x)) => { - let (dot_lhs, dot_rhs, pos) = *x; - Expr::Dot(Box::new(( + let rhs = Expr::Index(Box::new(BinaryExpr { + lhs: x.lhs.into_property(), + rhs: x.rhs, + pos: x.pos, + })); + Expr::Dot(Box::new(BinaryExpr { lhs, - Expr::Index(Box::new((dot_lhs.into_property(), dot_rhs, pos))), - op_pos, - ))) + rhs, + pos: op_pos, + })) } // lhs.Fn() or lhs.eval() (_, Expr::FnCall(x)) @@ -2376,7 +2411,11 @@ fn make_dot_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result Expr::Dot(Box::new((lhs, func, op_pos))), + (lhs, func @ Expr::FnCall(_)) => Expr::Dot(Box::new(BinaryExpr { + lhs, + rhs: func, + pos: op_pos, + })), // lhs.rhs (_, rhs) => return Err(PERR::PropertyExpected.into_err(rhs.position())), }) @@ -2389,7 +2428,6 @@ fn make_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { @@ -2452,13 +2490,6 @@ fn make_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { - return Err(PERR::MalformedInExpr( - "'in' expression for a string expects a string, not an assignment".into(), - ) - .into_err(x.position())) - } // () in "xxxx" (x @ Expr::Unit(_), Expr::StringConstant(_)) => { return Err(PERR::MalformedInExpr( @@ -2511,13 +2542,6 @@ fn make_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { - return Err(PERR::MalformedInExpr( - "'in' expression for an object map expects a string, not an assignment".into(), - ) - .into_err(x.position())) - } // () in #{...} (x @ Expr::Unit(_), Expr::Map(_)) => { return Err(PERR::MalformedInExpr( @@ -2529,7 +2553,11 @@ fn make_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result (), } - Ok(Expr::In(Box::new((lhs, rhs, op_pos)))) + Ok(Expr::In(Box::new(BinaryExpr { + lhs, + rhs, + pos: op_pos, + }))) } /// Parse a binary expression. @@ -2549,9 +2577,17 @@ fn parse_binary_op( let mut root = lhs; loop { - let (current_op, _) = input.peek().unwrap(); - let custom = state.engine.custom_keywords.as_ref(); - let precedence = current_op.precedence(custom); + let (current_op, current_pos) = input.peek().unwrap(); + let precedence = if let Token::Custom(c) = current_op { + // Custom operators + if let Some(Some(p)) = state.engine.custom_keywords.get(c) { + *p + } else { + return Err(PERR::Reserved(c.clone()).into_err(*current_pos)); + } + } else { + current_op.precedence() + }; let bind_right = current_op.is_bind_right(); // Bind left to the parent lhs expression if precedence is higher @@ -2574,7 +2610,17 @@ fn parse_binary_op( let rhs = parse_unary(input, state, lib, settings)?; - let next_precedence = input.peek().unwrap().0.precedence(custom); + let (next_op, next_pos) = input.peek().unwrap(); + let next_precedence = if let Token::Custom(c) = next_op { + // Custom operators + if let Some(Some(p)) = state.engine.custom_keywords.get(c) { + *p + } else { + return Err(PERR::Reserved(c.clone()).into_err(*next_pos)); + } + } else { + next_op.precedence() + }; // Bind to right if the next operator has higher precedence // If same precedence, then check if the operator binds right @@ -2626,12 +2672,20 @@ fn parse_binary_op( Token::Or => { let rhs = args.pop().unwrap(); let current_lhs = args.pop().unwrap(); - Expr::Or(Box::new((current_lhs, rhs, pos))) + Expr::Or(Box::new(BinaryExpr { + lhs: current_lhs, + rhs, + pos, + })) } Token::And => { let rhs = args.pop().unwrap(); let current_lhs = args.pop().unwrap(); - Expr::And(Box::new((current_lhs, rhs, pos))) + Expr::And(Box::new(BinaryExpr { + lhs: current_lhs, + rhs, + pos, + })) } Token::In => { let rhs = args.pop().unwrap(); @@ -2646,14 +2700,7 @@ fn parse_binary_op( make_dot_expr(current_lhs, rhs, pos)? } - Token::Custom(s) - if state - .engine - .custom_keywords - .as_ref() - .map(|c| c.contains_key(&s)) - .unwrap_or(false) => - { + Token::Custom(s) if state.engine.custom_keywords.contains_key(&s) => { // Accept non-native functions for custom operators let op = (op.0, false, op.2, op.3); Expr::FnCall(Box::new((op, None, hash, args, None))) @@ -2665,7 +2712,7 @@ fn parse_binary_op( } /// Parse a custom syntax. -fn parse_custom( +fn parse_custom_syntax( input: &mut TokenStream, state: &mut ParseState, lib: &mut FunctionsLib, @@ -2691,13 +2738,26 @@ fn parse_custom( _ => (), } - for segment in syntax.segments.iter() { + let parse_func = &syntax.parse; + + let mut segments: StaticVec<_> = Default::default(); + segments.push(key.to_string()); + + loop { settings.pos = input.peek().unwrap().1; let settings = settings.level_up(); - match segment.as_str() { + let token = + if let Some(seg) = parse_func(&segments).map_err(|err| err.0.into_err(settings.pos))? { + seg + } else { + break; + }; + + match token.as_str() { MARKER_IDENT => match input.next().unwrap() { (Token::Identifier(s), pos) => { + segments.push(s.clone()); exprs.push(Expr::Variable(Box::new(((s, pos), None, 0, None)))); } (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { @@ -2705,31 +2765,37 @@ fn parse_custom( } (_, pos) => return Err(PERR::VariableExpected.into_err(pos)), }, - MARKER_EXPR => exprs.push(parse_expr(input, state, lib, settings)?), + MARKER_EXPR => { + exprs.push(parse_expr(input, state, lib, settings)?); + segments.push(MARKER_EXPR.into()); + } MARKER_BLOCK => { let stmt = parse_block(input, state, lib, settings)?; let pos = stmt.position(); - exprs.push(Expr::Stmt(Box::new((stmt, pos)))) + exprs.push(Expr::Stmt(Box::new((stmt, pos)))); + segments.push(MARKER_BLOCK.into()); } - s => match input.peek().unwrap() { + s => match input.next().unwrap() { + (Token::LexError(err), pos) => return Err(err.into_err(pos)), (t, _) if t.syntax().as_ref() == s => { - input.next().unwrap(); + segments.push(t.syntax().into_owned()); } (_, pos) => { return Err(PERR::MissingToken( s.to_string(), - format!("for '{}' expression", key), + format!("for '{}' expression", segments[0]), ) - .into_err(*pos)) + .into_err(pos)) } }, } } - Ok(Expr::Custom(Box::new(( - CustomExpr(exprs, syntax.func.clone()), + Ok(Expr::Custom(Box::new(CustomExpr { + keywords: exprs, + func: syntax.func.clone(), pos, - )))) + }))) } /// Parse an expression. @@ -2745,16 +2811,21 @@ fn parse_expr( settings.ensure_level_within_max_limit(state.max_expr_depth)?; // Check if it is a custom syntax. - if let Some(ref custom) = state.engine.custom_syntax { + if !state.engine.custom_syntax.is_empty() { let (token, pos) = input.peek().unwrap(); let token_pos = *pos; match token { - Token::Custom(key) if custom.contains_key(key) => { - let custom = custom.get_key_value(key).unwrap(); - let (key, syntax) = custom; - input.next().unwrap(); - return parse_custom(input, state, lib, settings, key, syntax, token_pos); + Token::Custom(key) | Token::Reserved(key) | Token::Identifier(key) => { + match state.engine.custom_syntax.get_key_value(key) { + Some((key, syntax)) => { + input.next().unwrap(); + return parse_custom_syntax( + input, state, lib, settings, key, syntax, token_pos, + ); + } + _ => (), + } } _ => (), } @@ -2835,9 +2906,11 @@ fn parse_if( None }; - Ok(Stmt::IfThenElse(Box::new(( - guard, if_body, else_body, token_pos, - )))) + Ok(Stmt::IfThenElse( + guard, + Box::new((if_body, else_body)), + token_pos, + )) } /// Parse a while loop. @@ -2860,9 +2933,9 @@ fn parse_while( ensure_not_assignment(input)?; settings.is_breakable = true; - let body = parse_block(input, state, lib, settings.level_up())?; + let body = Box::new(parse_block(input, state, lib, settings.level_up())?); - Ok(Stmt::While(Box::new((guard, body, token_pos)))) + Ok(Stmt::While(guard, body, token_pos)) } /// Parse a loop statement. @@ -2881,9 +2954,9 @@ fn parse_loop( // loop { body } settings.is_breakable = true; - let body = parse_block(input, state, lib, settings.level_up())?; + let body = Box::new(parse_block(input, state, lib, settings.level_up())?); - Ok(Stmt::Loop(Box::new((body, token_pos)))) + Ok(Stmt::Loop(body, token_pos)) } /// Parse a for loop. @@ -2938,7 +3011,7 @@ fn parse_for( state.stack.truncate(prev_stack_len); - Ok(Stmt::For(Box::new((name, expr, body, token_pos)))) + Ok(Stmt::For(expr, Box::new((name, body)), token_pos)) } /// Parse a variable definition statement. @@ -2978,12 +3051,12 @@ fn parse_let( // let name = expr ScopeEntryType::Normal => { state.stack.push((name.clone(), ScopeEntryType::Normal)); - Ok(Stmt::Let(Box::new(((name, pos), init_value, token_pos)))) + Ok(Stmt::Let(Box::new((name, pos)), init_value, token_pos)) } // const name = { expr:constant } ScopeEntryType::Constant => { state.stack.push((name.clone(), ScopeEntryType::Constant)); - Ok(Stmt::Const(Box::new(((name, pos), init_value, token_pos)))) + Ok(Stmt::Const(Box::new((name, pos)), init_value, token_pos)) } } } @@ -3008,7 +3081,7 @@ fn parse_import( // import expr as ... if !match_token(input, Token::As).0 { - return Ok(Stmt::Import(Box::new((expr, None, token_pos)))); + return Ok(Stmt::Import(expr, None, token_pos)); } // import expr as name ... @@ -3023,11 +3096,11 @@ fn parse_import( state.modules.push(name.clone()); - Ok(Stmt::Import(Box::new(( + Ok(Stmt::Import( expr, - Some((name.into(), settings.pos)), + Some(Box::new((name.into(), settings.pos))), token_pos, - )))) + )) } /// Parse an export statement. @@ -3044,7 +3117,7 @@ fn parse_export( #[cfg(not(feature = "unchecked"))] settings.ensure_level_within_max_limit(_state.max_expr_depth)?; - let mut exports = StaticVec::new(); + let mut exports = Vec::new(); loop { let (id, id_pos) = match input.next().unwrap() { @@ -3099,7 +3172,7 @@ fn parse_export( }) .map_err(|(id2, pos)| PERR::DuplicatedExport(id2.to_string()).into_err(pos))?; - Ok(Stmt::Export(Box::new((exports, token_pos)))) + Ok(Stmt::Export(exports, token_pos)) } /// Parse a statement block. @@ -3125,7 +3198,7 @@ fn parse_block( #[cfg(not(feature = "unchecked"))] settings.ensure_level_within_max_limit(state.max_expr_depth)?; - let mut statements = StaticVec::new(); + let mut statements = Vec::new(); let prev_stack_len = state.stack.len(); #[cfg(not(feature = "no_module"))] @@ -3178,7 +3251,7 @@ fn parse_block( #[cfg(not(feature = "no_module"))] state.modules.truncate(prev_mods_len); - Ok(Stmt::Block(Box::new((statements, settings.pos)))) + Ok(Stmt::Block(statements, settings.pos)) } /// Parse an expression as a statement. @@ -3194,8 +3267,8 @@ fn parse_expr_stmt( settings.ensure_level_within_max_limit(state.max_expr_depth)?; let expr = parse_expr(input, state, lib, settings.level_up())?; - let expr = parse_op_assignment_stmt(input, state, lib, expr, settings.level_up())?; - Ok(Stmt::Expr(Box::new(expr))) + let stmt = parse_op_assignment_stmt(input, state, lib, expr, settings.level_up())?; + Ok(stmt) } /// Parse a single statement. @@ -3306,26 +3379,26 @@ fn parse_stmt( match input.peek().unwrap() { // `return`/`throw` at - (Token::EOF, pos) => Ok(Some(Stmt::ReturnWithVal(Box::new(( + (Token::EOF, pos) => Ok(Some(Stmt::ReturnWithVal( (return_type, token_pos), None, *pos, - ))))), + ))), // `return;` or `throw;` - (Token::SemiColon, _) => Ok(Some(Stmt::ReturnWithVal(Box::new(( + (Token::SemiColon, _) => Ok(Some(Stmt::ReturnWithVal( (return_type, token_pos), None, settings.pos, - ))))), + ))), // `return` or `throw` with expression (_, _) => { let expr = parse_expr(input, state, lib, settings.level_up())?; let pos = expr.position(); - Ok(Some(Stmt::ReturnWithVal(Box::new(( + Ok(Some(Stmt::ReturnWithVal( (return_type, token_pos), Some(expr), pos, - ))))) + ))) } } } @@ -3559,12 +3632,16 @@ fn make_curry_from_externals( #[cfg(not(feature = "no_closure"))] { // Statement block - let mut statements: StaticVec<_> = Default::default(); + let mut statements: Vec<_> = Default::default(); // Insert `Share` statements - statements.extend(externals.into_iter().map(|x| Stmt::Share(Box::new(x)))); + statements.extend( + externals + .into_iter() + .map(|(var_name, pos)| Stmt::Share(var_name, pos)), + ); // Final expression - statements.push(Stmt::Expr(Box::new(expr))); - Expr::Stmt(Box::new((Stmt::Block(Box::new((statements, pos))), pos))) + statements.push(Stmt::Expr(expr)); + Expr::Stmt(Box::new((Stmt::Block(statements, pos), pos))) } #[cfg(feature = "no_closure")] @@ -3742,7 +3819,7 @@ impl Engine { } } - let expr = vec![Stmt::Expr(Box::new(expr))]; + let expr = vec![Stmt::Expr(expr)]; Ok( // Optimize AST diff --git a/src/result.rs b/src/result.rs index e5faeeb8..cb43cff3 100644 --- a/src/result.rs +++ b/src/result.rs @@ -20,12 +20,18 @@ use crate::stdlib::{ /// /// All wrapped `Position` values represent the location in the script where the error occurs. /// +/// # Thread Safety +/// /// Currently, `EvalAltResult` is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. #[derive(Debug)] #[non_exhaustive] pub enum EvalAltResult { /// System error. Wrapped values are the error message and the internal error. + #[cfg(not(feature = "sync"))] ErrorSystem(String, Box), + /// System error. Wrapped values are the error message and the internal error. + #[cfg(feature = "sync")] + ErrorSystem(String, Box), /// Syntax error. ErrorParsing(ParseErrorType, Position), @@ -65,8 +71,6 @@ pub enum EvalAltResult { ErrorFor(Position), /// Data race detected when accessing a variable. Wrapped value is the variable name. ErrorDataRace(String, Position), - /// Assignment to an inappropriate LHS (left-hand-side) expression. - ErrorAssignmentToUnknownLHS(Position), /// Assignment to a constant variable. Wrapped value is the variable name. ErrorAssignmentToConstant(String, Position), /// Inappropriate property access. Wrapped value is the property name. @@ -123,9 +127,6 @@ impl EvalAltResult { Self::ErrorVariableNotFound(_, _) => "Variable not found", Self::ErrorModuleNotFound(_, _) => "Module not found", Self::ErrorDataRace(_, _) => "Data race detected when accessing variable", - Self::ErrorAssignmentToUnknownLHS(_) => { - "Assignment to an unsupported left-hand side expression" - } Self::ErrorAssignmentToConstant(_, _) => "Assignment to a constant variable", Self::ErrorMismatchOutputType(_, _, _) => "Output type is incorrect", Self::ErrorInExpr(_) => "Malformed 'in' expression", @@ -179,7 +180,6 @@ impl fmt::Display for EvalAltResult { Self::ErrorIndexingType(_, _) | Self::ErrorUnboundThis(_) | Self::ErrorFor(_) - | Self::ErrorAssignmentToUnknownLHS(_) | Self::ErrorInExpr(_) | Self::ErrorDotExpr(_, _) | Self::ErrorTooManyOperations(_) @@ -270,10 +270,10 @@ impl> From for Box { impl EvalAltResult { /// Can this error be caught? - pub fn catchable(&self) -> bool { + pub fn is_catchable(&self) -> bool { match self { Self::ErrorSystem(_, _) => false, - Self::ErrorParsing(_, _) => false, + Self::ErrorParsing(_, _) => unreachable!(), Self::ErrorFunctionNotFound(_, _) | Self::ErrorInFunctionCall(_, _, _) @@ -287,7 +287,6 @@ impl EvalAltResult { | Self::ErrorVariableNotFound(_, _) | Self::ErrorModuleNotFound(_, _) | Self::ErrorDataRace(_, _) - | Self::ErrorAssignmentToUnknownLHS(_) | Self::ErrorAssignmentToConstant(_, _) | Self::ErrorMismatchOutputType(_, _, _) | Self::ErrorInExpr(_) @@ -299,9 +298,28 @@ impl EvalAltResult { | Self::ErrorTooManyModules(_) | Self::ErrorStackOverflow(_) | Self::ErrorDataTooLarge(_, _, _, _) - | Self::ErrorTerminated(_) - | Self::LoopBreak(_, _) - | Self::Return(_, _) => false, + | Self::ErrorTerminated(_) => false, + + Self::LoopBreak(_, _) | Self::Return(_, _) => unreachable!(), + } + } + + /// Is this error a system exception? + pub fn is_system_exception(&self) -> bool { + match self { + Self::ErrorSystem(_, _) => true, + Self::ErrorParsing(_, _) => unreachable!(), + + Self::ErrorTooManyOperations(_) + | Self::ErrorTooManyModules(_) + | Self::ErrorStackOverflow(_) + | Self::ErrorDataTooLarge(_, _, _, _) => true, + + Self::ErrorTerminated(_) => true, + + Self::LoopBreak(_, _) | Self::Return(_, _) => unreachable!(), + + _ => false, } } @@ -323,7 +341,6 @@ impl EvalAltResult { | Self::ErrorVariableNotFound(_, pos) | Self::ErrorModuleNotFound(_, pos) | Self::ErrorDataRace(_, pos) - | Self::ErrorAssignmentToUnknownLHS(pos) | Self::ErrorAssignmentToConstant(_, pos) | Self::ErrorMismatchOutputType(_, _, pos) | Self::ErrorInExpr(pos) @@ -358,7 +375,6 @@ impl EvalAltResult { | Self::ErrorVariableNotFound(_, pos) | Self::ErrorModuleNotFound(_, pos) | Self::ErrorDataRace(_, pos) - | Self::ErrorAssignmentToUnknownLHS(pos) | Self::ErrorAssignmentToConstant(_, pos) | Self::ErrorMismatchOutputType(_, _, pos) | Self::ErrorInExpr(pos) diff --git a/src/scope.rs b/src/scope.rs index c1c78181..d36fa478 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -44,6 +44,10 @@ pub struct Entry<'a> { /// Type containing information about the current scope. /// Useful for keeping state between `Engine` evaluation runs. /// +/// # Thread Safety +/// +/// Currently, `Scope` is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. +/// /// # Example /// /// ``` @@ -67,15 +71,13 @@ pub struct Entry<'a> { /// /// When searching for entries, newly-added entries are found before similarly-named but older entries, /// allowing for automatic _shadowing_. -/// -/// Currently, `Scope` is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. #[derive(Debug, Clone, Default)] pub struct Scope<'a>(Vec>); impl<'a> Scope<'a> { /// Create a new Scope. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Scope; @@ -92,7 +94,7 @@ impl<'a> Scope<'a> { /// Empty the Scope. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Scope; @@ -117,7 +119,7 @@ impl<'a> Scope<'a> { /// Get the number of entries inside the Scope. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Scope; @@ -135,7 +137,7 @@ impl<'a> Scope<'a> { /// Is the Scope empty? /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Scope; @@ -153,7 +155,7 @@ impl<'a> Scope<'a> { /// Add (push) a new entry to the Scope. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Scope; @@ -174,7 +176,7 @@ impl<'a> Scope<'a> { /// Add (push) a new `Dynamic` entry to the Scope. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Dynamic, Scope}; @@ -197,7 +199,7 @@ impl<'a> Scope<'a> { /// However, in order to be used for optimization, constants must be in one of the recognized types: /// `INT` (default to `i64`, `i32` if `only_i32`), `f64`, `String`, `char` and `bool`. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Scope; @@ -225,7 +227,7 @@ impl<'a> Scope<'a> { /// recognized types: /// `INT` (default to `i64`, `i32` if `only_i32`), `f64`, `String`, `char` and `bool`. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Dynamic, Scope}; @@ -272,7 +274,7 @@ impl<'a> Scope<'a> { /// Truncate (rewind) the Scope to a previous size. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Scope; @@ -304,7 +306,7 @@ impl<'a> Scope<'a> { /// Does the scope contain the entry? /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Scope; @@ -350,7 +352,7 @@ impl<'a> Scope<'a> { /// Get the value of an entry in the Scope, starting from the last. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Scope; @@ -374,7 +376,7 @@ impl<'a> Scope<'a> { /// /// Panics when trying to update the value of a constant. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::Scope; @@ -450,7 +452,7 @@ impl<'a> Scope<'a> { /// Get an iterator to entries in the Scope. /// - /// # Examples + /// # Example /// /// ``` /// use rhai::{Dynamic, Scope}; diff --git a/src/serde_impl/de.rs b/src/serde_impl/de.rs index ad73731a..8a199b14 100644 --- a/src/serde_impl/de.rs +++ b/src/serde_impl/de.rs @@ -70,7 +70,7 @@ impl<'de> DynamicDeserializer<'de> { /// Deserialize a `Dynamic` value into a Rust type that implements `serde::Deserialize`. /// -/// # Examples +/// # Example /// /// ``` /// # fn main() -> Result<(), Box> { diff --git a/src/serde_impl/ser.rs b/src/serde_impl/ser.rs index 78c64f38..14689166 100644 --- a/src/serde_impl/ser.rs +++ b/src/serde_impl/ser.rs @@ -46,7 +46,7 @@ impl DynamicSerializer { /// Serialize a Rust type that implements `serde::Serialize` into a `Dynamic`. /// -/// # Examples +/// # Example /// /// ``` /// # fn main() -> Result<(), Box> { diff --git a/src/settings.rs b/src/settings.rs index bd23114a..987187f1 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -240,15 +240,7 @@ impl Engine { /// ``` #[inline(always)] pub fn disable_symbol(&mut self, symbol: &str) -> &mut Self { - if self.disabled_symbols.is_none() { - self.disabled_symbols = Some(Default::default()); - } - - self.disabled_symbols - .as_mut() - .unwrap() - .insert(symbol.into()); - + self.disabled_symbols.insert(symbol.into()); self } @@ -256,7 +248,7 @@ impl Engine { /// /// The operator must be a valid identifier (i.e. it cannot be a symbol). /// - /// # Examples + /// # Example /// /// ```rust /// # fn main() -> Result<(), Box> { @@ -291,28 +283,14 @@ impl Engine { // 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) => - { - () - } + Some(token) if !self.disabled_symbols.contains(token.syntax().as_ref()) => (), // 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()); - } - self.custom_keywords - .as_mut() - .unwrap() - .insert(keyword.into(), precedence); + .insert(keyword.into(), Some(precedence)); Ok(self) } diff --git a/src/syntax.rs b/src/syntax.rs index c5e9d138..83149911 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -7,11 +7,12 @@ use crate::fn_native::{SendSync, Shared}; use crate::parser::Expr; use crate::result::EvalAltResult; use crate::token::{is_valid_identifier, Position, Token}; +use crate::utils::ImmutableString; use crate::StaticVec; use crate::stdlib::{ boxed::Box, - fmt, format, + format, string::{String, ToString}, }; @@ -24,6 +25,14 @@ pub type FnCustomSyntaxEval = pub type FnCustomSyntaxEval = dyn Fn(&mut EvalContext, &[Expression]) -> Result> + Send + Sync; +/// A general expression parsing trait object. +#[cfg(not(feature = "sync"))] +pub type FnCustomSyntaxParse = dyn Fn(&[String]) -> Result, ParseError>; +/// A general expression parsing trait object. +#[cfg(feature = "sync")] +pub type FnCustomSyntaxParse = + dyn Fn(&[String]) -> Result, ParseError> + Send + Sync; + /// An expression sub-tree in an AST. #[derive(Debug, Clone, Hash)] pub struct Expression<'a>(&'a Expr); @@ -76,20 +85,12 @@ impl EvalContext<'_, '_, '_, '_, '_, '_, '_, '_, '_> { } } -#[derive(Clone)] pub struct CustomSyntax { - pub segments: StaticVec, + pub parse: Box, pub func: Shared, pub scope_delta: isize, } -impl fmt::Debug for CustomSyntax { - #[inline(always)] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self.segments, f) - } -} - impl Engine { /// Register a custom syntax with the `Engine`. /// @@ -119,47 +120,40 @@ impl Engine { let seg = match s { // Markers not in first position MARKER_EXPR | MARKER_BLOCK | MARKER_IDENT if !segments.is_empty() => s.to_string(), - // Standard symbols not in first position + // Standard or reserved keyword/symbol 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() { - self.custom_keywords = Some(Default::default()); - } - - if !self.custom_keywords.as_ref().unwrap().contains_key(s) { - self.custom_keywords.as_mut().unwrap().insert(s.into(), 0); - } + // Make it a custom keyword/symbol + if !self.custom_keywords.contains_key(s) { + self.custom_keywords.insert(s.into(), None); } - s.into() } - // Identifier - s if is_valid_identifier(s.chars()) => { - if self.custom_keywords.is_none() { - self.custom_keywords = Some(Default::default()); + // Standard keyword in first position + s if segments.is_empty() + && Token::lookup_from_syntax(s) + .map(|v| v.is_keyword() || v.is_reserved()) + .unwrap_or(false) => + { + return Err(LexError::ImproperSymbol(format!( + "Improper symbol for custom syntax at position #{}: '{}'", + segments.len() + 1, + s + )) + .into_err(Position::none()) + .into()); + } + // Identifier in first position + s if segments.is_empty() && is_valid_identifier(s.chars()) => { + if !self.custom_keywords.contains_key(s) { + self.custom_keywords.insert(s.into(), None); } - - if !self.custom_keywords.as_ref().unwrap().contains_key(s) { - self.custom_keywords.as_mut().unwrap().insert(s.into(), 0); - } - s.into() } // Anything else is an error _ => { return Err(LexError::ImproperSymbol(format!( - "Improper symbol for custom syntax: '{}'", + "Improper symbol for custom syntax at position #{}: '{}'", + segments.len() + 1, s )) .into_err(Position::none()) @@ -175,24 +169,54 @@ impl Engine { return Ok(self); } - // Remove the first keyword as the discriminator - let key = segments.remove(0); + // The first keyword is the discriminator + let key = segments[0].clone(); + self.register_custom_syntax_raw( + key, + // Construct the parsing function + move |stream| { + if stream.len() >= segments.len() { + Ok(None) + } else { + Ok(Some(segments[stream.len()].clone())) + } + }, + new_vars, + func, + ); + + Ok(self) + } + + /// Register a custom syntax with the `Engine`. + /// + /// ## WARNING - Low Level API + /// + /// This function is very low level. + /// + /// * `new_vars` is the number of new variables declared by this custom syntax, or the number of variables removed (if negative). + /// * `parse` is the parsing function. + /// * `func` is the implementation function. + /// + /// All custom keywords must be manually registered via `Engine::register_custom_operator`. + /// Otherwise, custom keywords won't be recognized. + pub fn register_custom_syntax_raw( + &mut self, + key: impl Into, + parse: impl Fn(&[String]) -> Result, ParseError> + SendSync + 'static, + new_vars: isize, + func: impl Fn(&mut EvalContext, &[Expression]) -> Result> + + SendSync + + 'static, + ) -> &mut Self { let syntax = CustomSyntax { - segments, + parse: Box::new(parse), func: (Box::new(func) as Box).into(), scope_delta: new_vars, }; - if self.custom_syntax.is_none() { - self.custom_syntax = Some(Default::default()); - } - - self.custom_syntax - .as_mut() - .unwrap() - .insert(key, syntax.into()); - - Ok(self) + self.custom_syntax.insert(key.into(), syntax); + self } } diff --git a/src/token.rs b/src/token.rs index 3a27c4cf..82d7c611 100644 --- a/src/token.rs +++ b/src/token.rs @@ -18,9 +18,7 @@ use crate::parser::FLOAT; use crate::stdlib::{ borrow::Cow, boxed::Box, - char, - collections::HashMap, - fmt, format, + char, fmt, format, iter::Peekable, str::{Chars, FromStr}, string::{String, ToString}, @@ -610,7 +608,7 @@ impl Token { } /// Get the precedence number of the token. - pub fn precedence(&self, custom: Option<&HashMap>) -> u8 { + pub fn precedence(&self) -> u8 { use Token::*; match self { @@ -639,9 +637,6 @@ impl Token { Period => 240, - // Custom operators - Custom(s) => custom.map_or(0, |c| *c.get(s).unwrap()), - _ => 0, } } @@ -692,7 +687,7 @@ impl Token { Import | Export | As => true, True | False | Let | Const | If | Else | While | Loop | For | In | Continue | Break - | Return | Throw => true, + | Return | Throw | Try | Catch => true, _ => false, } @@ -1647,16 +1642,12 @@ impl<'a> Iterator for TokenIterator<'a, '_> { type Item = (Token, Position); fn next(&mut self) -> Option { - let token = match ( - get_next_token(&mut self.stream, &mut self.state, &mut self.pos), - self.engine.disabled_symbols.as_ref(), - self.engine.custom_keywords.as_ref(), - ) { + let token = match get_next_token(&mut self.stream, &mut self.state, &mut self.pos) { // {EOF} - (None, _, _) => None, + None => None, // 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)) + Some((Token::Reserved(s), pos)) => Some((match + (s.as_str(), self.engine.custom_keywords.contains_key(&s)) { ("===", false) => Token::LexError(Box::new(LERR::ImproperSymbol( "'===' is not a valid operator. This is not JavaScript! Should it be '=='?".to_string(), @@ -1691,21 +1682,19 @@ impl<'a> Iterator for TokenIterator<'a, '_> { format!("'{}' is a reserved symbol", token) ))), // 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( + (token, false) if self.engine.disabled_symbols.contains(token) => Token::LexError(Box::new(LERR::ImproperSymbol( format!("reserved symbol '{}' is disabled", token) ))), // Reserved keyword/operator that is not custom. (_, false) => Token::Reserved(s), }, pos)), // Custom keyword - (Some((Token::Identifier(s), pos)), _, Some(custom)) if custom.contains_key(&s) => { + Some((Token::Identifier(s), pos)) if self.engine.custom_keywords.contains_key(&s) => { Some((Token::Custom(s), pos)) } // Custom standard keyword - must be disabled - (Some((token, pos)), Some(disabled), Some(custom)) - if token.is_keyword() && custom.contains_key(token.syntax().as_ref()) => - { - if disabled.contains(token.syntax().as_ref()) { + Some((token, pos)) if token.is_keyword() && self.engine.custom_keywords.contains_key(token.syntax().as_ref()) => { + if self.engine.disabled_symbols.contains(token.syntax().as_ref()) { // Disabled standard keyword Some((Token::Custom(token.syntax().into()), pos)) } else { @@ -1714,21 +1703,17 @@ impl<'a> Iterator for TokenIterator<'a, '_> { } } // Disabled operator - (Some((token, pos)), Some(disabled), _) - if token.is_operator() && disabled.contains(token.syntax().as_ref()) => - { + Some((token, pos)) if token.is_operator() && self.engine.disabled_symbols.contains(token.syntax().as_ref()) => { 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()) => - { + Some((token, pos)) if token.is_keyword() && self.engine.disabled_symbols.contains(token.syntax().as_ref()) => { Some((Token::Reserved(token.syntax().into()), pos)) } - (r, _, _) => r, + r => r, }; match token { diff --git a/src/utils.rs b/src/utils.rs index 06a7ef53..7c01117e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -96,7 +96,7 @@ pub fn calc_fn_hash<'a>( /// An `ImmutableString` wraps an `Rc` (or `Arc` under the `sync` feature) /// so that it can be simply shared and not cloned. /// -/// # Examples +/// # Example /// /// ``` /// use rhai::ImmutableString; diff --git a/tests/stack.rs b/tests/stack.rs index b161e192..3319a55c 100644 --- a/tests/stack.rs +++ b/tests/stack.rs @@ -18,12 +18,15 @@ fn test_stack_overflow_fn_calls() -> Result<(), Box> { #[cfg(not(feature = "unchecked"))] assert!(matches!( - *engine.eval::<()>( - r" + *engine + .eval::<()>( + r" fn foo(n) { if n == 0 { 0 } else { n + foo(n-1) } } foo(1000) - ").expect_err("should error"), - EvalAltResult::ErrorInFunctionCall(name, _, _) if name.starts_with("foo > foo > foo") + " + ) + .expect_err("should error"), + EvalAltResult::ErrorStackOverflow(_) )); Ok(()) diff --git a/tests/syntax.rs b/tests/syntax.rs index c0433e9e..9cda2bf6 100644 --- a/tests/syntax.rs +++ b/tests/syntax.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, EvalAltResult, EvalContext, Expression, ParseErrorType, INT}; +use rhai::{Engine, EvalAltResult, ParseError, ParseErrorType, Position, INT}; #[test] fn test_custom_syntax() -> Result<(), Box> { @@ -19,10 +19,10 @@ fn test_custom_syntax() -> Result<(), Box> { engine.register_custom_syntax( &[ - "do", "|", "$ident$", "|", "->", "$block$", "while", "$expr$", + "exec", "|", "$ident$", "|", "->", "$block$", "while", "$expr$", ], 1, - |context: &mut EvalContext, inputs: &[Expression]| { + |context, inputs| { let var_name = inputs[0].get_variable_name().unwrap().to_string(); let stmt = inputs.get(1).unwrap(); let condition = inputs.get(2).unwrap(); @@ -52,13 +52,10 @@ fn test_custom_syntax() -> Result<(), Box> { }, )?; - // 'while' is now a custom keyword so this it can no longer be a variable - engine.consume("let while = 0").expect_err("should error"); - assert_eq!( engine.eval::( r" - do |x| -> { x += 1 } while x < 42; + exec |x| -> { x += 1 } while x < 42; x " )?, @@ -71,7 +68,48 @@ fn test_custom_syntax() -> Result<(), Box> { .register_custom_syntax(&["!"], 0, |_, _| Ok(().into())) .expect_err("should error") .0, - ParseErrorType::BadInput("Improper symbol for custom syntax: '!'".to_string()) + ParseErrorType::BadInput( + "Improper symbol for custom syntax at position #1: '!'".to_string() + ) + ); + + Ok(()) +} + +#[test] +fn test_custom_syntax_raw() -> Result<(), Box> { + let mut engine = Engine::new(); + + engine.register_custom_syntax_raw( + "hello", + |stream| match stream.len() { + 0 => unreachable!(), + 1 => Ok(Some("$ident$".to_string())), + 2 => match stream[1].as_str() { + "world" | "kitty" => Ok(None), + s => Err(ParseError( + Box::new(ParseErrorType::BadInput(s.to_string())), + Position::none(), + )), + }, + _ => unreachable!(), + }, + 0, + |_, inputs| { + Ok(match inputs[0].get_variable_name().unwrap() { + "world" => 123 as INT, + "kitty" => 42 as INT, + _ => unreachable!(), + } + .into()) + }, + ); + + assert_eq!(engine.eval::("hello world")?, 123); + assert_eq!(engine.eval::("hello kitty")?, 42); + assert_eq!( + *engine.compile("hello hey").expect_err("should error").0, + ParseErrorType::BadInput("hey".to_string()) ); Ok(())