diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c0f4af9..d67f2585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Rhai Release Notes Version 0.19.14 =============== +This version runs faster due to optimizations done on AST node structures. It also fixes a number of +panic bugs related to passing shared values as function call arguments. + Bug fixes --------- @@ -27,18 +30,24 @@ Breaking changes * `Module::update_fn_metadata` input parameter is changed. * Function keywords (e.g. `type_of`, `eval`, `Fn`) can no longer be overloaded. It is more trouble than worth. To disable these keywords, use `Engine::disable_symbol`. * `is_def_var` and `is_def_fn` are now reserved keywords. -* `Engine::id` field is removed. +* `Engine::id` field is removed because it is never used. * `num-traits` is now a required dependency. +* The `in` operator is now implemented on top of the `contains` function and is no longer restricted to a few specific types. +* `EvalAltResult::ErrorInExpr` is removed because the `in` operator now calls `contains`. +* The methods `AST::walk`, `Expr::walk`, `Stmt::walk` and `ASTNode::walk` and the callbacks they take now return `bool` to optionally terminate the recursive walk. Enhancements ------------ +* Layout of AST nodes is optimized to reduce redirections, so speed is improved. * Function calls are more optimized and should now run faster. * `range` function now supports negative step and decreasing streams (i.e. to < from). * More information is provided to the error variable captured by the `catch` statement in an _object map_. * Previously, `private` functions in an `AST` cannot be called with `call_fn` etc. This is inconvenient when trying to call a function inside a script which also serves as a loadable module exporting part (but not all) of the functions. Now, all functions (`private` or not) can be called in an `AST`. The `private` keyword is relegated to preventing a function from being exported. * `Dynamic::as_unit` just for completeness sake. * `bytes` method added for strings to get length quickly (if the string is ASCII-only). +* `FileModuleResolver` can now enable/disable caching. +* Recursively walking an `AST` can now be terminated in the middle. Version 0.19.13 diff --git a/src/ast.rs b/src/ast.rs index 81b77671..9ce4c8bf 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -47,7 +47,7 @@ pub enum FnAccess { #[derive(Debug, Clone)] pub struct ScriptFnDef { /// Function body. - pub body: Stmt, + pub body: StmtBlock, /// Encapsulated running environment, if any. pub lib: Option>, /// Encapsulated imported modules. @@ -61,9 +61,9 @@ pub struct ScriptFnDef { pub params: StaticVec, /// Access to external variables. #[cfg(not(feature = "no_closure"))] - pub externals: Vec, + pub externals: StaticVec, /// Function doc-comments (if any). - pub comments: Vec, + pub comments: StaticVec, } impl fmt::Display for ScriptFnDef { @@ -149,7 +149,7 @@ pub struct AST { /// Source of the [`AST`]. source: Option, /// Global statements. - statements: Vec, + body: StmtBlock, /// Script-defined functions. functions: Shared, /// Embedded module resolver, if any. @@ -162,7 +162,7 @@ impl Default for AST { fn default() -> Self { Self { source: None, - statements: Vec::with_capacity(16), + body: Default::default(), functions: Default::default(), #[cfg(not(feature = "no_module"))] resolver: None, @@ -179,7 +179,10 @@ impl AST { ) -> Self { Self { source: None, - statements: statements.into_iter().collect(), + body: StmtBlock { + statements: statements.into_iter().collect(), + pos: Position::NONE, + }, functions: functions.into(), #[cfg(not(feature = "no_module"))] resolver: None, @@ -194,7 +197,10 @@ impl AST { ) -> Self { Self { source: Some(source.into()), - statements: statements.into_iter().collect(), + body: StmtBlock { + statements: statements.into_iter().collect(), + pos: Position::NONE, + }, functions: functions.into(), #[cfg(not(feature = "no_module"))] resolver: None, @@ -231,7 +237,7 @@ impl AST { #[cfg(not(feature = "internals"))] #[inline(always)] pub(crate) fn statements(&self) -> &[Stmt] { - &self.statements + &self.body.statements } /// _(INTERNALS)_ Get the statements. /// Exported under the `internals` feature only. @@ -239,13 +245,13 @@ impl AST { #[deprecated = "this method is volatile and may change"] #[inline(always)] pub fn statements(&self) -> &[Stmt] { - &self.statements + &self.body.statements } /// 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.statements + pub(crate) fn statements_mut(&mut self) -> &mut StaticVec { + &mut self.body.statements } /// Get the internal shared [`Module`] containing all script-defined functions. #[cfg(not(feature = "internals"))] @@ -333,7 +339,7 @@ impl AST { functions.merge_filtered(&self.functions, &filter); Self { source: self.source.clone(), - statements: Default::default(), + body: Default::default(), functions: functions.into(), #[cfg(not(feature = "no_module"))] resolver: self.resolver.clone(), @@ -345,7 +351,7 @@ impl AST { pub fn clone_statements_only(&self) -> Self { Self { source: self.source.clone(), - statements: self.statements.clone(), + body: self.body.clone(), functions: Default::default(), #[cfg(not(feature = "no_module"))] resolver: self.resolver.clone(), @@ -515,20 +521,19 @@ impl AST { filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool, ) -> Self { let Self { - statements, - functions, - .. + body, functions, .. } = self; - let ast = match (statements.is_empty(), other.statements.is_empty()) { + let merged = match (body.is_empty(), other.body.is_empty()) { (false, false) => { - let mut statements = statements.clone(); - statements.extend(other.statements.iter().cloned()); - statements + let mut body = body.clone(); + body.statements + .extend(other.body.statements.iter().cloned()); + body } - (false, true) => statements.clone(), - (true, false) => other.statements.clone(), - (true, true) => vec![], + (false, true) => body.clone(), + (true, false) => other.body.clone(), + (true, true) => Default::default(), }; let source = other.source.clone().or_else(|| self.source.clone()); @@ -537,9 +542,9 @@ impl AST { functions.merge_filtered(&other.functions, &filter); if let Some(source) = source { - Self::new_with_source(ast, functions, source) + Self::new_with_source(merged.statements, functions, source) } else { - Self::new(ast, functions) + Self::new(merged.statements, functions) } } /// Combine one [`AST`] with another. The second [`AST`] is consumed. @@ -599,7 +604,10 @@ impl AST { other: Self, filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool, ) -> &mut Self { - self.statements.extend(other.statements.into_iter()); + self.body + .statements + .extend(other.body.statements.into_iter()); + if !other.functions.is_empty() { shared_make_mut(&mut self.functions).merge_filtered(&other.functions, &filter); } @@ -673,45 +681,51 @@ impl AST { /// Clear all statements in the [`AST`], leaving only function definitions. #[inline(always)] pub fn clear_statements(&mut self) { - self.statements = vec![]; + self.body = Default::default(); } /// Recursively walk the [`AST`], including function bodies (if any). + /// Return `false` from the callback to terminate the walk. #[cfg(not(feature = "internals"))] #[cfg(not(feature = "no_module"))] #[inline(always)] - pub(crate) fn walk(&self, on_node: &mut impl FnMut(&[ASTNode])) { - self.statements() - .iter() - .chain({ - #[cfg(not(feature = "no_function"))] - { - self.iter_fn_def().map(|f| &f.body) - } - #[cfg(feature = "no_function")] - { - crate::stdlib::iter::empty() - } - }) - .for_each(|stmt| stmt.walk(&mut Default::default(), on_node)); + pub(crate) fn walk(&self, on_node: &mut impl FnMut(&[ASTNode]) -> bool) -> bool { + let path = &mut Default::default(); + + for stmt in self.statements() { + if !stmt.walk(path, on_node) { + return false; + } + } + #[cfg(not(feature = "no_function"))] + for stmt in self.iter_fn_def().flat_map(|f| f.body.statements.iter()) { + if !stmt.walk(path, on_node) { + return false; + } + } + + true } /// _(INTERNALS)_ Recursively walk the [`AST`], including function bodies (if any). + /// Return `false` from the callback to terminate the walk. /// Exported under the `internals` feature only. #[cfg(feature = "internals")] #[inline(always)] - pub fn walk(&self, on_node: &mut impl FnMut(&[ASTNode])) { - self.statements() - .iter() - .chain({ - #[cfg(not(feature = "no_function"))] - { - self.iter_fn_def().map(|f| &f.body) - } - #[cfg(feature = "no_function")] - { - crate::stdlib::iter::empty() - } - }) - .for_each(|stmt| stmt.walk(&mut Default::default(), on_node)); + pub fn walk(&self, on_node: &mut impl FnMut(&[ASTNode]) -> bool) -> bool { + let path = &mut Default::default(); + + for stmt in self.statements() { + if !stmt.walk(path, on_node) { + return false; + } + } + #[cfg(not(feature = "no_function"))] + for stmt in self.iter_fn_def().flat_map(|f| f.body.statements.iter()) { + if !stmt.walk(path, on_node) { + return false; + } + } + + true } } @@ -804,6 +818,41 @@ impl<'a> From<&'a Expr> for ASTNode<'a> { } } +/// _(INTERNALS)_ A statements block. +/// Exported under the `internals` feature only. +/// +/// # Volatile Data Structure +/// +/// This type is volatile and may change. +#[derive(Clone, Hash, Default)] +pub struct StmtBlock { + pub statements: StaticVec, + pub pos: Position, +} + +impl StmtBlock { + /// Is this statements block empty? + #[inline(always)] + pub fn is_empty(&self) -> bool { + self.statements.is_empty() + } + /// Number of statements in this statements block. + #[inline(always)] + pub fn len(&self) -> usize { + self.statements.len() + } +} + +impl fmt::Debug for StmtBlock { + #[inline(always)] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.pos.is_none() { + write!(f, "{} @ ", self.pos)?; + } + fmt::Debug::fmt(&self.statements, f) + } +} + /// _(INTERNALS)_ A statement. /// Exported under the `internals` feature only. /// @@ -815,32 +864,36 @@ pub enum Stmt { /// No-op. Noop(Position), /// `if` expr `{` stmt `}` `else` `{` stmt `}` - If(Expr, Box<(Stmt, Option)>, Position), + If(Expr, Box<(StmtBlock, StmtBlock)>, Position), /// `switch` expr `{` literal or _ `=>` stmt `,` ... `}` Switch( Expr, Box<( - HashableHashMap, - Option, + HashableHashMap, + StmtBlock, )>, Position, ), /// `while` expr `{` stmt `}` - While(Option, Box, Position), + While(Expr, Box, Position), /// `do` `{` stmt `}` `while`|`until` expr - Do(Box, Expr, bool, Position), + Do(Box, Expr, bool, Position), /// `for` id `in` expr `{` stmt `}` - For(Expr, Box<(String, Stmt)>, Position), + For(Expr, Box<(String, StmtBlock)>, Position), /// \[`export`\] `let` id `=` expr - Let(Box, Option, bool, Position), + Let(Expr, Ident, bool, Position), /// \[`export`\] `const` id `=` expr - Const(Box, Option, bool, Position), + Const(Expr, Ident, bool, Position), /// expr op`=` expr Assignment(Box<(Expr, Expr, Option)>, Position), /// `{` stmt`;` ... `}` Block(Vec, Position), /// `try` `{` stmt; ... `}` `catch` `(` var `)` `{` stmt; ... `}` - TryCatch(Box<(Stmt, Option, Stmt)>, Position, Position), + TryCatch( + Box<(StmtBlock, Option, StmtBlock)>, + Position, + Position, + ), /// [expression][Expr] Expr(Expr), /// `continue` @@ -848,10 +901,10 @@ pub enum Stmt { /// `break` Break(Position), /// `return`/`throw` - Return((ReturnType, Position), Option, Position), + Return(ReturnType, Option, Position), /// `import` expr `as` var #[cfg(not(feature = "no_module"))] - Import(Expr, Option>, Position), + Import(Expr, Option, Position), /// `export` var `as` var `,` ... #[cfg(not(feature = "no_module"))] Export(Vec<(Ident, Option)>, Position), @@ -867,6 +920,27 @@ impl Default for Stmt { } } +impl From for StmtBlock { + #[inline(always)] + fn from(stmt: Stmt) -> Self { + match stmt { + Stmt::Block(block, pos) => Self { + statements: block.into(), + pos, + }, + Stmt::Noop(pos) => Self { + statements: Default::default(), + pos, + }, + _ => { + let pos = stmt.position(); + let statements = vec![stmt].into(); + Self { statements, pos } + } + } + } +} + impl Stmt { /// Is this statement [`Noop`][Stmt::Noop]? #[inline(always)] @@ -889,7 +963,7 @@ impl Stmt { | Self::While(_, _, pos) | Self::Do(_, _, _, pos) | Self::For(_, _, pos) - | Self::Return((_, pos), _, _) + | Self::Return(_, _, pos) | Self::Let(_, _, _, pos) | Self::Const(_, _, _, pos) | Self::TryCatch(_, pos, _) => *pos, @@ -918,7 +992,7 @@ impl Stmt { | Self::While(_, _, pos) | Self::Do(_, _, _, pos) | Self::For(_, _, pos) - | Self::Return((_, pos), _, _) + | Self::Return(_, _, pos) | Self::Let(_, _, _, pos) | Self::Const(_, _, _, pos) | Self::TryCatch(_, pos, _) => *pos = new_pos, @@ -938,6 +1012,31 @@ impl Stmt { self } + /// Does this statement return a value? + pub fn returns_value(&self) -> bool { + match self { + Self::If(_, _, _) | Self::Switch(_, _, _) | Self::Block(_, _) | Self::Expr(_) => true, + + Self::Noop(_) + | Self::While(_, _, _) + | Self::Do(_, _, _, _) + | Self::For(_, _, _) + | Self::TryCatch(_, _, _) => false, + + Self::Let(_, _, _, _) + | Self::Const(_, _, _, _) + | Self::Assignment(_, _) + | Self::Continue(_) + | Self::Break(_) + | Self::Return(_, _, _) => false, + + #[cfg(not(feature = "no_module"))] + Self::Import(_, _, _) | Self::Export(_, _) => false, + + #[cfg(not(feature = "no_closure"))] + Self::Share(_) => unreachable!("Stmt::Share should not be parsed"), + } + } /// Is this statement self-terminated (i.e. no need for a semicolon terminator)? pub fn is_self_terminated(&self) -> bool { match self { @@ -976,23 +1075,29 @@ impl Stmt { Self::Expr(expr) => expr.is_pure(), Self::If(condition, x, _) => { condition.is_pure() - && x.0.is_pure() - && x.1.as_ref().map(Stmt::is_pure).unwrap_or(true) + && x.0.statements.iter().all(Stmt::is_pure) + && x.1.statements.iter().all(Stmt::is_pure) } Self::Switch(expr, x, _) => { expr.is_pure() - && x.0.values().all(Stmt::is_pure) - && x.1.as_ref().map(Stmt::is_pure).unwrap_or(true) + && x.0 + .values() + .flat_map(|block| block.statements.iter()) + .all(Stmt::is_pure) + && x.1.statements.iter().all(Stmt::is_pure) } - Self::While(Some(condition), block, _) | Self::Do(block, condition, _, _) => { - condition.is_pure() && block.is_pure() + Self::While(condition, block, _) | Self::Do(block, condition, _, _) => { + condition.is_pure() && block.statements.iter().all(Stmt::is_pure) + } + Self::For(iterable, x, _) => { + iterable.is_pure() && x.1.statements.iter().all(Stmt::is_pure) } - Self::While(None, 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::Return(_, _, _) => false, - Self::TryCatch(x, _, _) => x.0.is_pure() && x.2.is_pure(), + Self::TryCatch(x, _, _) => { + x.0.statements.iter().all(Stmt::is_pure) && x.2.statements.iter().all(Stmt::is_pure) + } #[cfg(not(feature = "no_module"))] Self::Import(_, _, _) => false, @@ -1003,53 +1108,150 @@ impl Stmt { Self::Share(_) => false, } } - /// Recursively walk this statement. + /// Is this statement _pure_ within the containing block? + /// + /// An internally pure statement only has side effects that disappear outside the block. + /// + /// Only variable declarations (i.e. `let` and `const`) and `import`/`export` statements + /// are internally pure. #[inline(always)] - pub fn walk<'a>(&'a self, path: &mut Vec>, on_node: &mut impl FnMut(&[ASTNode])) { + pub fn is_internally_pure(&self) -> bool { + match self { + Self::Let(expr, _, _, _) | Self::Const(expr, _, _, _) => expr.is_pure(), + + #[cfg(not(feature = "no_module"))] + Self::Import(expr, _, _) => expr.is_pure(), + #[cfg(not(feature = "no_module"))] + Self::Export(_, _) => true, + + _ => self.is_pure(), + } + } + /// Does this statement break the current control flow through the containing block? + /// + /// Currently this is only true for `return`, `throw`, `break` and `continue`. + /// + /// All statements following this statement will essentially be dead code. + #[inline(always)] + pub fn is_control_flow_break(&self) -> bool { + match self { + Self::Return(_, _, _) | Self::Break(_) | Self::Continue(_) => true, + _ => false, + } + } + /// Recursively walk this statement. + /// Return `false` from the callback to terminate the walk. + pub fn walk<'a>( + &'a self, + path: &mut Vec>, + on_node: &mut impl FnMut(&[ASTNode]) -> bool, + ) -> bool { path.push(self.into()); - on_node(path); + + if !on_node(path) { + return false; + } match self { - Self::Let(_, Some(e), _, _) | Self::Const(_, Some(e), _, _) => e.walk(path, on_node), + Self::Let(e, _, _, _) | Self::Const(e, _, _, _) => { + if !e.walk(path, on_node) { + return false; + } + } Self::If(e, x, _) => { - e.walk(path, on_node); - x.0.walk(path, on_node); - if let Some(ref s) = x.1 { - s.walk(path, on_node); + if !e.walk(path, on_node) { + return false; + } + for s in &x.0.statements { + if !s.walk(path, on_node) { + return false; + } + } + for s in &x.1.statements { + if !s.walk(path, on_node) { + return false; + } } } Self::Switch(e, x, _) => { - e.walk(path, on_node); - x.0.values().for_each(|s| s.walk(path, on_node)); - if let Some(ref s) = x.1 { - s.walk(path, on_node); + if !e.walk(path, on_node) { + return false; + } + for s in x.0.values().flat_map(|block| block.statements.iter()) { + if !s.walk(path, on_node) { + return false; + } + } + for s in &x.1.statements { + if !s.walk(path, on_node) { + return false; + } } } - Self::While(Some(e), s, _) | Self::Do(s, e, _, _) => { - e.walk(path, on_node); - s.walk(path, on_node); + Self::While(e, s, _) | Self::Do(s, e, _, _) => { + if !e.walk(path, on_node) { + return false; + } + for s in &s.statements { + if !s.walk(path, on_node) { + return false; + } + } } - Self::While(None, s, _) => s.walk(path, on_node), Self::For(e, x, _) => { - e.walk(path, on_node); - x.1.walk(path, on_node); + if !e.walk(path, on_node) { + return false; + } + for s in &x.1.statements { + if !s.walk(path, on_node) { + return false; + } + } } Self::Assignment(x, _) => { - x.0.walk(path, on_node); - x.1.walk(path, on_node); + if !x.0.walk(path, on_node) { + return false; + } + if !x.1.walk(path, on_node) { + return false; + } + } + Self::Block(x, _) => { + for s in x { + if !s.walk(path, on_node) { + return false; + } + } } - Self::Block(x, _) => x.iter().for_each(|s| s.walk(path, on_node)), Self::TryCatch(x, _, _) => { - x.0.walk(path, on_node); - x.2.walk(path, on_node); + for s in &x.0.statements { + if !s.walk(path, on_node) { + return false; + } + } + for s in &x.2.statements { + if !s.walk(path, on_node) { + return false; + } + } + } + Self::Expr(e) | Self::Return(_, Some(e), _) => { + if !e.walk(path, on_node) { + return false; + } } - Self::Expr(e) | Self::Return(_, Some(e), _) => e.walk(path, on_node), #[cfg(not(feature = "no_module"))] - Self::Import(e, _, _) => e.walk(path, on_node), + Self::Import(e, _, _) => { + if !e.walk(path, on_node) { + return false; + } + } _ => (), } path.pop().unwrap(); + + true } } @@ -1102,7 +1304,7 @@ pub struct OpAssignment { /// # Volatile Data Structure /// /// This type is volatile and may change. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)] +#[derive(Clone, Copy, Eq, PartialEq, Hash, Default)] pub struct FnHash { /// Pre-calculated hash for a script-defined function ([`None`] if native functions only). script: Option, @@ -1110,6 +1312,20 @@ pub struct FnHash { native: u64, } +impl fmt::Debug for FnHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(script) = self.script { + if script == self.native { + write!(f, "({}=={})", script, self.native) + } else { + write!(f, "({}, {})", script, self.native) + } + } else { + write!(f, "{}", self.native) + } + } +} + impl FnHash { /// Create a [`FnHash`] with only the native Rust hash. #[inline(always)] @@ -1185,6 +1401,7 @@ pub struct FloatWrapper(FLOAT); #[cfg(not(feature = "no_float"))] impl Hash for FloatWrapper { + #[inline(always)] fn hash(&self, state: &mut H) { self.0.to_ne_bytes().hash(state); } @@ -1192,6 +1409,7 @@ impl Hash for FloatWrapper { #[cfg(not(feature = "no_float"))] impl AsRef for FloatWrapper { + #[inline(always)] fn as_ref(&self) -> &FLOAT { &self.0 } @@ -1199,6 +1417,7 @@ impl AsRef for FloatWrapper { #[cfg(not(feature = "no_float"))] impl AsMut for FloatWrapper { + #[inline(always)] fn as_mut(&mut self) -> &mut FLOAT { &mut self.0 } @@ -1208,6 +1427,7 @@ impl AsMut for FloatWrapper { impl crate::stdlib::ops::Deref for FloatWrapper { type Target = FLOAT; + #[inline(always)] fn deref(&self) -> &Self::Target { &self.0 } @@ -1215,6 +1435,7 @@ impl crate::stdlib::ops::Deref for FloatWrapper { #[cfg(not(feature = "no_float"))] impl crate::stdlib::ops::DerefMut for FloatWrapper { + #[inline(always)] fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } @@ -1222,6 +1443,7 @@ impl crate::stdlib::ops::DerefMut for FloatWrapper { #[cfg(not(feature = "no_float"))] impl fmt::Debug for FloatWrapper { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(self, f) } @@ -1229,6 +1451,7 @@ impl fmt::Debug for FloatWrapper { #[cfg(not(feature = "no_float"))] impl fmt::Display for FloatWrapper { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { #[cfg(feature = "no_std")] use num_traits::Float; @@ -1244,6 +1467,7 @@ impl fmt::Display for FloatWrapper { #[cfg(not(feature = "no_float"))] impl From for FloatWrapper { + #[inline(always)] fn from(value: FLOAT) -> Self { Self::new(value) } @@ -1253,6 +1477,7 @@ impl From for FloatWrapper { impl FromStr for FloatWrapper { type Err = ::Err; + #[inline(always)] fn from_str(s: &str) -> Result { FLOAT::from_str(s).map(Into::::into) } @@ -1260,6 +1485,7 @@ impl FromStr for FloatWrapper { #[cfg(not(feature = "no_float"))] impl FloatWrapper { + #[inline(always)] pub const fn new(value: FLOAT) -> Self { Self(value) } @@ -1298,18 +1524,16 @@ pub enum Expr { Unit(Position), /// Variable access - (optional index, optional (hash, modules), variable name) Variable(Box<(Option, Option<(u64, NamespaceRef)>, Ident)>), - /// Property access - (getter, hash, setter, hash, prop) - Property(Box<(ImmutableString, u64, ImmutableString, u64, Ident)>), - /// { [statement][Stmt] } - Stmt(Box>, Position), + /// Property access - ((getter, hash), (setter, hash), prop) + Property(Box<((ImmutableString, u64), (ImmutableString, u64), Ident)>), + /// { [statement][Stmt] ... } + Stmt(Box), /// func `(` expr `,` ... `)` FnCall(Box, Position), /// lhs `.` rhs Dot(Box, Position), /// expr `[` expr `]` Index(Box, Position), - /// lhs `in` rhs - In(Box, Position), /// lhs `&&` rhs And(Box, Position), /// lhs `||` rhs @@ -1329,6 +1553,7 @@ impl Expr { /// Get the [`Dynamic`] value of a constant expression. /// /// Returns [`None`] if the expression is not constant. + #[inline] pub fn get_constant_value(&self) -> Option { Some(match self { Self::DynamicConstant(x, _) => x.as_ref().clone(), @@ -1379,6 +1604,7 @@ impl Expr { } } /// Get the [position][Position] of the expression. + #[inline] pub fn position(&self) -> Position { match self { #[cfg(not(feature = "no_float"))] @@ -1392,12 +1618,12 @@ impl Expr { Self::FnPointer(_, pos) => *pos, Self::Array(_, pos) => *pos, Self::Map(_, pos) => *pos, - Self::Property(x) => (x.4).pos, - Self::Stmt(_, pos) => *pos, + Self::Property(x) => (x.2).pos, + Self::Stmt(x) => x.pos, Self::Variable(x) => (x.2).pos, Self::FnCall(_, pos) => *pos, - Self::And(x, _) | Self::Or(x, _) | Self::In(x, _) => x.lhs.position(), + Self::And(x, _) | Self::Or(x, _) => x.lhs.position(), Self::Unit(pos) => *pos, @@ -1407,6 +1633,7 @@ impl Expr { } } /// Override the [position][Position] of the expression. + #[inline] pub fn set_position(&mut self, new_pos: Position) -> &mut Self { match self { #[cfg(not(feature = "no_float"))] @@ -1421,10 +1648,10 @@ impl Expr { Self::Array(_, pos) => *pos = new_pos, Self::Map(_, pos) => *pos = new_pos, Self::Variable(x) => (x.2).pos = new_pos, - Self::Property(x) => (x.4).pos = new_pos, - Self::Stmt(_, pos) => *pos = new_pos, + Self::Property(x) => (x.2).pos = new_pos, + Self::Stmt(x) => x.pos = new_pos, Self::FnCall(_, pos) => *pos = new_pos, - Self::And(_, pos) | Self::Or(_, pos) | Self::In(_, pos) => *pos = new_pos, + Self::And(_, pos) | Self::Or(_, pos) => *pos = new_pos, Self::Unit(pos) => *pos = new_pos, Self::Dot(_, pos) | Self::Index(_, pos) => *pos = new_pos, Self::Custom(_, pos) => *pos = new_pos, @@ -1435,17 +1662,18 @@ impl Expr { /// Is the expression pure? /// /// A pure expression has no side effects. + #[inline] pub fn is_pure(&self) -> bool { match self { Self::Array(x, _) => x.iter().all(Self::is_pure), Self::Map(x, _) => x.iter().map(|(_, v)| v).all(Self::is_pure), - Self::Index(x, _) | Self::And(x, _) | Self::Or(x, _) | Self::In(x, _) => { + Self::Index(x, _) | Self::And(x, _) | Self::Or(x, _) => { x.lhs.is_pure() && x.rhs.is_pure() } - Self::Stmt(x, _) => x.iter().all(Stmt::is_pure), + Self::Stmt(x) => x.statements.iter().all(Stmt::is_pure), Self::Variable(_) => true, @@ -1461,6 +1689,7 @@ impl Expr { } } /// Is the expression a constant? + #[inline] pub fn is_constant(&self) -> bool { match self { #[cfg(not(feature = "no_float"))] @@ -1480,17 +1709,11 @@ impl Expr { // An map literal is constant if all items are constant Self::Map(x, _) => x.iter().map(|(_, expr)| expr).all(Self::is_constant), - // Check in expression - Self::In(x, _) => match (&x.lhs, &x.rhs) { - (Self::StringConstant(_, _), Self::StringConstant(_, _)) - | (Self::CharConstant(_, _), Self::StringConstant(_, _)) => true, - _ => false, - }, - _ => false, } } /// Is a particular [token][Token] allowed as a postfix operator to this expression? + #[inline] pub fn is_valid_postfix(&self, token: &Token) -> bool { match token { #[cfg(not(feature = "no_object"))] @@ -1507,14 +1730,13 @@ impl Expr { | Self::IntegerConstant(_, _) | Self::CharConstant(_, _) | Self::FnPointer(_, _) - | Self::In(_, _) | Self::And(_, _) | Self::Or(_, _) | Self::Unit(_) => false, Self::StringConstant(_, _) | Self::FnCall(_, _) - | Self::Stmt(_, _) + | Self::Stmt(_) | Self::Dot(_, _) | Self::Index(_, _) | Self::Array(_, _) @@ -1544,29 +1766,68 @@ impl Expr { } } /// Recursively walk this expression. - #[inline(always)] - pub fn walk<'a>(&'a self, path: &mut Vec>, on_node: &mut impl FnMut(&[ASTNode])) { + /// Return `false` from the callback to terminate the walk. + pub fn walk<'a>( + &'a self, + path: &mut Vec>, + on_node: &mut impl FnMut(&[ASTNode]) -> bool, + ) -> bool { path.push(self.into()); - on_node(path); + + if !on_node(path) { + return false; + } match self { - Self::Stmt(x, _) => x.iter().for_each(|s| s.walk(path, on_node)), - Self::Array(x, _) => x.iter().for_each(|e| e.walk(path, on_node)), - Self::Map(x, _) => x.iter().for_each(|(_, e)| e.walk(path, on_node)), - Self::Index(x, _) - | Self::Dot(x, _) - | Expr::In(x, _) - | Expr::And(x, _) - | Expr::Or(x, _) => { - x.lhs.walk(path, on_node); - x.rhs.walk(path, on_node); + Self::Stmt(x) => { + for s in &x.statements { + if !s.walk(path, on_node) { + return false; + } + } + } + Self::Array(x, _) => { + for e in x.as_ref() { + if !e.walk(path, on_node) { + return false; + } + } + } + Self::Map(x, _) => { + for (_, e) in x.as_ref() { + if !e.walk(path, on_node) { + return false; + } + } + } + Self::Index(x, _) | Self::Dot(x, _) | Expr::And(x, _) | Expr::Or(x, _) => { + if !x.lhs.walk(path, on_node) { + return false; + } + if !x.rhs.walk(path, on_node) { + return false; + } + } + Self::FnCall(x, _) => { + for e in &x.args { + if !e.walk(path, on_node) { + return false; + } + } + } + Self::Custom(x, _) => { + for e in &x.keywords { + if !e.walk(path, on_node) { + return false; + } + } } - Self::FnCall(x, _) => x.args.iter().for_each(|e| e.walk(path, on_node)), - Self::Custom(x, _) => x.keywords.iter().for_each(|e| e.walk(path, on_node)), _ => (), } path.pop().unwrap(); + + true } } @@ -1583,8 +1844,8 @@ mod tests { assert_eq!(size_of::(), 4); assert_eq!(size_of::(), 16); assert_eq!(size_of::>(), 16); - assert_eq!(size_of::(), 32); - assert_eq!(size_of::>(), 32); + assert_eq!(size_of::(), 40); + assert_eq!(size_of::>(), 40); assert_eq!(size_of::(), 32); assert_eq!(size_of::(), 48); assert_eq!(size_of::(), 56); diff --git a/src/bin/rhai-repl.rs b/src/bin/rhai-repl.rs index c5c91a18..fedd1dc0 100644 --- a/src/bin/rhai-repl.rs +++ b/src/bin/rhai-repl.rs @@ -1,8 +1,5 @@ use rhai::{Dynamic, Engine, EvalAltResult, Module, Scope, AST}; -#[cfg(not(feature = "no_optimize"))] -use rhai::OptimizationLevel; - use std::{ env, fs::File, @@ -56,35 +53,38 @@ fn print_help() { } fn main() { - let mut engine = Engine::new(); - println!("Rhai REPL tool"); println!("=============="); print_help(); - // Load init scripts + // Initialize scripting engine + let mut engine = Engine::new(); #[cfg(not(feature = "no_module"))] { + // Set a file module resolver without caching + let mut resolver = rhai::module_resolvers::FileModuleResolver::new(); + resolver.enable_cache(false); + engine.set_module_resolver(resolver); + + // Load init scripts let mut contents = String::new(); let mut has_init_scripts = false; for filename in env::args().skip(1) { - { - contents.clear(); + contents.clear(); - let mut f = match File::open(&filename) { - Err(err) => { - eprintln!("Error reading script file: {}\n{}", filename, err); - exit(1); - } - Ok(f) => f, - }; - - if let Err(err) = f.read_to_string(&mut contents) { - println!("Error reading script file: {}\n{}", filename, err); + let mut f = match File::open(&filename) { + Err(err) => { + eprintln!("Error reading script file: {}\n{}", filename, err); exit(1); } + Ok(f) => f, + }; + + if let Err(err) = f.read_to_string(&mut contents) { + println!("Error reading script file: {}\n{}", filename, err); + exit(1); } let module = match engine @@ -119,9 +119,8 @@ fn main() { } // Setup Engine - #[cfg(not(feature = "no_optimize"))] - engine.set_optimization_level(OptimizationLevel::None); + engine.set_optimization_level(rhai::OptimizationLevel::None); let mut scope = Scope::new(); @@ -130,8 +129,10 @@ fn main() { let mut ast_u: AST = Default::default(); let mut ast: AST = Default::default(); - // REPL loop + // Make Engine immutable + let engine = engine; + // REPL loop 'main_loop: loop { print!("rhai-repl> "); stdout().flush().expect("couldn't flush stdout"); @@ -233,7 +234,7 @@ fn main() { #[cfg(not(feature = "no_optimize"))] { - ast = engine.optimize_ast(&scope, r, OptimizationLevel::Simple); + ast = engine.optimize_ast(&scope, r, rhai::OptimizationLevel::Simple); } #[cfg(feature = "no_optimize")] diff --git a/src/engine.rs b/src/engine.rs index 30f6d1a4..973cf56b 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,6 +1,6 @@ //! Main module defining the script evaluation [`Engine`]. -use crate::ast::{Expr, FnCallExpr, FnHash, Ident, OpAssignment, ReturnType, Stmt}; +use crate::ast::{Expr, FnCallExpr, FnHash, Ident, OpAssignment, ReturnType, Stmt, StmtBlock}; use crate::dynamic::{map_std_type_name, AccessMode, Union, Variant}; use crate::fn_native::{ CallableFunction, IteratorFn, OnDebugCallback, OnPrintCallback, OnProgressCallback, @@ -17,7 +17,6 @@ use crate::stdlib::{ collections::{HashMap, HashSet}, fmt, format, hash::{Hash, Hasher}, - iter::empty, num::{NonZeroU64, NonZeroU8, NonZeroUsize}, ops::DerefMut, string::{String, ToString}, @@ -25,12 +24,12 @@ use crate::stdlib::{ use crate::syntax::CustomSyntax; use crate::utils::{get_hasher, StraightHasherBuilder}; use crate::{ - calc_fn_hash, Dynamic, EvalAltResult, FnPtr, ImmutableString, Module, Position, RhaiResult, - Scope, Shared, StaticVec, + Dynamic, EvalAltResult, FnPtr, ImmutableString, Module, Position, RhaiResult, Scope, Shared, + StaticVec, }; #[cfg(not(feature = "no_index"))] -use crate::Array; +use crate::{calc_fn_hash, stdlib::iter::empty, Array}; #[cfg(not(feature = "no_index"))] pub const TYPICAL_ARRAY_SIZE: usize = 8; // Small arrays are typical @@ -203,9 +202,15 @@ pub const FN_IDX_GET: &str = "index$get$"; pub const FN_IDX_SET: &str = "index$set$"; #[cfg(not(feature = "no_function"))] pub const FN_ANONYMOUS: &str = "anon$"; -#[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] + +/// Standard equality comparison operator. pub const OP_EQUALS: &str = "=="; +/// Standard method function for containment testing. +/// +/// The `in` operator is implemented as a call to this method. +pub const OP_CONTAINS: &str = "contains"; + /// Method of chaining. #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] @@ -223,11 +228,11 @@ pub enum ChainType { #[derive(Debug, Clone, Hash)] pub enum ChainArgument { /// Dot-property access. - Property, + Property(Position), /// Arguments to a dot-function call. - FnCallArgs(StaticVec), + FnCallArgs(StaticVec, StaticVec), /// Index value. - IndexValue(Dynamic), + IndexValue(Dynamic, Position), } #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] @@ -241,8 +246,10 @@ impl ChainArgument { #[cfg(not(feature = "no_index"))] pub fn as_index_value(self) -> Dynamic { match self { - Self::Property | Self::FnCallArgs(_) => panic!("expecting ChainArgument::IndexValue"), - Self::IndexValue(value) => value, + Self::Property(_) | Self::FnCallArgs(_, _) => { + panic!("expecting ChainArgument::IndexValue") + } + Self::IndexValue(value, _) => value, } } /// Return the `StaticVec` value. @@ -252,27 +259,29 @@ impl ChainArgument { /// Panics if not `ChainArgument::FnCallArgs`. #[inline(always)] #[cfg(not(feature = "no_object"))] - pub fn as_fn_call_args(self) -> StaticVec { + pub fn as_fn_call_args(self) -> (StaticVec, StaticVec) { match self { - Self::Property | Self::IndexValue(_) => panic!("expecting ChainArgument::FnCallArgs"), - Self::FnCallArgs(value) => value, + Self::Property(_) | Self::IndexValue(_, _) => { + panic!("expecting ChainArgument::FnCallArgs") + } + Self::FnCallArgs(values, positions) => (values, positions), } } } #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] -impl From> for ChainArgument { +impl From<(StaticVec, StaticVec)> for ChainArgument { #[inline(always)] - fn from(value: StaticVec) -> Self { - Self::FnCallArgs(value) + fn from((values, positions): (StaticVec, StaticVec)) -> Self { + Self::FnCallArgs(values, positions) } } #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] -impl From for ChainArgument { +impl From<(Dynamic, Position)> for ChainArgument { #[inline(always)] - fn from(value: Dynamic) -> Self { - Self::IndexValue(value) + fn from((value, pos): (Dynamic, Position)) -> Self { + Self::IndexValue(value, pos) } } @@ -395,7 +404,11 @@ impl<'a> Target<'a> { } /// 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, pos: Position) -> Result<(), Box> { + pub fn set_value( + &mut self, + new_val: Dynamic, + _pos: Position, + ) -> Result<(), Box> { match self { Self::Ref(r) => **r = new_val, #[cfg(not(feature = "no_closure"))] @@ -411,7 +424,7 @@ impl<'a> Target<'a> { Box::new(EvalAltResult::ErrorMismatchDataType( "char".to_string(), err.to_string(), - pos, + _pos, )) })?; @@ -893,6 +906,7 @@ impl Engine { } /// Search for a module within an imports stack. + #[inline] pub(crate) fn search_imports( &self, mods: &Imports, @@ -932,7 +946,7 @@ impl Engine { match expr { Expr::Variable(v) => match v.as_ref() { // Qualified variable - (_, Some((hash_var, modules)), Ident { name, pos }) => { + (_, Some((hash_var, modules)), Ident { name, pos, .. }) => { let module = self.search_imports(mods, state, modules).ok_or_else(|| { EvalAltResult::ErrorModuleNotFound( modules[0].name.to_string(), @@ -971,7 +985,7 @@ impl Engine { this_ptr: &'s mut Option<&mut Dynamic>, expr: &Expr, ) -> Result<(Target<'s>, Position), Box> { - let (index, _, Ident { name, pos }) = match expr { + let (index, _, Ident { name, pos, .. }) = match expr { Expr::Variable(v) => v.as_ref(), _ => unreachable!("Expr::Variable expected, but gets {:?}", expr), }; @@ -1152,9 +1166,9 @@ impl Engine { // xxx.fn_name(arg_expr_list) Expr::FnCall(x, pos) if x.namespace.is_none() && new_val.is_none() => { let FnCallExpr { name, hash, .. } = x.as_ref(); - let args = idx_val.as_fn_call_args(); + let mut args = idx_val.as_fn_call_args(); self.make_method_call( - mods, state, lib, name, *hash, target, args, *pos, level, + mods, state, lib, name, *hash, target, &mut args, *pos, level, ) } // xxx.fn_name(...) = ??? @@ -1167,7 +1181,7 @@ impl Engine { } // {xxx:map}.id op= ??? Expr::Property(x) if target_val.is::() && new_val.is_some() => { - let Ident { name, pos } = &x.4; + let Ident { name, pos, .. } = &x.2; let index = name.clone().into(); let val = self.get_indexed_mut( mods, state, lib, target_val, index, *pos, true, is_ref, false, level, @@ -1180,7 +1194,7 @@ impl Engine { } // {xxx:map}.id Expr::Property(x) if target_val.is::() => { - let Ident { name, pos } = &x.4; + let Ident { name, pos, .. } = &x.2; let index = name.clone().into(); let val = self.get_indexed_mut( mods, state, lib, target_val, index, *pos, false, is_ref, false, level, @@ -1190,7 +1204,7 @@ impl Engine { } // xxx.id = ??? Expr::Property(x) if new_val.is_some() => { - let (_, _, setter, hash_set, Ident { pos, .. }) = x.as_ref(); + let (_, (setter, hash_set), Ident { pos, .. }) = x.as_ref(); let hash = FnHash::from_native(*hash_set); let mut new_val = new_val; let mut args = [target_val, &mut (new_val.as_mut().unwrap().0).0]; @@ -1202,7 +1216,7 @@ impl Engine { } // xxx.id Expr::Property(x) => { - let (getter, hash_get, _, _, Ident { pos, .. }) = x.as_ref(); + let ((getter, hash_get), _, Ident { pos, .. }) = x.as_ref(); let hash = FnHash::from_native(*hash_get); let mut args = [target_val]; self.exec_fn_call( @@ -1215,7 +1229,7 @@ impl Engine { Expr::Index(x, x_pos) | Expr::Dot(x, x_pos) if target_val.is::() => { let mut val = match &x.lhs { Expr::Property(p) => { - let Ident { name, pos } = &p.4; + let Ident { name, pos, .. } = &p.2; let index = name.clone().into(); self.get_indexed_mut( mods, state, lib, target_val, index, *pos, false, is_ref, true, @@ -1225,9 +1239,9 @@ impl Engine { // {xxx:map}.fn_name(arg_expr_list)[expr] | {xxx:map}.fn_name(arg_expr_list).expr Expr::FnCall(x, pos) if x.namespace.is_none() => { let FnCallExpr { name, hash, .. } = x.as_ref(); - let args = idx_val.as_fn_call_args(); + let mut args = idx_val.as_fn_call_args(); let (val, _) = self.make_method_call( - mods, state, lib, name, *hash, target, args, *pos, level, + mods, state, lib, name, *hash, target, &mut args, *pos, level, )?; val.into() } @@ -1250,7 +1264,7 @@ impl Engine { match &x.lhs { // xxx.prop[expr] | xxx.prop.expr Expr::Property(p) => { - let (getter, hash_get, setter, hash_set, Ident { pos, .. }) = + let ((getter, hash_get), (setter, hash_set), Ident { pos, .. }) = p.as_ref(); let hash_get = FnHash::from_native(*hash_get); let hash_set = FnHash::from_native(*hash_set); @@ -1303,9 +1317,9 @@ impl Engine { // xxx.fn_name(arg_expr_list)[expr] | xxx.fn_name(arg_expr_list).expr Expr::FnCall(f, pos) if f.namespace.is_none() => { let FnCallExpr { name, hash, .. } = f.as_ref(); - let args = idx_val.as_fn_call_args(); + let mut args = idx_val.as_fn_call_args(); let (mut val, _) = self.make_method_call( - mods, state, lib, name, *hash, target, args, *pos, level, + mods, state, lib, name, *hash, target, &mut args, *pos, level, )?; let val = &mut val; let target = &mut val.into(); @@ -1364,6 +1378,7 @@ impl Engine { let Ident { name: var_name, pos: var_pos, + .. } = &x.2; self.inc_operations(state, *var_pos)?; @@ -1422,23 +1437,26 @@ impl Engine { match expr { Expr::FnCall(x, _) if parent_chain_type == ChainType::Dot && x.namespace.is_none() => { + let mut arg_positions: StaticVec<_> = Default::default(); + let arg_values = x .args .iter() .map(|arg_expr| { + arg_positions.push(arg_expr.position()); self.eval_expr(scope, mods, state, lib, this_ptr, arg_expr, level) .map(Dynamic::flatten) }) .collect::, _>>()?; - idx_values.push(arg_values.into()); + idx_values.push((arg_values, arg_positions).into()); } Expr::FnCall(_, _) if parent_chain_type == ChainType::Dot => { unreachable!("function call in dot chain should not be namespace-qualified") } - Expr::Property(_) if parent_chain_type == ChainType::Dot => { - idx_values.push(ChainArgument::Property) + Expr::Property(x) if parent_chain_type == ChainType::Dot => { + idx_values.push(ChainArgument::Property(x.2.pos)) } Expr::Property(_) => unreachable!("unexpected Expr::Property for indexing"), @@ -1447,29 +1465,33 @@ impl Engine { // Evaluate in left-to-right order let lhs_val = match lhs { - Expr::Property(_) if parent_chain_type == ChainType::Dot => { - ChainArgument::Property + Expr::Property(x) if parent_chain_type == ChainType::Dot => { + ChainArgument::Property(x.2.pos) } Expr::Property(_) => unreachable!("unexpected Expr::Property for indexing"), Expr::FnCall(x, _) if parent_chain_type == ChainType::Dot && x.namespace.is_none() => { - x.args + let mut arg_positions: StaticVec<_> = Default::default(); + + let arg_values = x + .args .iter() .map(|arg_expr| { + arg_positions.push(arg_expr.position()); self.eval_expr(scope, mods, state, lib, this_ptr, arg_expr, level) .map(Dynamic::flatten) }) - .collect::, _>>()? - .into() + .collect::, _>>()?; + + (arg_values, arg_positions).into() } Expr::FnCall(_, _) if parent_chain_type == ChainType::Dot => { unreachable!("function call in dot chain should not be namespace-qualified") } _ => self - .eval_expr(scope, mods, state, lib, this_ptr, lhs, level)? - .flatten() - .into(), + .eval_expr(scope, mods, state, lib, this_ptr, lhs, level) + .map(|v| (v.flatten(), lhs.position()).into())?, }; // Push in reverse order @@ -1486,9 +1508,8 @@ impl Engine { } _ => idx_values.push( - self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)? - .flatten() - .into(), + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level) + .map(|v| (v.flatten(), expr.position()).into())?, ), } @@ -1603,63 +1624,6 @@ impl Engine { } } - // Evaluate an 'in' expression. - fn eval_in_expr( - &self, - scope: &mut Scope, - mods: &mut Imports, - state: &mut State, - lib: &[&Module], - this_ptr: &mut Option<&mut Dynamic>, - lhs: &Expr, - rhs: &Expr, - level: usize, - ) -> RhaiResult { - self.inc_operations(state, rhs.position())?; - - let lhs_value = self.eval_expr(scope, mods, state, lib, this_ptr, lhs, level)?; - let rhs_value = self - .eval_expr(scope, mods, state, lib, this_ptr, rhs, level)? - .flatten(); - - match rhs_value { - #[cfg(not(feature = "no_index"))] - Dynamic(Union::Array(mut rhs_value, _)) => { - // Call the `==` operator to compare each value - let hash = calc_fn_hash(empty(), OP_EQUALS, 2); - for value in rhs_value.iter_mut() { - let args = &mut [&mut lhs_value.clone(), value]; - let pos = rhs.position(); - - if self - .call_native_fn(mods, state, lib, OP_EQUALS, hash, args, false, false, pos)? - .0 - .as_bool() - .unwrap_or(false) - { - return Ok(true.into()); - } - } - - Ok(false.into()) - } - #[cfg(not(feature = "no_object"))] - Dynamic(Union::Map(rhs_value, _)) => match lhs_value { - // Only allows string or char - Dynamic(Union::Str(s, _)) => Ok(rhs_value.contains_key(&s).into()), - Dynamic(Union::Char(c, _)) => Ok(rhs_value.contains_key(&c.to_string()).into()), - _ => EvalAltResult::ErrorInExpr(lhs.position()).into(), - }, - Dynamic(Union::Str(rhs_value, _)) => match lhs_value { - // Only allows string or char - Dynamic(Union::Str(s, _)) => Ok(rhs_value.contains(s.as_str()).into()), - Dynamic(Union::Char(c, _)) => Ok(rhs_value.contains(c).into()), - _ => EvalAltResult::ErrorInExpr(lhs.position()).into(), - }, - _ => EvalAltResult::ErrorInExpr(rhs.position()).into(), - } - } - /// Evaluate an expression. pub(crate) fn eval_expr( &self, @@ -1691,16 +1655,11 @@ impl Engine { .map(|(val, _)| val.take_or_clone()), // Statement block - Expr::Stmt(x, _) => self.eval_stmt_block( - scope, - mods, - state, - lib, - this_ptr, - x.as_ref().as_ref(), - true, - level, - ), + Expr::Stmt(x) if x.is_empty() => Ok(Dynamic::UNIT), + Expr::Stmt(x) => { + let statements = &x.statements; + self.eval_stmt_block(scope, mods, state, lib, this_ptr, statements, true, level) + } // lhs[idx_expr] #[cfg(not(feature = "no_index"))] @@ -1745,13 +1704,13 @@ impl Engine { Expr::FnCall(x, pos) if x.namespace.is_none() => { let FnCallExpr { name, - capture: cap_scope, + capture, hash, args, .. } = x.as_ref(); self.make_function_call( - scope, mods, state, lib, this_ptr, name, args, *hash, *pos, *cap_scope, level, + scope, mods, state, lib, this_ptr, name, args, *hash, *pos, *capture, level, ) } @@ -1771,10 +1730,6 @@ impl Engine { ) } - 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)? @@ -1844,6 +1799,10 @@ impl Engine { restore_prev_state: bool, level: usize, ) -> RhaiResult { + if statements.is_empty() { + return Ok(Dynamic::UNIT); + } + let mut _extra_fn_resolution_cache = false; let prev_always_search = state.always_search; let prev_scope_len = scope.len(); @@ -2065,23 +2024,43 @@ impl Engine { } // Block scope + Stmt::Block(statements, _) if statements.is_empty() => Ok(Dynamic::UNIT), Stmt::Block(statements, _) => { self.eval_stmt_block(scope, mods, state, lib, this_ptr, statements, true, level) } // If statement Stmt::If(expr, x, _) => { - let (if_block, else_block) = x.as_ref(); + let ( + StmtBlock { + statements: if_stmt, + .. + }, + StmtBlock { + statements: else_stmt, + .. + }, + ) = 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())) .and_then(|guard_val| { if guard_val { - self.eval_stmt(scope, mods, state, lib, this_ptr, if_block, level) - } else if let Some(stmt) = else_block { - self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level) + if !if_stmt.is_empty() { + self.eval_stmt_block( + scope, mods, state, lib, this_ptr, if_stmt, true, level, + ) + } else { + Ok(Dynamic::UNIT) + } } else { - Ok(Dynamic::UNIT) + if !else_stmt.is_empty() { + self.eval_stmt_block( + scope, mods, state, lib, this_ptr, else_stmt, true, level, + ) + } else { + Ok(Dynamic::UNIT) + } } }) } @@ -2097,36 +2076,55 @@ impl Engine { value.hash(hasher); let hash = hasher.finish(); - table - .get(&hash) - .map(|stmt| self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level)) + table.get(&hash).map(|StmtBlock { statements, .. }| { + if !statements.is_empty() { + self.eval_stmt_block( + scope, mods, state, lib, this_ptr, statements, true, level, + ) + } else { + Ok(Dynamic::UNIT) + } + }) } else { // Non-hashable values never match any specific clause None } .unwrap_or_else(|| { // Default match clause - def_stmt.as_ref().map_or_else( - || Ok(Dynamic::UNIT), - |def_stmt| { - self.eval_stmt(scope, mods, state, lib, this_ptr, def_stmt, level) - }, - ) + let def_stmt = &def_stmt.statements; + if !def_stmt.is_empty() { + self.eval_stmt_block( + scope, mods, state, lib, this_ptr, def_stmt, true, level, + ) + } else { + Ok(Dynamic::UNIT) + } }) } // While loop - Stmt::While(expr, body, _) => loop { - let condition = if let Some(expr) = expr { - self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)? - .as_bool() - .map_err(|err| self.make_type_mismatch_err::(err, expr.position()))? - } else { - true - }; + Stmt::While(expr, body, _) => { + let body = &body.statements; + loop { + let condition = if !expr.is_unit() { + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)? + .as_bool() + .map_err(|err| { + self.make_type_mismatch_err::(err, expr.position()) + })? + } else { + true + }; - if condition { - match self.eval_stmt(scope, mods, state, lib, this_ptr, body, level) { + if !condition { + return Ok(Dynamic::UNIT); + } + if body.is_empty() { + continue; + } + + match self.eval_stmt_block(scope, mods, state, lib, this_ptr, body, true, level) + { Ok(_) => (), Err(err) => match *err { EvalAltResult::LoopBreak(false, _) => (), @@ -2134,40 +2132,46 @@ impl Engine { _ => return Err(err), }, } - } else { - return Ok(Dynamic::UNIT); } - }, + } // Do loop - Stmt::Do(body, expr, is_while, _) => loop { - match self.eval_stmt(scope, mods, state, lib, this_ptr, body, level) { - Ok(_) => (), - Err(err) => match *err { - EvalAltResult::LoopBreak(false, _) => continue, - EvalAltResult::LoopBreak(true, _) => return Ok(Dynamic::UNIT), - _ => return Err(err), - }, - } + Stmt::Do(body, expr, is_while, _) => { + let body = &body.statements; - if self - .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? - .as_bool() - .map_err(|err| self.make_type_mismatch_err::(err, expr.position()))? - { - if !*is_while { - return Ok(Dynamic::UNIT); + loop { + if !body.is_empty() { + match self + .eval_stmt_block(scope, mods, state, lib, this_ptr, body, true, level) + { + Ok(_) => (), + Err(err) => match *err { + EvalAltResult::LoopBreak(false, _) => continue, + EvalAltResult::LoopBreak(true, _) => return Ok(Dynamic::UNIT), + _ => return Err(err), + }, + } } - } else { - if *is_while { - return Ok(Dynamic::UNIT); + + if self + .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? + .as_bool() + .map_err(|err| self.make_type_mismatch_err::(err, expr.position()))? + { + if !*is_while { + return Ok(Dynamic::UNIT); + } + } else { + if *is_while { + return Ok(Dynamic::UNIT); + } } } - }, + } // For loop Stmt::For(expr, x, _) => { - let (name, stmt) = x.as_ref(); + let (name, StmtBlock { statements, pos }) = x.as_ref(); let iter_obj = self .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? .flatten(); @@ -2216,9 +2220,15 @@ impl Engine { *loop_var = value; } - self.inc_operations(state, stmt.position())?; + self.inc_operations(state, *pos)?; - match self.eval_stmt(scope, mods, state, lib, this_ptr, stmt, level) { + if statements.is_empty() { + continue; + } + + match self.eval_stmt_block( + scope, mods, state, lib, this_ptr, statements, true, level, + ) { Ok(_) => (), Err(err) => match *err { EvalAltResult::LoopBreak(false, _) => (), @@ -2244,10 +2254,20 @@ impl Engine { // Try/Catch statement Stmt::TryCatch(x, _, _) => { - let (try_body, err_var, catch_body) = x.as_ref(); + let ( + StmtBlock { + statements: try_body, + .. + }, + err_var, + StmtBlock { + statements: catch_body, + .. + }, + ) = x.as_ref(); let result = self - .eval_stmt(scope, mods, state, lib, this_ptr, try_body, level) + .eval_stmt_block(scope, mods, state, lib, this_ptr, try_body, true, level) .map(|_| Dynamic::UNIT); match result { @@ -2306,8 +2326,9 @@ impl Engine { scope.push(unsafe_cast_var_name_to_lifetime(&name), err_value); } - let result = - self.eval_stmt(scope, mods, state, lib, this_ptr, catch_body, level); + let result = self.eval_stmt_block( + scope, mods, state, lib, this_ptr, catch_body, true, level, + ); state.scope_level -= 1; scope.rewind(orig_scope_len); @@ -2328,7 +2349,7 @@ impl Engine { } // Return value - Stmt::Return((ReturnType::Return, pos), Some(expr), _) => { + Stmt::Return(ReturnType::Return, Some(expr), pos) => { let value = self .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? .flatten(); @@ -2336,12 +2357,12 @@ impl Engine { } // Empty return - Stmt::Return((ReturnType::Return, pos), None, _) => { + Stmt::Return(ReturnType::Return, None, pos) => { EvalAltResult::Return(Default::default(), *pos).into() } // Throw value - Stmt::Return((ReturnType::Exception, pos), Some(expr), _) => { + Stmt::Return(ReturnType::Exception, Some(expr), pos) => { let value = self .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? .flatten(); @@ -2349,38 +2370,34 @@ impl Engine { } // Empty throw - Stmt::Return((ReturnType::Exception, pos), None, _) => { + Stmt::Return(ReturnType::Exception, None, pos) => { EvalAltResult::ErrorRuntime(Dynamic::UNIT, *pos).into() } // Let/const statement - Stmt::Let(var_def, expr, export, _) | Stmt::Const(var_def, expr, export, _) => { + Stmt::Let(expr, Ident { name, .. }, export, _) + | Stmt::Const(expr, Ident { name, .. }, export, _) => { let entry_type = match stmt { Stmt::Let(_, _, _, _) => AccessMode::ReadWrite, Stmt::Const(_, _, _, _) => AccessMode::ReadOnly, _ => unreachable!("should be Stmt::Let or Stmt::Const, but gets {:?}", stmt), }; - let value = if let Some(expr) = expr { - self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)? - .flatten() - } else { - Dynamic::UNIT - }; + let value = self + .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? + .flatten(); + let (var_name, _alias): (Cow<'_, str>, _) = if state.is_global() { ( - var_def.name.to_string().into(), - if *export { - Some(var_def.name.clone()) - } else { - None - }, + name.to_string().into(), + if *export { Some(name.clone()) } else { None }, ) } else if *export { unreachable!("exported variable not on global level"); } else { - (unsafe_cast_var_name_to_lifetime(&var_def.name).into(), None) + (unsafe_cast_var_name_to_lifetime(name).into(), None) }; + scope.push_dynamic_value(var_name, entry_type, value); #[cfg(not(feature = "no_module"))] @@ -2392,7 +2409,7 @@ impl Engine { // Import statement #[cfg(not(feature = "no_module"))] - Stmt::Import(expr, alias, _pos) => { + Stmt::Import(expr, export, _pos) => { // Guard against too many modules #[cfg(not(feature = "unchecked"))] if state.modules >= self.max_modules() { @@ -2419,14 +2436,14 @@ impl Engine { }) .unwrap_or_else(|| self.module_resolver.resolve(self, &path, expr_pos))?; - if let Some(name_def) = alias { + if let Some(name) = export.as_ref().map(|x| x.name.clone()) { if !module.is_indexed() { // Index the module (making a clone copy if necessary) if it is not indexed let mut module = crate::fn_native::shared_take_or_clone(module); module.build_index(); - mods.push(name_def.name.clone(), module); + mods.push(name, module); } else { - mods.push(name_def.name.clone(), module); + mods.push(name, module); } } @@ -2441,14 +2458,13 @@ impl Engine { // Export statement #[cfg(not(feature = "no_module"))] Stmt::Export(list, _) => { - for (Ident { name, pos: id_pos }, rename) in list.iter() { + for (Ident { name, pos, .. }, rename) in list.iter() { // Mark scope variables as public if let Some(index) = scope.get_index(name).map(|(i, _)| i) { let alias = rename.as_ref().map(|x| &x.name).unwrap_or_else(|| name); scope.add_entry_alias(index, alias.clone()); } else { - return EvalAltResult::ErrorVariableNotFound(name.to_string(), *id_pos) - .into(); + return EvalAltResult::ErrorVariableNotFound(name.to_string(), *pos).into(); } } Ok(Dynamic::UNIT) @@ -2482,6 +2498,7 @@ impl Engine { /// Check a result to ensure that the data size is within allowable limit. #[cfg(not(feature = "unchecked"))] + #[inline(always)] fn check_data_size(&self, result: RhaiResult, pos: Position) -> RhaiResult { // Simply return all errors if result.is_err() { diff --git a/src/engine_api.rs b/src/engine_api.rs index 9ac38a97..7218582d 100644 --- a/src/engine_api.rs +++ b/src/engine_api.rs @@ -926,8 +926,9 @@ impl Engine { if !resolver.contains_path(s) && !imports.contains(s) => { imports.insert(s.clone()); + true } - _ => (), + _ => true, }); } @@ -1832,7 +1833,7 @@ impl Engine { let lib = Default::default(); let stmt = crate::stdlib::mem::take(ast.statements_mut()); - crate::optimize::optimize_into_ast(self, scope, stmt, lib, optimization_level) + crate::optimize::optimize_into_ast(self, scope, stmt.into_vec(), lib, optimization_level) } /// Generate a list of all registered functions. /// diff --git a/src/fn_builtin.rs b/src/fn_builtin.rs index 8a1d96d8..c592bb0e 100644 --- a/src/fn_builtin.rs +++ b/src/fn_builtin.rs @@ -1,5 +1,6 @@ //! Built-in implementations for common operators. +use crate::engine::OP_CONTAINS; use crate::fn_native::{FnCallArgs, NativeCallContext}; use crate::stdlib::{any::TypeId, format, string::ToString}; use crate::{Dynamic, ImmutableString, RhaiResult, INT}; @@ -77,6 +78,13 @@ pub fn get_builtin_binary_op_fn( Ok((x $op y).into()) }) }; + ($xx:ident . $func:ident ( $yy:ty )) => { + return Some(|_, args| { + let x = &*args[0].read_lock::<$xx>().unwrap(); + let y = &*args[1].read_lock::<$yy>().unwrap(); + Ok(x.$func(y).into()) + }) + }; ($func:ident ( $op:tt )) => { return Some(|_, args| { let (x, y) = $func(args); @@ -259,6 +267,24 @@ pub fn get_builtin_binary_op_fn( ">=" => impl_op!(get_s1s2(>=)), "<" => impl_op!(get_s1s2(<)), "<=" => impl_op!(get_s1s2(<=)), + OP_CONTAINS => { + return Some(|_, args| { + let s = &*args[0].read_lock::().unwrap(); + let c = args[1].as_char().unwrap(); + Ok((s.contains(c)).into()) + }) + } + _ => return None, + } + } + + // map op string + #[cfg(not(feature = "no_object"))] + if types_pair == (TypeId::of::(), TypeId::of::()) { + use crate::Map; + + match op { + OP_CONTAINS => impl_op!(Map.contains_key(ImmutableString)), _ => return None, } } @@ -342,6 +368,13 @@ pub fn get_builtin_binary_op_fn( ">=" => impl_op!(ImmutableString >= ImmutableString), "<" => impl_op!(ImmutableString < ImmutableString), "<=" => impl_op!(ImmutableString <= ImmutableString), + OP_CONTAINS => { + return Some(|_, args| { + let s1 = &*args[0].read_lock::().unwrap(); + let s2 = &*args[1].read_lock::().unwrap(); + Ok((s1.contains(s2.as_str())).into()) + }) + } _ => return None, } } diff --git a/src/fn_call.rs b/src/fn_call.rs index 63fe6079..8a008ee1 100644 --- a/src/fn_call.rs +++ b/src/fn_call.rs @@ -493,6 +493,10 @@ impl Engine { self.inc_operations(state, pos)?; + if fn_def.body.is_empty() { + return Ok(Dynamic::UNIT); + } + // Check for stack overflow #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "unchecked"))] @@ -539,10 +543,10 @@ impl Engine { } // Evaluate the function - let stmt = &fn_def.body; + let body = &fn_def.body.statements; let result = self - .eval_stmt(scope, mods, state, unified_lib, this_ptr, stmt, level) + .eval_stmt_block(scope, mods, state, unified_lib, this_ptr, body, true, level) .or_else(|err| match *err { // Convert return statement to return value EvalAltResult::Return(x, _) => Ok(x), @@ -722,6 +726,10 @@ impl Engine { let func = func.get_fn_def(); + if func.body.is_empty() { + return Ok((Dynamic::UNIT, false)); + } + let scope: &mut Scope = &mut Default::default(); // Move captured variables into scope @@ -831,6 +839,7 @@ impl Engine { } /// Evaluate a text script in place - used primarily for 'eval'. + #[inline] fn eval_script_expr_in_place( &self, scope: &mut Scope, @@ -884,7 +893,7 @@ impl Engine { fn_name: &str, mut hash: FnHash, target: &mut crate::engine::Target, - mut call_args: StaticVec, + (call_args, call_arg_positions): &mut (StaticVec, StaticVec), pos: Position, level: usize, ) -> Result<(Dynamic, bool), Box> { @@ -902,7 +911,7 @@ impl Engine { let fn_name = fn_ptr.fn_name(); let args_len = call_args.len() + fn_ptr.curry().len(); // Recalculate hashes - let hash = FnHash::from_script(calc_fn_hash(empty(), fn_name, args_len)); + let new_hash = FnHash::from_script(calc_fn_hash(empty(), fn_name, args_len)); // Arguments are passed as-is, adding the curried arguments let mut curry = fn_ptr.curry().iter().cloned().collect::>(); let mut arg_values = curry @@ -913,17 +922,32 @@ impl Engine { // Map it to name(args) in function-call style self.exec_fn_call( - mods, state, lib, fn_name, hash, args, false, false, pos, None, level, + mods, state, lib, fn_name, new_hash, args, false, false, pos, None, level, ) } - KEYWORD_FN_PTR_CALL if call_args.len() > 0 && call_args[0].is::() => { + KEYWORD_FN_PTR_CALL => { + if call_args.len() > 0 { + if !call_args[0].is::() { + return Err(self.make_type_mismatch_err::( + self.map_type_name(obj.type_name()), + call_arg_positions[0], + )); + } + } else { + return Err(self.make_type_mismatch_err::( + self.map_type_name(obj.type_name()), + pos, + )); + } + // FnPtr call on object let fn_ptr = call_args.remove(0).cast::(); + call_arg_positions.remove(0); // Redirect function name let fn_name = fn_ptr.fn_name(); let args_len = call_args.len() + fn_ptr.curry().len(); // Recalculate hash - let hash = FnHash::from_script_and_native( + let new_hash = FnHash::from_script_and_native( calc_fn_hash(empty(), fn_name, args_len), calc_fn_hash(empty(), fn_name, args_len + 1), ); @@ -937,22 +961,34 @@ impl Engine { // Map it to name(args) in function-call style self.exec_fn_call( - mods, state, lib, fn_name, hash, args, is_ref, true, pos, None, level, + mods, state, lib, fn_name, new_hash, args, is_ref, true, pos, None, level, ) } - KEYWORD_FN_PTR_CURRY if obj.is::() => { - // Curry call + KEYWORD_FN_PTR_CURRY => { + if !obj.is::() { + return Err(self.make_type_mismatch_err::( + self.map_type_name(obj.type_name()), + pos, + )); + } + let fn_ptr = obj.read_lock::().unwrap(); + + // Curry call Ok(( - FnPtr::new_unchecked( - fn_ptr.get_fn_name().clone(), - fn_ptr - .curry() - .iter() - .cloned() - .chain(call_args.into_iter()) - .collect(), - ) + if call_args.is_empty() { + fn_ptr.clone() + } else { + FnPtr::new_unchecked( + fn_ptr.get_fn_name().clone(), + fn_ptr + .curry() + .iter() + .cloned() + .chain(call_args.iter_mut().map(|v| mem::take(v))) + .collect(), + ) + } .into(), false, )) @@ -981,7 +1017,10 @@ impl Engine { .iter() .cloned() .enumerate() - .for_each(|(i, v)| call_args.insert(i, v)); + .for_each(|(i, v)| { + call_args.insert(i, v); + call_arg_positions.insert(i, Position::NONE); + }); // Recalculate the hash based on the new function name and new arguments hash = FnHash::from_script_and_native( calc_fn_hash(empty(), fn_name, call_args.len()), @@ -1065,7 +1104,6 @@ impl Engine { FnHash::from_native(calc_fn_hash(empty(), name, args_len)) }; } - // Handle Fn() KEYWORD_FN_PTR if args_expr.len() == 1 => { // Fn - only in function call style @@ -1361,22 +1399,27 @@ impl Engine { match func { #[cfg(not(feature = "no_function"))] Some(f) if f.is_script() => { - let args = args.as_mut(); - let new_scope = &mut Default::default(); - let fn_def = f.get_fn_def().clone(); + let fn_def = f.get_fn_def(); - let mut source = module.id_raw().cloned(); - mem::swap(&mut state.source, &mut source); + if fn_def.body.is_empty() { + Ok(Dynamic::UNIT) + } else { + let args = args.as_mut(); + let new_scope = &mut Default::default(); - let level = level + 1; + let mut source = module.id_raw().cloned(); + mem::swap(&mut state.source, &mut source); - let result = self.call_script_fn( - new_scope, mods, state, lib, &mut None, &fn_def, args, pos, level, - ); + let level = level + 1; - state.source = source; + let result = self.call_script_fn( + new_scope, mods, state, lib, &mut None, fn_def, args, pos, level, + ); - result + state.source = source; + + result + } } Some(f) if f.is_plugin_fn() => f diff --git a/src/lib.rs b/src/lib.rs index aa95de62..503890d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,7 +124,7 @@ pub type FLOAT = f32; pub use ast::{FnAccess, AST}; pub use dynamic::Dynamic; -pub use engine::{Engine, EvalContext}; +pub use engine::{Engine, EvalContext, OP_CONTAINS, OP_EQUALS}; pub use fn_native::{FnPtr, NativeCallContext}; pub use fn_register::{RegisterFn, RegisterResultFn}; pub use module::{FnNamespace, Module}; @@ -192,7 +192,7 @@ pub use token::{get_next_token, parse_string_literal, InputStream, Token, Tokeni #[deprecated = "this type is volatile and may change"] pub use ast::{ ASTNode, BinaryExpr, CustomExpr, Expr, FloatWrapper, FnCallExpr, FnHash, Ident, OpAssignment, - ReturnType, ScriptFnDef, Stmt, + ReturnType, ScriptFnDef, Stmt, StmtBlock, }; #[cfg(feature = "internals")] diff --git a/src/module/mod.rs b/src/module/mod.rs index c79e2396..2a0c2edc 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -39,6 +39,7 @@ pub enum FnNamespace { } impl Default for FnNamespace { + #[inline(always)] fn default() -> Self { Self::Internal } @@ -150,6 +151,7 @@ pub struct Module { } impl Default for Module { + #[inline(always)] fn default() -> Self { Self { id: None, @@ -226,6 +228,7 @@ impl AsRef for Module { impl> Add for &Module { type Output = Module; + #[inline(always)] fn add(self, rhs: M) -> Self::Output { let mut module = self.clone(); module.merge(rhs.as_ref()); @@ -236,6 +239,7 @@ impl> Add for &Module { impl> Add for Module { type Output = Self; + #[inline(always)] fn add(mut self, rhs: M) -> Self::Output { self.merge(rhs.as_ref()); self @@ -243,6 +247,7 @@ impl> Add for Module { } impl> AddAssign for Module { + #[inline(always)] fn add_assign(&mut self, rhs: M) { self.combine(rhs.into()); } @@ -1953,16 +1958,19 @@ impl Module { } /// Does a type iterator exist in the entire module tree? + #[inline(always)] pub fn contains_qualified_iter(&self, id: TypeId) -> bool { self.all_type_iterators.contains_key(&id) } /// Does a type iterator exist in the module? + #[inline(always)] pub fn contains_iter(&self, id: TypeId) -> bool { self.type_iterators.contains_key(&id) } /// Set a type iterator into the [`Module`]. + #[inline(always)] pub fn set_iter(&mut self, typ: TypeId, func: IteratorFn) -> &mut Self { self.type_iterators.insert(typ, func); self.indexed = false; @@ -1971,6 +1979,7 @@ impl Module { } /// Set a type iterator into the [`Module`]. + #[inline(always)] pub fn set_iterable(&mut self) -> &mut Self where T: Variant + Clone + IntoIterator, @@ -1982,6 +1991,7 @@ impl Module { } /// Set an iterator type into the [`Module`] as a type iterator. + #[inline(always)] pub fn set_iterator(&mut self) -> &mut Self where T: Variant + Clone + Iterator, @@ -1993,11 +2003,13 @@ impl Module { } /// Get the specified type iterator. + #[inline(always)] pub(crate) fn get_qualified_iter(&self, id: TypeId) -> Option { self.all_type_iterators.get(&id).cloned() } /// Get the specified type iterator. + #[inline(always)] pub(crate) fn get_iter(&self, id: TypeId) -> Option { self.type_iterators.get(&id).cloned() } @@ -2021,32 +2033,41 @@ pub struct NamespaceRef { } impl fmt::Debug for NamespaceRef { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self.path, f)?; - if let Some(index) = self.index { - write!(f, " -> {}", index) - } else { - Ok(()) + write!(f, "{} -> ", index)?; } + + f.write_str( + &self + .path + .iter() + .map(|Ident { name, .. }| name.as_str()) + .collect::>() + .join("::"), + ) } } impl Deref for NamespaceRef { type Target = StaticVec; + #[inline(always)] fn deref(&self) -> &Self::Target { &self.path } } impl DerefMut for NamespaceRef { + #[inline(always)] fn deref_mut(&mut self) -> &mut Self::Target { &mut self.path } } impl fmt::Display for NamespaceRef { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for Ident { name, .. } in self.path.iter() { write!(f, "{}{}", name, Token::DoubleColon.syntax())?; @@ -2056,6 +2077,7 @@ impl fmt::Display for NamespaceRef { } impl From> for NamespaceRef { + #[inline(always)] fn from(path: StaticVec) -> Self { Self { index: None, path } } @@ -2063,11 +2085,13 @@ impl From> for NamespaceRef { impl NamespaceRef { /// Get the [`Scope`][crate::Scope] index offset. + #[inline(always)] pub(crate) fn index(&self) -> Option { self.index } /// Set the [`Scope`][crate::Scope] index offset. #[cfg(not(feature = "no_module"))] + #[inline(always)] pub(crate) fn set_index(&mut self, index: Option) { self.index = index } diff --git a/src/module/resolvers/file.rs b/src/module/resolvers/file.rs index 5d71e0a2..e3a86634 100644 --- a/src/module/resolvers/file.rs +++ b/src/module/resolvers/file.rs @@ -41,6 +41,7 @@ use crate::{Engine, EvalAltResult, Module, ModuleResolver, Position, Shared}; pub struct FileModuleResolver { base_path: PathBuf, extension: String, + cache_enabled: bool, #[cfg(not(feature = "sync"))] cache: crate::stdlib::cell::RefCell>>, @@ -101,6 +102,7 @@ impl FileModuleResolver { Self { base_path: path.into(), extension: extension.into(), + cache_enabled: true, cache: Default::default(), } } @@ -152,9 +154,25 @@ impl FileModuleResolver { self } + /// Enable/disable the cache. + #[inline(always)] + pub fn enable_cache(&mut self, enable: bool) -> &mut Self { + self.cache_enabled = enable; + self + } + /// Is the cache enabled? + #[inline(always)] + pub fn is_cache_enabled(&self) -> bool { + self.cache_enabled + } + /// Is a particular path cached? #[inline(always)] pub fn is_cached(&self, path: &str) -> bool { + if !self.cache_enabled { + return false; + } + let file_path = self.get_file_path(path); #[cfg(not(feature = "sync"))] @@ -211,7 +229,7 @@ impl ModuleResolver for FileModuleResolver { let file_path = self.get_file_path(path); // See if it is cached - { + if self.is_cache_enabled() { #[cfg(not(feature = "sync"))] let c = self.cache.borrow(); #[cfg(feature = "sync")] @@ -242,10 +260,12 @@ impl ModuleResolver for FileModuleResolver { .into(); // Put it into the cache - #[cfg(not(feature = "sync"))] - self.cache.borrow_mut().insert(file_path, m.clone()); - #[cfg(feature = "sync")] - self.cache.write().unwrap().insert(file_path, m.clone()); + if self.is_cache_enabled() { + #[cfg(not(feature = "sync"))] + self.cache.borrow_mut().insert(file_path, m.clone()); + #[cfg(feature = "sync")] + self.cache.write().unwrap().insert(file_path, m.clone()); + } Ok(m) } diff --git a/src/optimize.rs b/src/optimize.rs index 20456ad2..06a06eaf 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -1,6 +1,6 @@ //! Module implementing the [`AST`] optimizer. -use crate::ast::{Expr, Stmt}; +use crate::ast::{Expr, Ident, Stmt, StmtBlock}; use crate::dynamic::AccessMode; use crate::engine::{KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_PRINT, KEYWORD_TYPE_OF}; use crate::fn_builtin::get_builtin_binary_op_fn; @@ -15,7 +15,6 @@ use crate::stdlib::{ vec, vec::Vec, }; -use crate::token::is_valid_identifier; use crate::utils::get_hasher; use crate::{ calc_fn_hash, calc_fn_params_hash, combine_hashes, Dynamic, Engine, Module, Position, Scope, @@ -75,14 +74,18 @@ struct State<'a> { impl<'a> State<'a> { /// Create a new State. #[inline(always)] - pub fn new(engine: &'a Engine, lib: &'a [&'a Module], level: OptimizationLevel) -> Self { + pub fn new( + engine: &'a Engine, + lib: &'a [&'a Module], + optimization_level: OptimizationLevel, + ) -> Self { Self { changed: false, variables: vec![], propagate_constants: true, engine, lib, - optimization_level: level, + optimization_level, } } /// Reset the state from dirty to clean. @@ -95,6 +98,11 @@ impl<'a> State<'a> { pub fn set_dirty(&mut self) { self.changed = true; } + /// Set the [`AST`] state to be not dirty (i.e. unchanged). + #[inline(always)] + pub fn clear_dirty(&mut self) { + self.changed = false; + } /// Is the [`AST`] dirty (i.e. changed)? #[inline(always)] pub fn is_dirty(&self) -> bool { @@ -169,139 +177,112 @@ fn call_fn_with_constant_arguments( /// Optimize a block of [statements][Stmt]. fn optimize_stmt_block( mut statements: Vec, - pos: Position, state: &mut State, preserve_result: bool, - count_promote_as_dirty: bool, -) -> Stmt { - let orig_len = statements.len(); // Original number of statements in the block, for change detection - let orig_constants_len = state.variables.len(); // Original number of constants in the state, for restore later - let orig_propagate_constants = state.propagate_constants; +) -> Vec { + if statements.is_empty() { + return statements; + } - // Optimize each statement in the block - statements.iter_mut().for_each(|stmt| { - match stmt { - // Add constant literals into the state - Stmt::Const(var_def, Some(value_expr), _, _) => { - optimize_expr(value_expr, state); + let mut is_dirty = state.is_dirty(); - if value_expr.is_constant() { - state.push_var(&var_def.name, AccessMode::ReadOnly, value_expr.clone()); - } + loop { + state.clear_dirty(); + + let orig_constants_len = state.variables.len(); // Original number of constants in the state, for restore later + let orig_propagate_constants = state.propagate_constants; + + // Remove everything following control flow breaking statements + let mut dead_code = false; + + statements.retain(|stmt| { + if dead_code { + state.set_dirty(); + false + } else if stmt.is_control_flow_break() { + dead_code = true; + true + } else { + true } - Stmt::Const(var_def, None, _, _) => { - state.push_var(&var_def.name, AccessMode::ReadOnly, Expr::Unit(var_def.pos)); - } - // Add variables into the state - Stmt::Let(var_def, expr, _, _) => { - if let Some(value_expr) = expr { + }); + + // Optimize each statement in the block + statements.iter_mut().for_each(|stmt| { + match stmt { + // Add constant literals into the state + Stmt::Const(value_expr, Ident { name, .. }, _, _) => { optimize_expr(value_expr, state); + + if value_expr.is_constant() { + state.push_var(name, AccessMode::ReadOnly, value_expr.clone()); + } } - - state.push_var( - &var_def.name, - AccessMode::ReadWrite, - Expr::Unit(var_def.pos), - ); + // Add variables into the state + Stmt::Let(value_expr, Ident { name, pos, .. }, _, _) => { + optimize_expr(value_expr, state); + state.push_var(name, AccessMode::ReadWrite, Expr::Unit(*pos)); + } + // Optimize the statement + _ => optimize_stmt(stmt, state, preserve_result), } - // Optimize the statement - _ => optimize_stmt(stmt, state, preserve_result), - } - }); + }); - // Remove all raw expression statements that are pure except for the very last statement - let last_stmt = if preserve_result { - statements.pop() - } else { - None - }; - - statements.retain(|stmt| !stmt.is_pure()); - - if let Some(stmt) = last_stmt { - statements.push(stmt); - } - - // Remove all let/import statements at the end of a block - the new variables will go away anyway. - // But be careful only remove ones that have no initial values or have values that are pure expressions, - // otherwise there may be side effects. - let mut removed = false; - - while let Some(expr) = statements.pop() { - match expr { - Stmt::Let(_, expr, _, _) | Stmt::Const(_, expr, _, _) => { - removed = expr.as_ref().map(Expr::is_pure).unwrap_or(true) + // Remove all pure statements that do not return values at the end of a block. + // We cannot remove anything for non-pure statements due to potential side-effects. + if preserve_result { + loop { + match &statements[..] { + [stmt] if !stmt.returns_value() && stmt.is_internally_pure() => { + state.set_dirty(); + statements.clear(); + } + [.., second_last_stmt, Stmt::Noop(_)] if second_last_stmt.returns_value() => {} + [.., second_last_stmt, last_stmt] + if !last_stmt.returns_value() && last_stmt.is_internally_pure() => + { + state.set_dirty(); + if second_last_stmt.returns_value() { + *statements.last_mut().unwrap() = Stmt::Noop(last_stmt.position()); + } else { + statements.pop().unwrap(); + } + } + _ => break, + } } - #[cfg(not(feature = "no_module"))] - Stmt::Import(expr, _, _) => removed = expr.is_pure(), - _ => { - statements.push(expr); - break; + } else { + loop { + match &statements[..] { + [stmt] if stmt.is_internally_pure() => { + state.set_dirty(); + statements.clear(); + } + [.., last_stmt] if last_stmt.is_internally_pure() => { + state.set_dirty(); + statements.pop().unwrap(); + } + _ => break, + } } } + + // Pop the stack and remove all the local constants + state.restore_var(orig_constants_len); + state.propagate_constants = orig_propagate_constants; + + if !state.is_dirty() { + break; + } + + is_dirty = true; } - if preserve_result { - if removed { - statements.push(Stmt::Noop(pos)) - } - - // Optimize all the statements again - let num_statements = statements.len(); - statements - .iter_mut() - .enumerate() - .for_each(|(i, stmt)| optimize_stmt(stmt, state, i == num_statements)); - } - - // Remove everything following the the first return/throw - let mut dead_code = false; - - statements.retain(|stmt| { - if dead_code { - return false; - } - - match stmt { - Stmt::Return(_, _, _) | Stmt::Break(_) => dead_code = true, - _ => (), - } - - true - }); - - // Change detection - if orig_len != statements.len() { + if is_dirty { state.set_dirty(); } - // Pop the stack and remove all the local constants - state.restore_var(orig_constants_len); - - state.propagate_constants = orig_propagate_constants; - - match &statements[..] { - // No statements in block - change to No-op - [] => { - state.set_dirty(); - Stmt::Noop(pos) - } - // Only one let statement - leave it alone - [x] if matches!(x, Stmt::Let(_, _, _, _)) => Stmt::Block(statements, pos), - // Only one const statement - leave it alone - [x] if matches!(x, Stmt::Const(_, _, _, _)) => Stmt::Block(statements, pos), - // Only one import statement - leave it alone - #[cfg(not(feature = "no_module"))] - [x] if matches!(x, Stmt::Import(_, _, _)) => Stmt::Block(statements, pos), - // Only one statement - promote - [_] => { - if count_promote_as_dirty { - state.set_dirty(); - } - statements.remove(0) - } - _ => Stmt::Block(statements, pos), - } + statements } /// Optimize a [statement][Stmt]. @@ -316,18 +297,8 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { } }, - // if false { if_block } -> Noop - Stmt::If(Expr::BoolConstant(false, pos), x, _) if x.1.is_none() => { - state.set_dirty(); - *stmt = Stmt::Noop(*pos); - } - // if true { if_block } -> if_block - Stmt::If(Expr::BoolConstant(true, _), x, _) if x.1.is_none() => { - *stmt = mem::take(&mut x.0); - optimize_stmt(stmt, state, true); - } - // if expr { Noop } - Stmt::If(condition, x, _) if x.1.is_none() && matches!(x.0, Stmt::Noop(_)) => { + // if expr {} + Stmt::If(condition, x, _) if x.0.is_empty() && x.1.is_empty() => { state.set_dirty(); let pos = condition.position(); @@ -336,38 +307,56 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { *stmt = if preserve_result { // -> { expr, Noop } - Stmt::Block(vec![Stmt::Expr(expr), mem::take(&mut x.0)], pos) + Stmt::Block(vec![Stmt::Expr(expr), Stmt::Noop(pos)], pos) } else { // -> expr Stmt::Expr(expr) }; } - // if expr { if_block } - Stmt::If(condition, x, _) if x.1.is_none() => { - optimize_expr(condition, state); - optimize_stmt(&mut x.0, state, true); + // if false { if_block } -> Noop + Stmt::If(Expr::BoolConstant(false, pos), x, _) if x.1.is_empty() => { + state.set_dirty(); + *stmt = Stmt::Noop(*pos); } // if false { if_block } else { else_block } -> else_block - Stmt::If(Expr::BoolConstant(false, _), x, _) if x.1.is_some() => { - *stmt = mem::take(x.1.as_mut().unwrap()); - optimize_stmt(stmt, state, true); + Stmt::If(Expr::BoolConstant(false, _), x, _) => { + state.set_dirty(); + *stmt = match optimize_stmt_block( + mem::take(&mut x.1.statements).into_vec(), + state, + preserve_result, + ) { + statements if statements.is_empty() => Stmt::Noop(x.1.pos), + statements => Stmt::Block(statements, x.1.pos), + } } // if true { if_block } else { else_block } -> if_block Stmt::If(Expr::BoolConstant(true, _), x, _) => { - *stmt = mem::take(&mut x.0); - optimize_stmt(stmt, state, true); + state.set_dirty(); + *stmt = match optimize_stmt_block( + mem::take(&mut x.0.statements).into_vec(), + state, + preserve_result, + ) { + statements if statements.is_empty() => Stmt::Noop(x.0.pos), + statements => Stmt::Block(statements, x.0.pos), + } } // if expr { if_block } else { else_block } Stmt::If(condition, x, _) => { optimize_expr(condition, state); - optimize_stmt(&mut x.0, state, true); - if let Some(else_block) = x.1.as_mut() { - optimize_stmt(else_block, state, true); - match else_block { - Stmt::Noop(_) => x.1 = None, // Noop -> no else block - _ => (), - } - } + x.0.statements = optimize_stmt_block( + mem::take(&mut x.0.statements).into_vec(), + state, + preserve_result, + ) + .into(); + x.1.statements = optimize_stmt_block( + mem::take(&mut x.1.statements).into_vec(), + state, + preserve_result, + ) + .into(); } // switch const { ... } @@ -381,117 +370,144 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut State, preserve_result: bool) { let table = &mut x.0; - if let Some(stmt) = table.get_mut(&hash) { - optimize_stmt(stmt, state, true); - *expr = Expr::Stmt(Box::new(vec![mem::take(stmt)].into()), *pos); - } else if let Some(def_stmt) = x.1.as_mut() { - optimize_stmt(def_stmt, state, true); - *expr = Expr::Stmt(Box::new(vec![mem::take(def_stmt)].into()), *pos); + let (statements, new_pos) = if let Some(block) = table.get_mut(&hash) { + ( + optimize_stmt_block(mem::take(&mut block.statements).into_vec(), state, true) + .into(), + block.pos, + ) } else { - *expr = Expr::Unit(*pos); - } + ( + optimize_stmt_block(mem::take(&mut x.1.statements).into_vec(), state, true) + .into(), + if x.1.pos.is_none() { *pos } else { x.1.pos }, + ) + }; + + *expr = Expr::Stmt(Box::new(StmtBlock { + statements, + pos: new_pos, + })); } // switch Stmt::Switch(expr, x, _) => { optimize_expr(expr, state); - x.0.values_mut() - .for_each(|stmt| optimize_stmt(stmt, state, true)); - if let Some(def_stmt) = x.1.as_mut() { - optimize_stmt(def_stmt, state, true); - - match def_stmt { - Stmt::Noop(_) | Stmt::Expr(Expr::Unit(_)) => x.1 = None, - _ => (), - } - } + x.0.values_mut().for_each(|block| { + block.statements = optimize_stmt_block( + mem::take(&mut block.statements).into_vec(), + state, + preserve_result, + ) + .into() + }); + x.1.statements = optimize_stmt_block( + mem::take(&mut x.1.statements).into_vec(), + state, + preserve_result, + ) + .into() } // while false { block } -> Noop - Stmt::While(Some(Expr::BoolConstant(false, pos)), _, _) => { + Stmt::While(Expr::BoolConstant(false, pos), _, _) => { state.set_dirty(); *stmt = Stmt::Noop(*pos) } // while expr { block } Stmt::While(condition, block, _) => { - optimize_stmt(block, state, false); + optimize_expr(condition, state); - if let Some(condition) = condition { - optimize_expr(condition, state); - } + block.statements = + optimize_stmt_block(mem::take(&mut block.statements).into_vec(), state, false) + .into(); - match **block { - // while expr { break; } -> { expr; } - Stmt::Break(pos) => { - // Only a single break statement - turn into running the guard expression once - state.set_dirty(); - if let Some(condition) = condition { - let mut statements = vec![Stmt::Expr(mem::take(condition))]; - if preserve_result { - statements.push(Stmt::Noop(pos)) - } - *stmt = Stmt::Block(statements, pos); - } else { - *stmt = Stmt::Noop(pos); - }; + if block.len() == 1 { + match block.statements[0] { + // while expr { break; } -> { expr; } + Stmt::Break(pos) => { + // Only a single break statement - turn into running the guard expression once + state.set_dirty(); + if !condition.is_unit() { + let mut statements = vec![Stmt::Expr(mem::take(condition))]; + if preserve_result { + statements.push(Stmt::Noop(pos)) + } + *stmt = Stmt::Block(statements, pos); + } else { + *stmt = Stmt::Noop(pos); + }; + } + _ => (), } - _ => (), } } // do { block } while false | do { block } until true -> { block } Stmt::Do(block, Expr::BoolConstant(true, _), false, _) | Stmt::Do(block, Expr::BoolConstant(false, _), true, _) => { state.set_dirty(); - optimize_stmt(block.as_mut(), state, false); - *stmt = mem::take(block.as_mut()); + *stmt = Stmt::Block( + optimize_stmt_block(mem::take(&mut block.statements).into_vec(), state, false), + block.pos, + ); } // do { block } while|until expr Stmt::Do(block, condition, _, _) => { - optimize_stmt(block.as_mut(), state, false); optimize_expr(condition, state); + block.statements = + optimize_stmt_block(mem::take(&mut block.statements).into_vec(), state, false) + .into(); } // for id in expr { block } Stmt::For(iterable, x, _) => { optimize_expr(iterable, state); - optimize_stmt(&mut x.1, state, false); + x.1.statements = + optimize_stmt_block(mem::take(&mut x.1.statements).into_vec(), state, false).into(); } // let id = expr; - Stmt::Let(_, Some(expr), _, _) => optimize_expr(expr, state), - // let id; - Stmt::Let(_, None, _, _) => (), + Stmt::Let(expr, _, _, _) => optimize_expr(expr, state), // import expr as var; #[cfg(not(feature = "no_module"))] Stmt::Import(expr, _, _) => optimize_expr(expr, state), // { block } Stmt::Block(statements, pos) => { - *stmt = optimize_stmt_block(mem::take(statements), *pos, state, preserve_result, true); + *stmt = match optimize_stmt_block(mem::take(statements), state, preserve_result) { + statements if statements.is_empty() => { + state.set_dirty(); + Stmt::Noop(*pos) + } + // Only one statement - promote + mut statements if statements.len() == 1 => { + state.set_dirty(); + statements.pop().unwrap() + } + statements => Stmt::Block(statements, *pos), + }; } - // try { block } catch ( var ) { block } - Stmt::TryCatch(x, _, _) if x.0.is_pure() => { + // try { pure block } catch ( var ) { block } + Stmt::TryCatch(x, _, _) if x.0.statements.iter().all(Stmt::is_pure) => { // If try block is pure, there will never be any exceptions state.set_dirty(); - let pos = x.0.position(); - optimize_stmt(&mut x.0, state, preserve_result); - let mut statements = match mem::take(&mut x.0) { - Stmt::Block(statements, _) => statements, - stmt => vec![stmt], - }; - statements.push(Stmt::Noop(pos)); - *stmt = Stmt::Block(statements, pos); + *stmt = Stmt::Block( + optimize_stmt_block(mem::take(&mut x.0.statements).into_vec(), state, false), + x.0.pos, + ); } // try { block } catch ( var ) { block } Stmt::TryCatch(x, _, _) => { - optimize_stmt(&mut x.0, state, false); - optimize_stmt(&mut x.2, state, false); + x.0.statements = + optimize_stmt_block(mem::take(&mut x.0.statements).into_vec(), state, false).into(); + x.2.statements = + optimize_stmt_block(mem::take(&mut x.2.statements).into_vec(), state, false).into(); } // {} - Stmt::Expr(Expr::Stmt(x, pos)) if x.is_empty() => { + Stmt::Expr(Expr::Stmt(x)) if x.statements.is_empty() => { state.set_dirty(); - *stmt = Stmt::Noop(*pos); + *stmt = Stmt::Noop(x.pos); } // {...}; - Stmt::Expr(Expr::Stmt(x, pos)) => { + Stmt::Expr(Expr::Stmt(x)) => { state.set_dirty(); - *stmt = Stmt::Block(mem::take(x).into_vec(), *pos); + *stmt = Stmt::Block(mem::take(&mut x.statements).into_vec(), x.pos); } // expr; Stmt::Expr(expr) => optimize_expr(expr, state), @@ -514,25 +530,15 @@ fn optimize_expr(expr: &mut Expr, state: &mut State) { match expr { // {} - Expr::Stmt(x, pos) if x.is_empty() => { state.set_dirty(); *expr = Expr::Unit(*pos) } + Expr::Stmt(x) if x.statements.is_empty() => { state.set_dirty(); *expr = Expr::Unit(x.pos) } // { stmt; ... } - do not count promotion as dirty because it gets turned back into an array - Expr::Stmt(x, pos) => match optimize_stmt_block(mem::take(x).into_vec(), *pos, state, true, false) { - // {} - Stmt::Noop(_) => { state.set_dirty(); *expr = Expr::Unit(*pos); } - // { stmt, .. } - Stmt::Block(statements, _) => *x = Box::new(statements.into()), - // { expr } - Stmt::Expr(inner) => { state.set_dirty(); *expr = inner; } - // { stmt } - stmt => x.push(stmt), - } - + Expr::Stmt(x) => x.statements = optimize_stmt_block(mem::take(&mut x.statements).into_vec(), state, true).into(), // lhs.rhs #[cfg(not(feature = "no_object"))] Expr::Dot(x, _) => match (&mut x.lhs, &mut x.rhs) { // map.string (Expr::Map(m, pos), Expr::Property(p)) if m.iter().all(|(_, x)| x.is_pure()) => { - let prop = &p.4.name; + let prop = &p.2.name; // Map literal where everything is pure - promote the indexed item. // All other items can be thrown away. state.set_dirty(); @@ -598,32 +604,6 @@ fn optimize_expr(expr: &mut Expr, state: &mut State) { // #{ key:value, .. } #[cfg(not(feature = "no_object"))] Expr::Map(x, _) => x.iter_mut().for_each(|(_, expr)| optimize_expr(expr, state)), - // lhs in rhs - Expr::In(x, _) => match (&mut x.lhs, &mut x.rhs) { - // "xxx" in "xxxxx" - (Expr::StringConstant(a, pos), Expr::StringConstant(b, _)) => { - state.set_dirty(); - *expr = Expr::BoolConstant( b.contains(a.as_str()), *pos); - } - // 'x' in "xxxxx" - (Expr::CharConstant(a, pos), Expr::StringConstant(b, _)) => { - state.set_dirty(); - *expr = Expr::BoolConstant(b.contains(*a), *pos); - } - // "xxx" in #{...} - (Expr::StringConstant(a, pos), Expr::Map(b, _)) => { - state.set_dirty(); - *expr = Expr::BoolConstant(b.iter().find(|(x, _)| x.name == *a).is_some(), *pos); - } - // 'x' in #{...} - (Expr::CharConstant(a, pos), Expr::Map(b, _)) => { - state.set_dirty(); - let ch = a.to_string(); - *expr = Expr::BoolConstant(b.iter().find(|(x, _)| x.name == &ch).is_some(), *pos); - } - // lhs in rhs - (lhs, rhs) => { optimize_expr(lhs, state); optimize_expr(rhs, state); } - }, // lhs && rhs Expr::And(x, _) => match (&mut x.lhs, &mut x.rhs) { // true && rhs -> rhs @@ -684,7 +664,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut State) { && state.optimization_level == OptimizationLevel::Simple // simple optimizations && x.args.len() == 2 // binary call && x.args.iter().all(Expr::is_constant) // all arguments are constants - && !is_valid_identifier(x.name.chars()) // cannot be scripted + //&& !is_valid_identifier(x.name.chars()) // cannot be scripted => { let mut arg_values: StaticVec<_> = x.args.iter().map(|e| e.get_constant_value().unwrap()).collect(); let arg_types: StaticVec<_> = arg_values.iter().map(Dynamic::type_id).collect(); @@ -783,16 +763,16 @@ fn optimize_top_level( engine: &Engine, scope: &Scope, lib: &[&Module], - level: OptimizationLevel, + optimization_level: OptimizationLevel, ) -> Vec { // If optimization level is None then skip optimizing - if level == OptimizationLevel::None { + if optimization_level == OptimizationLevel::None { statements.shrink_to_fit(); return statements; } // Set up the state - let mut state = State::new(engine, lib, level); + let mut state = State::new(engine, lib, optimization_level); // Add constants and variables from the scope scope.iter().for_each(|(name, constant, value)| { @@ -816,34 +796,17 @@ fn optimize_top_level( statements.iter_mut().enumerate().for_each(|(i, stmt)| { match stmt { - Stmt::Const(var_def, expr, _, _) if expr.is_some() => { + Stmt::Const(value_expr, Ident { name, .. }, _, _) => { // Load constants - let value_expr = expr.as_mut().unwrap(); optimize_expr(value_expr, &mut state); if value_expr.is_constant() { - state.push_var(&var_def.name, AccessMode::ReadOnly, value_expr.clone()); - } - - // Keep it in the global scope - if value_expr.is_unit() { - state.set_dirty(); - *expr = None; + state.push_var(name, AccessMode::ReadOnly, value_expr.clone()); } } - Stmt::Const(var_def, None, _, _) => { - state.push_var(&var_def.name, AccessMode::ReadOnly, Expr::Unit(var_def.pos)); - } - Stmt::Let(var_def, expr, _, _) => { - if let Some(value_expr) = expr { - optimize_expr(value_expr, &mut state); - } - - state.push_var( - &var_def.name, - AccessMode::ReadWrite, - Expr::Unit(var_def.pos), - ); + Stmt::Let(value_expr, Ident { name, pos, .. }, _, _) => { + optimize_expr(value_expr, &mut state); + state.push_var(name, AccessMode::ReadWrite, Expr::Unit(*pos)); } _ => { // Keep all variable declarations at this level @@ -852,7 +815,7 @@ fn optimize_top_level( Stmt::Let(_, _, _, _) | Stmt::Const(_, _, _, _) => true, #[cfg(not(feature = "no_module"))] Stmt::Import(_, _, _) => true, - _ => i == num_statements - 1, + _ => i >= num_statements - 1, }; optimize_stmt(stmt, &mut state, keep); } @@ -887,12 +850,12 @@ pub fn optimize_into_ast( scope: &Scope, mut statements: Vec, _functions: Vec, - level: OptimizationLevel, + optimization_level: OptimizationLevel, ) -> AST { let level = if cfg!(feature = "no_optimize") { OptimizationLevel::None } else { - level + optimization_level }; #[cfg(not(feature = "no_function"))] @@ -921,33 +884,49 @@ pub fn optimize_into_ast( lib2.set_script_fn(fn_def); }); + let lib2 = &[&lib2]; + _functions .into_iter() .map(|mut fn_def| { - let pos = fn_def.body.position(); + let pos = fn_def.body.pos; - // Optimize the function body - let mut body = optimize_top_level( - vec![fn_def.body], - engine, - &Scope::new(), - &[&lib2], - level, - ); + let mut body = fn_def.body.statements.into_vec(); - // {} -> Noop - fn_def.body = match body.pop().unwrap_or_else(|| Stmt::Noop(pos)) { - // { return val; } -> val - Stmt::Return((crate::ast::ReturnType::Return, _), Some(expr), _) => { - Stmt::Expr(expr) + loop { + // Optimize the function body + let state = &mut State::new(engine, lib2, level); + + body = optimize_stmt_block(body, state, true); + + match &mut body[..] { + // { return; } -> {} + [Stmt::Return(crate::ast::ReturnType::Return, None, _)] => { + body.clear(); + } + // { ...; return; } -> { ... } + [.., last_stmt, Stmt::Return(crate::ast::ReturnType::Return, None, _)] + if !last_stmt.returns_value() => + { + body.pop().unwrap(); + } + // { ...; return val; } -> { ...; val } + [.., Stmt::Return(crate::ast::ReturnType::Return, expr, pos)] => { + *body.last_mut().unwrap() = if let Some(expr) = expr { + Stmt::Expr(mem::take(expr)) + } else { + Stmt::Noop(*pos) + }; + } + _ => break, } - // { return; } -> () - Stmt::Return((crate::ast::ReturnType::Return, pos), None, _) => { - Stmt::Expr(Expr::Unit(pos)) - } - // All others - stmt => stmt, + } + + fn_def.body = StmtBlock { + statements: body.into(), + pos, }; + fn_def }) .for_each(|fn_def| { diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index dc66d207..7e3012f9 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -164,7 +164,7 @@ mod array_functions { result } } - #[rhai_fn(return_raw)] + #[rhai_fn(return_raw, pure)] pub fn map( ctx: NativeCallContext, array: &mut Array, @@ -197,7 +197,7 @@ mod array_functions { Ok(ar.into()) } - #[rhai_fn(return_raw)] + #[rhai_fn(return_raw, pure)] pub fn filter( ctx: NativeCallContext, array: &mut Array, @@ -233,8 +233,70 @@ mod array_functions { Ok(ar.into()) } - #[rhai_fn(return_raw)] + #[rhai_fn(return_raw, pure)] + pub fn contains( + ctx: NativeCallContext, + array: &mut Array, + value: Dynamic, + ) -> Result> { + for item in array.iter_mut() { + if ctx + .call_fn_dynamic_raw(OP_EQUALS, true, &mut [item, &mut value.clone()]) + .or_else(|err| match *err { + EvalAltResult::ErrorFunctionNotFound(ref fn_sig, _) + if fn_sig.starts_with(OP_EQUALS) => + { + if item.type_id() == value.type_id() { + // No default when comparing same type + Err(err) + } else { + Ok(Dynamic::FALSE) + } + } + _ => Err(err), + })? + .as_bool() + .unwrap_or(false) + { + return Ok(Dynamic::TRUE); + } + } + + Ok(Dynamic::FALSE) + } + #[rhai_fn(return_raw, pure)] pub fn index_of( + ctx: NativeCallContext, + array: &mut Array, + value: Dynamic, + ) -> Result> { + for (i, item) in array.iter_mut().enumerate() { + if ctx + .call_fn_dynamic_raw(OP_EQUALS, true, &mut [item, &mut value.clone()]) + .or_else(|err| match *err { + EvalAltResult::ErrorFunctionNotFound(ref fn_sig, _) + if fn_sig.starts_with(OP_EQUALS) => + { + if item.type_id() == value.type_id() { + // No default when comparing same type + Err(err) + } else { + Ok(Dynamic::FALSE) + } + } + _ => Err(err), + })? + .as_bool() + .unwrap_or(false) + { + return Ok((i as INT).into()); + } + } + + Ok((-1 as INT).into()) + } + #[rhai_fn(name = "index_of", return_raw, pure)] + pub fn index_of_filter( ctx: NativeCallContext, array: &mut Array, filter: FnPtr, @@ -267,7 +329,7 @@ mod array_functions { Ok((-1 as INT).into()) } - #[rhai_fn(return_raw)] + #[rhai_fn(return_raw, pure)] pub fn some( ctx: NativeCallContext, array: &mut Array, @@ -301,7 +363,7 @@ mod array_functions { Ok(false.into()) } - #[rhai_fn(return_raw)] + #[rhai_fn(return_raw, pure)] pub fn all( ctx: NativeCallContext, array: &mut Array, @@ -335,7 +397,7 @@ mod array_functions { Ok(true.into()) } - #[rhai_fn(return_raw)] + #[rhai_fn(return_raw, pure)] pub fn reduce( ctx: NativeCallContext, array: &mut Array, @@ -366,7 +428,7 @@ mod array_functions { Ok(result) } - #[rhai_fn(name = "reduce", return_raw)] + #[rhai_fn(name = "reduce", return_raw, pure)] pub fn reduce_with_initial( ctx: NativeCallContext, array: &mut Array, @@ -405,7 +467,7 @@ mod array_functions { Ok(result) } - #[rhai_fn(return_raw)] + #[rhai_fn(return_raw, pure)] pub fn reduce_rev( ctx: NativeCallContext, array: &mut Array, @@ -436,7 +498,7 @@ mod array_functions { Ok(result) } - #[rhai_fn(name = "reduce_rev", return_raw)] + #[rhai_fn(name = "reduce_rev", return_raw, pure)] pub fn reduce_rev_with_initial( ctx: NativeCallContext, array: &mut Array, @@ -634,7 +696,7 @@ mod array_functions { drained } - #[rhai_fn(name = "==", return_raw)] + #[rhai_fn(name = "==", return_raw, pure)] pub fn equals( ctx: NativeCallContext, array: &mut Array, @@ -648,18 +710,31 @@ mod array_functions { } for (a1, a2) in array.iter_mut().zip(array2.iter_mut()) { - let equals = ctx + if !ctx .call_fn_dynamic_raw(OP_EQUALS, true, &mut [a1, a2]) - .map(|v| v.as_bool().unwrap_or(false))?; - - if !equals { - return Ok(false.into()); + .or_else(|err| match *err { + EvalAltResult::ErrorFunctionNotFound(ref fn_sig, _) + if fn_sig.starts_with(OP_EQUALS) => + { + if a1.type_id() == a2.type_id() { + // No default when comparing same type + Err(err) + } else { + Ok(Dynamic::FALSE) + } + } + _ => Err(err), + })? + .as_bool() + .unwrap_or(false) + { + return Ok(Dynamic::FALSE); } } - Ok(true.into()) + Ok(Dynamic::TRUE) } - #[rhai_fn(name = "!=", return_raw)] + #[rhai_fn(name = "!=", return_raw, pure)] pub fn not_equals( ctx: NativeCallContext, array: &mut Array, diff --git a/src/packages/map_basic.rs b/src/packages/map_basic.rs index e1c0558b..f54af86c 100644 --- a/src/packages/map_basic.rs +++ b/src/packages/map_basic.rs @@ -13,8 +13,8 @@ def_package!(crate:BasicMapPackage:"Basic object map utilities.", lib, { #[export_module] mod map_functions { - #[rhai_fn(pure)] - pub fn has(map: &mut Map, prop: ImmutableString) -> bool { + #[rhai_fn(name = "has", pure)] + pub fn contains(map: &mut Map, prop: ImmutableString) -> bool { map.contains_key(&prop) } #[rhai_fn(pure)] diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 783dc2c4..ce3d819e 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -74,14 +74,6 @@ mod string_functions { } } - #[rhai_fn(name = "contains")] - pub fn contains_char(string: &str, character: char) -> bool { - string.contains(character) - } - pub fn contains(string: &str, find_string: &str) -> bool { - string.contains(find_string) - } - #[rhai_fn(name = "index_of")] pub fn index_of_char_starting_from(string: &str, character: char, start: INT) -> INT { let start = if start < 0 { diff --git a/src/parse_error.rs b/src/parse_error.rs index a965af0b..b31608b6 100644 --- a/src/parse_error.rs +++ b/src/parse_error.rs @@ -38,6 +38,7 @@ pub enum LexError { impl Error for LexError {} impl fmt::Display for LexError { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::UnexpectedInput(s) => write!(f, "Unexpected '{}'", s), @@ -293,7 +294,7 @@ pub struct ParseError(pub Box, pub Position); impl Error for ParseError {} impl fmt::Display for ParseError { - #[inline] + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f)?; diff --git a/src/parser.rs b/src/parser.rs index 3361921b..db15c06a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,10 +2,10 @@ use crate::ast::{ BinaryExpr, CustomExpr, Expr, FnCallExpr, FnHash, Ident, OpAssignment, ReturnType, ScriptFnDef, - Stmt, + Stmt, StmtBlock, }; use crate::dynamic::{AccessMode, Union}; -use crate::engine::KEYWORD_THIS; +use crate::engine::{KEYWORD_THIS, OP_CONTAINS}; use crate::module::NamespaceRef; use crate::optimize::optimize_into_ast; use crate::optimize::OptimizationLevel; @@ -243,7 +243,11 @@ impl Expr { let setter = state.get_interned_string(crate::engine::make_setter(&ident.name)); let hash_set = calc_fn_hash(empty(), &setter, 2); - Self::Property(Box::new((getter, hash_get, setter, hash_set, ident.into()))) + Self::Property(Box::new(( + (getter, hash_get), + (setter, hash_set), + ident.into(), + ))) } _ => self, } @@ -480,7 +484,6 @@ fn parse_index_chain( Expr::CharConstant(_, _) | Expr::And(_, _) | Expr::Or(_, _) - | Expr::In(_, _) | Expr::BoolConstant(_, _) | Expr::Unit(_) => { return Err(PERR::MalformedIndexExpr( @@ -514,7 +517,6 @@ fn parse_index_chain( Expr::CharConstant(_, _) | Expr::And(_, _) | Expr::Or(_, _) - | Expr::In(_, _) | Expr::BoolConstant(_, _) | Expr::Unit(_) => { return Err(PERR::MalformedIndexExpr( @@ -548,8 +550,8 @@ fn parse_index_chain( ) .into_err(x.position())) } - // lhs[??? && ???], lhs[??? || ???], lhs[??? in ???] - x @ Expr::And(_, _) | x @ Expr::Or(_, _) | x @ Expr::In(_, _) => { + // lhs[??? && ???], lhs[??? || ???] + x @ Expr::And(_, _) | x @ Expr::Or(_, _) => { return Err(PERR::MalformedIndexExpr( "Array access expects integer index, not a boolean".into(), ) @@ -811,7 +813,7 @@ fn parse_switch( } } - let mut table = HashMap::new(); + let mut table = HashMap::::new(); let mut def_stmt = None; loop { @@ -871,10 +873,10 @@ fn parse_switch( let need_comma = !stmt.is_self_terminated(); def_stmt = if let Some(hash) = hash { - table.insert(hash, stmt); + table.insert(hash, stmt.into()); None } else { - Some(stmt) + Some(stmt.into()) }; match input.peek().unwrap() { @@ -905,7 +907,10 @@ fn parse_switch( Ok(Stmt::Switch( item, - Box::new((final_table.into(), def_stmt)), + Box::new(( + final_table.into(), + def_stmt.unwrap_or_else(|| Stmt::Noop(Position::NONE).into()), + )), settings.pos, )) } @@ -956,7 +961,7 @@ fn parse_primary( // { - block statement as expression Token::LeftBrace if settings.allow_stmt_expr => { match parse_block(input, state, lib, settings.level_up())? { - Stmt::Block(statements, pos) => Expr::Stmt(Box::new(statements.into()), pos), + block @ Stmt::Block(_, _) => Expr::Stmt(Box::new(block.into())), stmt => unreachable!("expecting Stmt::Block, but gets {:?}", stmt), } } @@ -964,15 +969,14 @@ fn parse_primary( Token::LeftParen => parse_paren_expr(input, state, lib, settings.level_up())?, // If statement is allowed to act as expressions - Token::If if settings.allow_if_expr => Expr::Stmt( - Box::new(vec![parse_if(input, state, lib, settings.level_up())?].into()), - settings.pos, - ), + Token::If if settings.allow_if_expr => Expr::Stmt(Box::new( + parse_if(input, state, lib, settings.level_up())?.into(), + )), // Switch statement is allowed to act as expressions - Token::Switch if settings.allow_switch_expr => Expr::Stmt( - Box::new(vec![parse_switch(input, state, lib, settings.level_up())?].into()), - settings.pos, - ), + Token::Switch if settings.allow_switch_expr => Expr::Stmt(Box::new( + parse_switch(input, state, lib, settings.level_up())?.into(), + )), + // | ... #[cfg(not(feature = "no_function"))] Token::Pipe | Token::Or if settings.allow_anonymous_fn => { @@ -1149,14 +1153,14 @@ fn parse_primary( .into_err(pos)); } - let (_, namespace, Ident { name, pos }) = *x; + let (_, namespace, Ident { name, pos, .. }) = *x; settings.pos = pos; let ns = namespace.map(|(_, ns)| ns); parse_fn_call(input, state, lib, name, true, ns, settings.level_up())? } // Function call (Expr::Variable(x), Token::LeftParen) => { - let (_, namespace, Ident { name, pos }) = *x; + let (_, namespace, Ident { name, pos, .. }) = *x; settings.pos = pos; let ns = namespace.map(|(_, ns)| ns); parse_fn_call(input, state, lib, name, false, ns, settings.level_up())? @@ -1397,7 +1401,7 @@ fn make_assignment_stmt<'a>( } // var (indexed) = rhs Expr::Variable(x) => { - let (index, _, Ident { name, pos }) = x.as_ref(); + let (index, _, Ident { name, pos, .. }) = x.as_ref(); match state.stack[(state.stack.len() - index.unwrap().get())].1 { AccessMode::ReadWrite => { Ok(Stmt::Assignment(Box::new((lhs, rhs, op_info)), op_pos)) @@ -1418,7 +1422,7 @@ fn make_assignment_stmt<'a>( } // var[???] (indexed) = rhs, var.??? (indexed) = rhs Expr::Variable(x) => { - let (index, _, Ident { name, pos }) = x.as_ref(); + let (index, _, Ident { name, pos, .. }) = x.as_ref(); match state.stack[(state.stack.len() - index.unwrap().get())].1 { AccessMode::ReadWrite => { Ok(Stmt::Assignment(Box::new((lhs, rhs, op_info)), op_pos)) @@ -1508,7 +1512,7 @@ fn make_dot_expr( let setter = state.get_interned_string(crate::engine::make_setter(&ident.name)); let hash_set = calc_fn_hash(empty(), &setter, 2); - let rhs = Expr::Property(Box::new((getter, hash_get, setter, hash_set, ident))); + let rhs = Expr::Property(Box::new(((getter, hash_get), (setter, hash_set), ident))); Expr::Dot(Box::new(BinaryExpr { lhs, rhs }), op_pos) } @@ -1602,139 +1606,6 @@ fn make_dot_expr( }) } -/// Make an 'in' expression. -fn make_in_expr(lhs: Expr, rhs: Expr, op_pos: Position) -> Result { - match (&lhs, &rhs) { - (_, x @ Expr::IntegerConstant(_, _)) - | (_, x @ Expr::And(_, _)) - | (_, x @ Expr::Or(_, _)) - | (_, x @ Expr::In(_, _)) - | (_, x @ Expr::BoolConstant(_, _)) - | (_, x @ Expr::Unit(_)) => { - return Err(PERR::MalformedInExpr( - "'in' expression expects a string, array or object map".into(), - ) - .into_err(x.position())) - } - - #[cfg(not(feature = "no_float"))] - (_, x @ Expr::FloatConstant(_, _)) => { - return Err(PERR::MalformedInExpr( - "'in' expression expects a string, array or object map".into(), - ) - .into_err(x.position())) - } - - // "xxx" in "xxxx", 'x' in "xxxx" - OK! - (Expr::StringConstant(_, _), Expr::StringConstant(_, _)) - | (Expr::CharConstant(_, _), Expr::StringConstant(_, _)) => (), - - // 123.456 in "xxxx" - #[cfg(not(feature = "no_float"))] - (x @ Expr::FloatConstant(_, _), Expr::StringConstant(_, _)) => { - return Err(PERR::MalformedInExpr( - "'in' expression for a string expects a string, not a float".into(), - ) - .into_err(x.position())) - } - // 123 in "xxxx" - (x @ Expr::IntegerConstant(_, _), Expr::StringConstant(_, _)) => { - return Err(PERR::MalformedInExpr( - "'in' expression for a string expects a string, not a number".into(), - ) - .into_err(x.position())) - } - // (??? && ???) in "xxxx", (??? || ???) in "xxxx", (??? in ???) in "xxxx", - // true in "xxxx", false in "xxxx" - (x @ Expr::And(_, _), Expr::StringConstant(_, _)) - | (x @ Expr::Or(_, _), Expr::StringConstant(_, _)) - | (x @ Expr::In(_, _), Expr::StringConstant(_, _)) - | (x @ Expr::BoolConstant(_, _), Expr::StringConstant(_, _)) => { - return Err(PERR::MalformedInExpr( - "'in' expression for a string expects a string, not a boolean".into(), - ) - .into_err(x.position())) - } - // [???, ???, ???] in "xxxx" - (x @ Expr::Array(_, _), Expr::StringConstant(_, _)) => { - return Err(PERR::MalformedInExpr( - "'in' expression for a string expects a string, not an array".into(), - ) - .into_err(x.position())) - } - // #{...} in "xxxx" - (x @ Expr::Map(_, _), Expr::StringConstant(_, _)) => { - return Err(PERR::MalformedInExpr( - "'in' expression for a string expects a string, not an object map".into(), - ) - .into_err(x.position())) - } - // () in "xxxx" - (x @ Expr::Unit(_), Expr::StringConstant(_, _)) => { - return Err(PERR::MalformedInExpr( - "'in' expression for a string expects a string, not ()".into(), - ) - .into_err(x.position())) - } - - // "xxx" in #{...}, 'x' in #{...} - OK! - (Expr::StringConstant(_, _), Expr::Map(_, _)) - | (Expr::CharConstant(_, _), Expr::Map(_, _)) => (), - - // 123.456 in #{...} - #[cfg(not(feature = "no_float"))] - (x @ Expr::FloatConstant(_, _), Expr::Map(_, _)) => { - return Err(PERR::MalformedInExpr( - "'in' expression for an object map expects a string, not a float".into(), - ) - .into_err(x.position())) - } - // 123 in #{...} - (x @ Expr::IntegerConstant(_, _), Expr::Map(_, _)) => { - return Err(PERR::MalformedInExpr( - "'in' expression for an object map expects a string, not a number".into(), - ) - .into_err(x.position())) - } - // (??? && ???) in #{...}, (??? || ???) in #{...}, (??? in ???) in #{...}, - // true in #{...}, false in #{...} - (x @ Expr::And(_, _), Expr::Map(_, _)) - | (x @ Expr::Or(_, _), Expr::Map(_, _)) - | (x @ Expr::In(_, _), Expr::Map(_, _)) - | (x @ Expr::BoolConstant(_, _), Expr::Map(_, _)) => { - return Err(PERR::MalformedInExpr( - "'in' expression for an object map expects a string, not a boolean".into(), - ) - .into_err(x.position())) - } - // [???, ???, ???] in #{..} - (x @ Expr::Array(_, _), Expr::Map(_, _)) => { - return Err(PERR::MalformedInExpr( - "'in' expression for an object map expects a string, not an array".into(), - ) - .into_err(x.position())) - } - // #{...} in #{..} - (x @ Expr::Map(_, _), Expr::Map(_, _)) => { - return Err(PERR::MalformedInExpr( - "'in' expression for an object map expects a string, not an object map".into(), - ) - .into_err(x.position())) - } - // () in #{...} - (x @ Expr::Unit(_), Expr::Map(_, _)) => { - return Err(PERR::MalformedInExpr( - "'in' expression for an object map expects a string, not ()".into(), - ) - .into_err(x.position())) - } - - _ => (), - } - - Ok(Expr::In(Box::new(BinaryExpr { lhs, rhs }), op_pos)) -} - /// Parse a binary expression. fn parse_binary_op( input: &mut TokenStream, @@ -1880,9 +1751,21 @@ fn parse_binary_op( ) } Token::In => { - let rhs = args.pop().unwrap(); - let current_lhs = args.pop().unwrap(); - make_in_expr(current_lhs, rhs, pos)? + // Swap the arguments + let current_lhs = args.remove(0); + args.push(current_lhs); + + // Convert into a call to `contains` + let hash = calc_fn_hash(empty(), OP_CONTAINS, 2); + Expr::FnCall( + Box::new(FnCallExpr { + hash: FnHash::from_script(hash), + args, + name: OP_CONTAINS.into(), + ..op_base + }), + pos, + ) } Token::Custom(s) @@ -1984,8 +1867,8 @@ fn parse_custom_syntax( tokens.push(keyword); } MARKER_BLOCK => match parse_block(input, state, lib, settings)? { - Stmt::Block(statements, pos) => { - keywords.push(Expr::Stmt(Box::new(statements.into()), pos)); + block @ Stmt::Block(_, _) => { + keywords.push(Expr::Stmt(Box::new(block.into()))); let keyword = state.get_interned_string(MARKER_BLOCK); segments.push(keyword.clone()); tokens.push(keyword); @@ -2118,20 +2001,20 @@ fn parse_if( // if guard { if_body } else ... let else_body = if match_token(input, Token::Else).0 { - Some(if let (Token::If, _) = input.peek().unwrap() { + if let (Token::If, _) = input.peek().unwrap() { // if guard { if_body } else if ... parse_if(input, state, lib, settings.level_up())? } else { // if guard { if_body } else { else-body } parse_block(input, state, lib, settings.level_up())? - }) + } } else { - None + Stmt::Noop(Position::NONE) }; Ok(Stmt::If( guard, - Box::new((if_body, else_body)), + Box::new((if_body.into(), else_body.into())), settings.pos, )) } @@ -2151,18 +2034,18 @@ fn parse_while_loop( (Token::While, pos) => { ensure_not_statement_expr(input, "a boolean")?; let expr = parse_expr(input, state, lib, settings.level_up())?; - (Some(expr), pos) + (expr, pos) } - (Token::Loop, pos) => (None, pos), + (Token::Loop, pos) => (Expr::Unit(Position::NONE), pos), _ => unreachable!(), }; settings.pos = token_pos; ensure_not_assignment(input)?; settings.is_breakable = true; - let body = Box::new(parse_block(input, state, lib, settings.level_up())?); + let body = parse_block(input, state, lib, settings.level_up())?; - Ok(Stmt::While(guard, body, settings.pos)) + Ok(Stmt::While(guard, Box::new(body.into()), settings.pos)) } /// Parse a do loop. @@ -2180,7 +2063,7 @@ fn parse_do( // do { body } [while|until] guard settings.is_breakable = true; - let body = Box::new(parse_block(input, state, lib, settings.level_up())?); + let body = parse_block(input, state, lib, settings.level_up())?; let is_while = match input.next().unwrap() { (Token::While, _) => true, @@ -2198,7 +2081,12 @@ fn parse_do( let guard = parse_expr(input, state, lib, settings.level_up())?; ensure_not_assignment(input)?; - Ok(Stmt::Do(body, guard, is_while, settings.pos)) + Ok(Stmt::Do( + Box::new(body.into()), + guard, + is_while, + settings.pos, + )) } /// Parse a for loop. @@ -2253,7 +2141,7 @@ fn parse_for( state.stack.truncate(prev_stack_len); - Ok(Stmt::For(expr, Box::new((name, body)), settings.pos)) + Ok(Stmt::For(expr, Box::new((name, body.into())), settings.pos)) } /// Parse a variable definition statement. @@ -2290,18 +2178,18 @@ fn parse_let( // let name = ... let expr = if match_token(input, Token::Equals).0 { // let name = expr - Some(parse_expr(input, state, lib, settings.level_up())?) + parse_expr(input, state, lib, settings.level_up())? } else { - None + Expr::Unit(Position::NONE) }; state.stack.push((name, var_type)); match var_type { // let name = expr - AccessMode::ReadWrite => Ok(Stmt::Let(Box::new(var_def), expr, export, settings.pos)), + AccessMode::ReadWrite => Ok(Stmt::Let(expr, var_def, export, settings.pos)), // const name = { expr:constant } - AccessMode::ReadOnly => Ok(Stmt::Const(Box::new(var_def), expr, export, settings.pos)), + AccessMode::ReadOnly => Ok(Stmt::Const(expr, var_def, export, settings.pos)), } } @@ -2342,10 +2230,10 @@ fn parse_import( Ok(Stmt::Import( expr, - Some(Box::new(Ident { + Some(Ident { name, pos: name_pos, - })), + }), settings.pos, )) } @@ -2542,7 +2430,7 @@ fn parse_stmt( ) -> Result { use AccessMode::{ReadOnly, ReadWrite}; - let mut _comments: Vec = Default::default(); + let mut _comments: StaticVec = Default::default(); #[cfg(not(feature = "no_function"))] { @@ -2688,16 +2576,13 @@ fn parse_stmt( match input.peek().unwrap() { // `return`/`throw` at - (Token::EOF, pos) => Ok(Stmt::Return((return_type, token_pos), None, *pos)), + (Token::EOF, _) => Ok(Stmt::Return(return_type, None, token_pos)), // `return;` or `throw;` - (Token::SemiColon, _) => { - Ok(Stmt::Return((return_type, token_pos), None, settings.pos)) - } + (Token::SemiColon, _) => Ok(Stmt::Return(return_type, None, token_pos)), // `return` or `throw` with expression (_, _) => { let expr = parse_expr(input, state, lib, settings.level_up())?; - let pos = expr.position(); - Ok(Stmt::Return((return_type, token_pos), Some(expr), pos)) + Ok(Stmt::Return(return_type, Some(expr), token_pos)) } } } @@ -2775,7 +2660,7 @@ fn parse_try_catch( let catch_body = parse_block(input, state, lib, settings.level_up())?; Ok(Stmt::TryCatch( - Box::new((body, var_def, catch_body)), + Box::new((body.into(), var_def, catch_body.into())), settings.pos, catch_pos, )) @@ -2789,7 +2674,7 @@ fn parse_fn( lib: &mut FunctionsLib, access: FnAccess, mut settings: ParseSettings, - comments: Vec, + comments: StaticVec, ) -> Result { #[cfg(not(feature = "unchecked"))] settings.ensure_level_within_max_limit(state.max_expr_depth)?; @@ -2852,7 +2737,8 @@ fn parse_fn( parse_block(input, state, lib, settings.level_up())? } (_, pos) => return Err(PERR::FnMissingBody(name).into_err(*pos)), - }; + } + .into(); let params: StaticVec<_> = params.into_iter().map(|(p, _)| p).collect(); @@ -2914,7 +2800,7 @@ fn make_curry_from_externals(fn_expr: Expr, externals: StaticVec, pos: Po let mut statements: StaticVec<_> = Default::default(); statements.extend(externals.into_iter().map(Stmt::Share)); statements.push(Stmt::Expr(expr)); - Expr::Stmt(Box::new(statements), pos) + Expr::Stmt(Box::new(StmtBlock { statements, pos })) } /// Parse an anonymous function definition. @@ -3016,7 +2902,7 @@ fn parse_anon_fn( params, #[cfg(not(feature = "no_closure"))] externals: Default::default(), - body, + body: body.into(), lib: None, #[cfg(not(feature = "no_module"))] mods: Default::default(), diff --git a/src/result.rs b/src/result.rs index 49743d1d..0e008cad 100644 --- a/src/result.rs +++ b/src/result.rs @@ -58,8 +58,6 @@ pub enum EvalAltResult { /// Trying to index into a type that is not an array, an object map, or a string, and has no /// indexer function defined. Wrapped value is the type name. ErrorIndexingType(String, Position), - /// Invalid arguments for `in` operator. - ErrorInExpr(Position), /// The `for` statement encounters a type that is not an iterator. ErrorFor(Position), /// Data race detected when accessing a variable. Wrapped value is the variable name. @@ -122,7 +120,6 @@ impl EvalAltResult { Self::ErrorDataRace(_, _) => "Data race detected when accessing variable", Self::ErrorAssignmentToConstant(_, _) => "Cannot modify a constant", Self::ErrorMismatchOutputType(_, _, _) => "Output type is incorrect", - Self::ErrorInExpr(_) => "Malformed 'in' expression", Self::ErrorDotExpr(_, _) => "Malformed dot expression", Self::ErrorArithmetic(_, _) => "Arithmetic error", Self::ErrorTooManyOperations(_) => "Too many operations", @@ -182,7 +179,6 @@ impl fmt::Display for EvalAltResult { Self::ErrorUnboundThis(_) | Self::ErrorFor(_) - | Self::ErrorInExpr(_) | Self::ErrorDotExpr(_, _) | Self::ErrorTooManyOperations(_) | Self::ErrorTooManyModules(_) @@ -305,7 +301,6 @@ impl EvalAltResult { | Self::ErrorDataRace(_, _) | Self::ErrorAssignmentToConstant(_, _) | Self::ErrorMismatchOutputType(_, _, _) - | Self::ErrorInExpr(_) | Self::ErrorDotExpr(_, _) | Self::ErrorArithmetic(_, _) | Self::ErrorRuntime(_, _) => true, @@ -362,7 +357,6 @@ impl EvalAltResult { | Self::ErrorParsing(_, _) | Self::ErrorUnboundThis(_) | Self::ErrorFor(_) - | Self::ErrorInExpr(_) | Self::ErrorArithmetic(_, _) | Self::ErrorTooManyOperations(_) | Self::ErrorTooManyModules(_) @@ -430,7 +424,6 @@ impl EvalAltResult { | Self::ErrorDataRace(_, pos) | Self::ErrorAssignmentToConstant(_, pos) | Self::ErrorMismatchOutputType(_, _, pos) - | Self::ErrorInExpr(pos) | Self::ErrorDotExpr(_, pos) | Self::ErrorArithmetic(_, pos) | Self::ErrorTooManyOperations(pos) @@ -471,7 +464,6 @@ impl EvalAltResult { | Self::ErrorDataRace(_, pos) | Self::ErrorAssignmentToConstant(_, pos) | Self::ErrorMismatchOutputType(_, _, pos) - | Self::ErrorInExpr(pos) | Self::ErrorDotExpr(_, pos) | Self::ErrorArithmetic(_, pos) | Self::ErrorTooManyOperations(pos) diff --git a/src/serde/metadata.rs b/src/serde/metadata.rs index 7f4edb25..05a709a9 100644 --- a/src/serde/metadata.rs +++ b/src/serde/metadata.rs @@ -151,7 +151,7 @@ impl From<&crate::module::FuncInfo> for FnMetadata { } #[cfg(not(feature = "no_function"))] { - info.func.get_fn_def().comments.clone() + info.func.get_fn_def().comments.to_vec() } } else { Default::default() diff --git a/src/utils.rs b/src/utils.rs index a51501e9..dd768752 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -116,16 +116,19 @@ pub(crate) fn combine_hashes(a: u64, b: u64) -> u64 { pub struct HashableHashMap(HashMap); impl From> for HashableHashMap { + #[inline(always)] fn from(value: HashMap) -> Self { Self(value) } } impl AsRef> for HashableHashMap { + #[inline(always)] fn as_ref(&self) -> &HashMap { &self.0 } } impl AsMut> for HashableHashMap { + #[inline(always)] fn as_mut(&mut self) -> &mut HashMap { &mut self.0 } @@ -133,21 +136,25 @@ impl AsMut> for HashableHashMap impl Deref for HashableHashMap { type Target = HashMap; + #[inline(always)] fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for HashableHashMap { + #[inline(always)] fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl Debug for HashableHashMap { + #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } impl Hash for HashableHashMap { + #[inline(always)] fn hash(&self, state: &mut B) { let mut keys: Vec<_> = self.0.keys().collect(); keys.sort(); diff --git a/tests/maps.rs b/tests/maps.rs index 988c51a3..5892c29d 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -38,7 +38,7 @@ fn test_map_indexing() -> Result<(), Box> { engine.eval::<()>("let y = #{a: 1, b: 2, c: 3}; y.z")?; assert!(engine.eval::(r#"let y = #{a: 1, b: 2, c: 3}; "c" in y"#)?); - assert!(engine.eval::("let y = #{a: 1, b: 2, c: 3}; 'b' in y")?); + assert!(engine.eval::(r#"let y = #{a: 1, b: 2, c: 3}; "b" in y"#)?); assert!(!engine.eval::(r#"let y = #{a: 1, b: 2, c: 3}; "z" in y"#)?); assert_eq!( diff --git a/tests/optimizer.rs b/tests/optimizer.rs index d062ec5f..4c7cfb49 100644 --- a/tests/optimizer.rs +++ b/tests/optimizer.rs @@ -55,20 +55,18 @@ fn test_optimizer_parse() -> Result<(), Box> { let ast = engine.compile("{ const DECISION = false; if DECISION { 42 } else { 123 } }")?; - assert!(format!("{:?}", ast).starts_with(r#"AST { source: None, statements: [Block([Const(Ident("DECISION" @ 1:9), Some(BoolConstant(false, 1:20)), false, 1:3), Expr(IntegerConstant(123, 1:53))], 1:1)]"#)); + assert!(format!("{:?}", ast).starts_with(r#"AST { source: None, body: [Block([Const(BoolConstant(false, 1:20), Ident("DECISION" @ 1:9), false, 1:3), Expr(IntegerConstant(123, 1:53))], 1:1)], functions: Module("#)); let ast = engine.compile("if 1 == 2 { 42 }")?; - assert!( - format!("{:?}", ast).starts_with("AST { source: None, statements: [], functions: Module(") - ); + assert!(format!("{:?}", ast).starts_with("AST { source: None, body: [], functions: Module(")); engine.set_optimization_level(OptimizationLevel::Full); let ast = engine.compile("abs(-42)")?; assert!(format!("{:?}", ast) - .starts_with(r"AST { source: None, statements: [Expr(IntegerConstant(42, 1:1))]")); + .starts_with(r"AST { source: None, body: [Expr(IntegerConstant(42, 1:1))]")); Ok(()) }