diff --git a/CHANGELOG.md b/CHANGELOG.md index e1c751a9..a982c131 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,23 +9,74 @@ Bug fixes * Custom syntax starting with a disabled standard keyword now works properly. * When calling `Engine::call_fn`, new variables defined during evaluation of the body script are removed and no longer spill into the function call. +* `NamespaceRef::new` is fixed. Enhancements ------------ +### `Engine` API + * `Engine::consume_XXX` methods are renamed to `Engine::run_XXX` to make meanings clearer. The `consume_XXX` API is deprecated. +* `Engine::register_type_XXX` are now available even under `no_object`. +* Added `Engine::on_parse_token` to allow remapping certain tokens during parsing. + +### Custom Syntax + * `$symbol$` is supported in custom syntax to match any symbol. * Custom syntax with `$block$`, `}` or `;` as the last symbol are now self-terminating (i.e. no need to attach a terminating `;`). + +### `Dynamic` Values + * `Dynamic::as_string` and `Dynamic::as_immutable_string` are deprecated and replaced by `into_string` and `into_immutable_string` respectively. * Added a number of constants to `Dynamic`. * Added a number of constants and `fromXXX` constant methods to `Dynamic`. +* Added `sin`, `cos` and `tan` for `Decimal` values. + +### `Decimal` Values + * `parse_float()`, `PI()` and `E()` now defer to `Decimal` under `no_float` if `decimal` is turned on. * Added `log10()` for `Decimal`. * `ln` for `Decimal` is now checked and won't panic. + +### String Values + +* `SmartString` now uses `LazyCompact` instead of `Compact` to minimize allocations. +* Added `pop` for strings. + +### `Scope` API + * `Scope::set_value` now takes anything that implements `Into>`. * Added `Scope::is_constant` to check if a variable is constant. * Added `Scope::set_or_push` to add a new variable only if one doesn't already exist. -* `Engine::register_type_XXX` are now available even under `no_object`. + +### `AST` API + +* Added `ASTNode::position`. +* `ReturnType` is removed in favor of option flags for `Stmt::Return`. +* `Stmt::Break` and `Stmt::Continue` are merged into `Stmt::BreakLoop` via an option flag. +* `StaticVec` is changed to keep three items inline instead of four. + + +Version 1.0.6 +============= + +* `MultiInputsStream`, `ParseState`, `TokenIterator`, `IdentifierBuilder` and `AccessMode` are exported under the `internals` feature. + + +Version 1.0.5 +============= + +Bug fixes +--------- + +* `FloatWrapper` is no longer erroneously exported under `no_float+internals`. +* The `sign` function now works properly for float values that are `NaN`. + + +Version 1.0.4 +============= + +* Fixed bug with `catch` variable used in `catch` block. Version 1.0.2 diff --git a/Cargo.toml b/Cargo.toml index bf0a805f..ef48ed62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ wasm-bindgen = ["instant/wasm-bindgen"] stdweb = ["instant/stdweb"] # internal feature flags - volatile -no_smartstring = [] # Do not use SmartString +no_smartstring = [] # do not use SmartString [profile.release] lto = "fat" @@ -92,7 +92,7 @@ default-features = false optional = true [dependencies.rust_decimal] -version = "1.15" +version = "1.16" default-features = false features = ["maths"] optional = true diff --git a/README.md b/README.md index f63abb9d..f3cef610 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,47 @@ For those who actually want their own language * Extend the language with [custom syntax](https://rhai.rs/book/engine/custom-syntax.html). +Example +------- + +The [`scripts`](https://github.com/rhaiscript/rhai/tree/master/scripts) subdirectory contains sample Rhai scripts. + +Below is the standard _Fibonacci_ example for scripting languages: + +```js +// This Rhai script calculates the n-th Fibonacci number using a really dumb algorithm +// to test the speed of the scripting engine. + +const TARGET = 28; +const REPEAT = 5; + +fn fib(n) { + if n < 2 { + n + } else { + fib(n-1) + fib(n-2) + } +} + +print(`Running Fibonacci(${TARGET}) x ${REPEAT} times...`); +print("Ready... Go!"); + +let result; +let now = timestamp(); + +for n in range(0, REPEAT) { + result = fib(TARGET); +} + +print(`Finished. Run time = ${now.elapsed} seconds.`); + +print(`Fibonacci number #${TARGET} = ${result}`); + +if result != 317_811 { + print("The answer is WRONG! Should be 317,811!"); +} +``` + Project Site ------------ diff --git a/codegen/src/module.rs b/codegen/src/module.rs index 7e7421a5..c5d20b51 100644 --- a/codegen/src/module.rs +++ b/codegen/src/module.rs @@ -49,7 +49,7 @@ impl ExportedParams for ExportedModParams { fn from_info(info: ExportInfo) -> syn::Result { let ExportInfo { items: attrs, .. } = info; - let mut name = Default::default(); + let mut name = String::new(); let mut skip = false; let mut scope = None; for attr in attrs { diff --git a/codegen/ui_tests/rhai_fn_non_clonable_return.stderr b/codegen/ui_tests/rhai_fn_non_clonable_return.stderr index f2733fce..70d877e8 100644 --- a/codegen/ui_tests/rhai_fn_non_clonable_return.stderr +++ b/codegen/ui_tests/rhai_fn_non_clonable_return.stderr @@ -2,10 +2,13 @@ error[E0277]: the trait bound `NonClonable: Clone` is not satisfied --> $DIR/rhai_fn_non_clonable_return.rs:11:8 | 11 | pub fn test_fn(input: f32) -> NonClonable { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `NonClonable` + | ^^^^^^^^^^^^^^^^^^^^^^^----------- + | | | + | | required by a bound introduced by this call + | the trait `Clone` is not implemented for `NonClonable` | note: required by a bound in `rhai::Dynamic::from` - --> $DIR/dynamic.rs:1121:30 + --> $DIR/dynamic.rs:1122:30 | -1121 | pub fn from(mut value: T) -> Self { +1122 | pub fn from(mut value: T) -> Self { | ^^^^^ required by this bound in `rhai::Dynamic::from` diff --git a/codegen/ui_tests/rhai_mod_non_clonable_return.stderr b/codegen/ui_tests/rhai_mod_non_clonable_return.stderr index 122d2b26..82e9725b 100644 --- a/codegen/ui_tests/rhai_mod_non_clonable_return.stderr +++ b/codegen/ui_tests/rhai_mod_non_clonable_return.stderr @@ -2,10 +2,13 @@ error[E0277]: the trait bound `NonClonable: Clone` is not satisfied --> $DIR/rhai_mod_non_clonable_return.rs:12:12 | 12 | pub fn test_fn(input: f32) -> NonClonable { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `NonClonable` + | ^^^^^^^^^^^^^^^^^^^^^^^----------- + | | | + | | required by a bound introduced by this call + | the trait `Clone` is not implemented for `NonClonable` | note: required by a bound in `rhai::Dynamic::from` - --> $DIR/dynamic.rs:1121:30 + --> $DIR/dynamic.rs:1122:30 | -1121 | pub fn from(mut value: T) -> Self { +1122 | pub fn from(mut value: T) -> Self { | ^^^^^ required by this bound in `rhai::Dynamic::from` diff --git a/scripts/fibonacci.rhai b/scripts/fibonacci.rhai index fd3ba5c0..dac77d22 100644 --- a/scripts/fibonacci.rhai +++ b/scripts/fibonacci.rhai @@ -12,7 +12,7 @@ fn fib(n) { } } -print(`Running Fibonacci(28) x ${REPEAT} times...`); +print(`Running Fibonacci(${TARGET}) x ${REPEAT} times...`); print("Ready... Go!"); let result; diff --git a/src/ast.rs b/src/ast.rs index e7541387..3e1cd195 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -16,7 +16,10 @@ use std::{ hash::Hash, mem, num::{NonZeroU8, NonZeroUsize}, - ops::{Add, AddAssign, Deref, DerefMut, Not, Sub, SubAssign}, + ops::{ + Add, AddAssign, BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref, DerefMut, Not, Sub, + SubAssign, + }, }; #[cfg(not(feature = "no_float"))] @@ -201,13 +204,7 @@ pub struct AST { impl Default for AST { #[inline(always)] fn default() -> Self { - Self { - source: None, - body: Default::default(), - functions: Default::default(), - #[cfg(not(feature = "no_module"))] - resolver: None, - } + Self::empty() } } @@ -227,6 +224,18 @@ impl AST { resolver: None, } } + /// Create an empty [`AST`]. + #[inline] + #[must_use] + pub(crate) fn empty() -> Self { + Self { + source: None, + body: Default::default(), + functions: Default::default(), + #[cfg(not(feature = "no_module"))] + resolver: None, + } + } /// Create a new [`AST`] with a source name. #[inline(always)] #[must_use] @@ -837,20 +846,6 @@ impl fmt::Debug for Ident { } } -/// _(internals)_ A type encapsulating the mode of a `return`/`throw` statement. -/// Exported under the `internals` feature only. -/// -/// # Volatile Data Structure -/// -/// This type is volatile and may change. -#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] -pub enum ReturnType { - /// `return` statement. - Return, - /// `throw` statement. - Exception, -} - /// _(internals)_ An [`AST`] node, consisting of either an [`Expr`] or a [`Stmt`]. /// Exported under the `internals` feature only. /// @@ -859,7 +854,9 @@ pub enum ReturnType { /// This type is volatile and may change. #[derive(Debug, Clone, Hash)] pub enum ASTNode<'a> { + /// A statement ([`Stmt`]). Stmt(&'a Stmt), + /// An expression ([`Expr`]). Expr(&'a Expr), } @@ -875,7 +872,17 @@ impl<'a> From<&'a Expr> for ASTNode<'a> { } } -/// _(internals)_ A statements block. +impl ASTNode<'_> { + /// Get the [`Position`] of this [`ASTNode`]. + pub const fn position(&self) -> Position { + match self { + ASTNode::Stmt(stmt) => stmt.position(), + ASTNode::Expr(expr) => expr.position(), + } + } +} + +/// _(internals)_ A scoped block of statements. /// Exported under the `internals` feature only. /// /// # Volatile Data Structure @@ -904,18 +911,12 @@ impl StmtBlock { pub fn len(&self) -> usize { self.0.len() } - /// Get the position of this statements block. + /// Get the position (location of the beginning `{`) of this statements block. #[inline(always)] #[must_use] pub const fn position(&self) -> Position { self.1 } - /// Get the statements of this statements block. - #[inline(always)] - #[must_use] - pub fn statements_mut(&mut self) -> &mut StaticVec { - &mut self.0 - } } impl Deref for StmtBlock { @@ -956,7 +957,7 @@ impl From for Stmt { pub struct OptionFlags(u8); impl OptionFlags { - /// Does this [`BitOptions`] contain a particular option flag? + /// Does this [`OptionFlags`] contain a particular option flag? #[inline(always)] #[must_use] pub const fn contains(self, flag: Self) -> bool { @@ -967,15 +968,17 @@ impl OptionFlags { impl Not for OptionFlags { type Output = Self; + /// Return the negation of the [`OptionFlags`]. #[inline(always)] fn not(self) -> Self::Output { - Self(!self.0) + Self(!self.0) & AST_OPTION_FLAGS::AST_OPTION_ALL } } impl Add for OptionFlags { type Output = Self; + /// Return the union of two [`OptionFlags`]. #[inline(always)] fn add(self, rhs: Self) -> Self::Output { Self(self.0 | rhs.0) @@ -983,15 +986,35 @@ impl Add for OptionFlags { } impl AddAssign for OptionFlags { + /// Add the option flags in one [`OptionFlags`] to another. #[inline(always)] fn add_assign(&mut self, rhs: Self) { self.0 |= rhs.0 } } +impl BitOr for OptionFlags { + type Output = Self; + + /// Return the union of two [`OptionFlags`]. + #[inline(always)] + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} + +impl BitOrAssign for OptionFlags { + /// Add the option flags in one [`OptionFlags`] to another. + #[inline(always)] + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0 + } +} + impl Sub for OptionFlags { type Output = Self; + /// Return the difference of two [`OptionFlags`]. #[inline(always)] fn sub(self, rhs: Self) -> Self::Output { Self(self.0 & !rhs.0) @@ -999,12 +1022,31 @@ impl Sub for OptionFlags { } impl SubAssign for OptionFlags { + /// Remove the option flags in one [`OptionFlags`] from another. #[inline(always)] fn sub_assign(&mut self, rhs: Self) { self.0 &= !rhs.0 } } +impl BitAnd for OptionFlags { + type Output = Self; + + /// Return the intersection of two [`OptionFlags`]. + #[inline(always)] + fn bitand(self, rhs: Self) -> Self::Output { + Self(self.0 & !rhs.0) + } +} + +impl BitAndAssign for OptionFlags { + /// Keep only the intersection of one [`OptionFlags`] with another. + #[inline(always)] + fn bitand_assign(&mut self, rhs: Self) { + self.0 &= !rhs.0 + } +} + /// Option bit-flags for [`AST`] nodes. #[allow(non_snake_case)] pub mod AST_OPTION_FLAGS { @@ -1016,12 +1058,20 @@ pub mod AST_OPTION_FLAGS { /// _(internals)_ The [`AST`][crate::AST] node is constant. /// Exported under the `internals` feature only. pub const AST_OPTION_CONSTANT: OptionFlags = OptionFlags(0b0000_0001); - /// _(internals)_ The [`AST`][crate::AST] node is exported. + /// _(internals)_ The [`AST`][crate::AST] node is public. /// Exported under the `internals` feature only. - pub const AST_OPTION_EXPORTED: OptionFlags = OptionFlags(0b0000_0010); + pub const AST_OPTION_PUBLIC: OptionFlags = OptionFlags(0b0000_0010); /// _(internals)_ The [`AST`][crate::AST] node is in negated mode. /// Exported under the `internals` feature only. pub const AST_OPTION_NEGATED: OptionFlags = OptionFlags(0b0000_0100); + /// _(internals)_ The [`AST`][crate::AST] node breaks out of normal control flow. + /// Exported under the `internals` feature only. + pub const AST_OPTION_BREAK_OUT: OptionFlags = OptionFlags(0b0000_1000); + /// _(internals)_ Mask of all options. + /// Exported under the `internals` feature only. + pub(crate) const AST_OPTION_ALL: OptionFlags = OptionFlags( + AST_OPTION_CONSTANT.0 | AST_OPTION_PUBLIC.0 | AST_OPTION_NEGATED.0 | AST_OPTION_BREAK_OUT.0, + ); impl std::fmt::Debug for OptionFlags { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -1046,8 +1096,9 @@ pub mod AST_OPTION_FLAGS { f.write_str("(")?; write_option(self, f, num_flags, AST_OPTION_CONSTANT, "Constant")?; - write_option(self, f, num_flags, AST_OPTION_EXPORTED, "Exported")?; + write_option(self, f, num_flags, AST_OPTION_PUBLIC, "Public")?; write_option(self, f, num_flags, AST_OPTION_NEGATED, "Negated")?; + write_option(self, f, num_flags, AST_OPTION_BREAK_OUT, "Break")?; f.write_str(")")?; Ok(()) @@ -1081,8 +1132,8 @@ pub enum Stmt { /// /// ### Option Flags /// - /// * [`AST_FLAG_NONE`][AST_FLAGS::AST_FLAG_NONE] = `while` - /// * [`AST_FLAG_NEGATED`][AST_FLAGS::AST_FLAG_NEGATED] = `until` + /// * [`AST_OPTION_NONE`][AST_OPTION_FLAGS::AST_OPTION_NONE] = `while` + /// * [`AST_OPTION_NEGATED`][AST_OPTION_FLAGS::AST_OPTION_NEGATED] = `until` Do(Box, Expr, OptionFlags, Position), /// `for` `(` id `,` counter `)` `in` expr `{` stmt `}` For(Expr, Box<(Ident, Option, StmtBlock)>, Position), @@ -1090,8 +1141,8 @@ pub enum Stmt { /// /// ### Option Flags /// - /// * [`AST_FLAG_EXPORTED`][AST_FLAGS::AST_FLAG_EXPORTED] = `export` - /// * [`AST_FLAG_CONSTANT`][AST_FLAGS::AST_FLAG_CONSTANT] = `const` + /// * [`AST_OPTION_PUBLIC`][AST_OPTION_FLAGS::AST_OPTION_PUBLIC] = `export` + /// * [`AST_OPTION_CONSTANT`][AST_OPTION_FLAGS::AST_OPTION_CONSTANT] = `const` Var(Expr, Box, OptionFlags, Position), /// expr op`=` expr Assignment(Box<(Expr, Option>, Expr)>, Position), @@ -1106,12 +1157,20 @@ pub enum Stmt { TryCatch(Box<(StmtBlock, Option, StmtBlock)>, Position), /// [expression][Expr] Expr(Expr), - /// `continue` - Continue(Position), - /// `break` - Break(Position), + /// `continue`/`break` + /// + /// ### Option Flags + /// + /// * [`AST_OPTION_NONE`][AST_OPTION_FLAGS::AST_OPTION_NONE] = `continue` + /// * [`AST_OPTION_BREAK_OUT`][AST_OPTION_FLAGS::AST_OPTION_BREAK_OUT] = `break` + BreakLoop(OptionFlags, Position), /// `return`/`throw` - Return(ReturnType, Option, Position), + /// + /// ### Option Flags + /// + /// * [`AST_OPTION_NONE`][AST_OPTION_FLAGS::AST_OPTION_NONE] = `return` + /// * [`AST_OPTION_BREAK_OUT`][AST_OPTION_FLAGS::AST_OPTION_BREAK_OUT] = `throw` + Return(OptionFlags, Option, Position), /// `import` expr `as` var /// /// Not available under `no_module`. @@ -1125,6 +1184,11 @@ pub enum Stmt { /// Convert a variable to shared. /// /// Not available under `no_closure`. + /// + /// # Notes + /// + /// This variant does not map to any language structure. It is currently only used only to + /// convert a normal variable into a shared variable when the variable is _captured_ by a closure. #[cfg(not(feature = "no_closure"))] Share(Identifier), } @@ -1162,8 +1226,7 @@ impl Stmt { pub const fn position(&self) -> Position { match self { Self::Noop(pos) - | Self::Continue(pos) - | Self::Break(pos) + | Self::BreakLoop(_, pos) | Self::Block(_, pos) | Self::Assignment(_, pos) | Self::FnCall(_, pos) @@ -1191,8 +1254,7 @@ impl Stmt { pub fn set_position(&mut self, new_pos: Position) -> &mut Self { match self { Self::Noop(pos) - | Self::Continue(pos) - | Self::Break(pos) + | Self::BreakLoop(_, pos) | Self::Block(_, pos) | Self::Assignment(_, pos) | Self::FnCall(_, pos) @@ -1238,8 +1300,7 @@ impl Stmt { Self::Var(_, _, _, _) | Self::Assignment(_, _) - | Self::Continue(_) - | Self::Break(_) + | Self::BreakLoop(_, _) | Self::Return(_, _, _) => false, #[cfg(not(feature = "no_module"))] @@ -1270,8 +1331,7 @@ impl Stmt { | Self::Expr(_) | Self::FnCall(_, _) | Self::Do(_, _, _, _) - | Self::Continue(_) - | Self::Break(_) + | Self::BreakLoop(_, _) | Self::Return(_, _, _) => false, #[cfg(not(feature = "no_module"))] @@ -1320,7 +1380,7 @@ impl Stmt { Self::Var(_, _, _, _) | Self::Assignment(_, _) | Self::FnCall(_, _) => false, Self::Block(block, _) => block.iter().all(|stmt| stmt.is_pure()), - Self::Continue(_) | Self::Break(_) | Self::Return(_, _, _) => false, + Self::BreakLoop(_, _) | Self::Return(_, _, _) => false, Self::TryCatch(x, _) => { (x.0).0.iter().all(Stmt::is_pure) && (x.2).0.iter().all(Stmt::is_pure) } @@ -1338,8 +1398,8 @@ impl Stmt { /// /// 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. + /// Currently only variable declarations (i.e. `let` and `const`) and `import`/`export` + /// statements are internally pure. #[inline] #[must_use] pub fn is_internally_pure(&self) -> bool { @@ -1363,7 +1423,7 @@ impl Stmt { #[must_use] pub const fn is_control_flow_break(&self) -> bool { match self { - Self::Return(_, _, _) | Self::Break(_) | Self::Continue(_) => true, + Self::Return(_, _, _) | Self::BreakLoop(_, _) => true, _ => false, } } @@ -1506,7 +1566,8 @@ impl Stmt { pub struct CustomExpr { /// List of keywords. pub keywords: StaticVec, - /// Is the current [`Scope`][crate::Scope] modified? + /// Is the current [`Scope`][crate::Scope] possibly modified by this custom statement + /// (e.g. introducing a new variable)? pub scope_may_be_changed: bool, /// List of tokens actually parsed. pub tokens: StaticVec, @@ -1679,6 +1740,9 @@ pub struct FnCallExpr { /// List of function call argument expressions. pub args: StaticVec, /// List of function call arguments that are constants. + /// + /// Any arguments in `args` that is [`Expr::Stack`][Expr::Stack] indexes into this + /// array to find the constant for use as its argument value. pub constants: smallvec::SmallVec<[Dynamic; 2]>, /// Function name. pub name: Identifier, @@ -1879,6 +1943,12 @@ pub enum Expr { )>, ), /// Stack slot + /// + /// # Notes + /// + /// This variant does not map to any language structure. It is currently only used in function + /// calls with constant arguments where the `usize` number indexes into an array containing a + /// list of constant arguments for the function call. See [`FnCallExpr`] for more details. Stack(usize, Position), /// { [statement][Stmt] ... } Stmt(Box), @@ -2313,37 +2383,3 @@ impl Expr { true } } - -#[cfg(test)] -mod tests { - /// This test is to make sure no code changes increase the sizes of critical data structures. - #[test] - fn check_struct_sizes() { - use crate::*; - use std::mem::size_of; - - assert_eq!(size_of::(), 16); - assert_eq!(size_of::>(), 16); - #[cfg(not(feature = "no_position"))] - 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::(), - if cfg!(feature = "no_smartstring") { - 80 - } else { - 96 - } - ); - assert_eq!(size_of::(), 464); - assert_eq!(size_of::(), 56); - assert_eq!( - size_of::(), - if cfg!(feature = "no_position") { 8 } else { 16 } - ); - assert_eq!(size_of::(), 72); - } -} diff --git a/src/custom_syntax.rs b/src/custom_syntax.rs index 678afce3..eaee3c27 100644 --- a/src/custom_syntax.rs +++ b/src/custom_syntax.rs @@ -198,7 +198,7 @@ impl Engine { ) -> Result<&mut Self, ParseError> { use markers::*; - let mut segments: StaticVec = Default::default(); + let mut segments = StaticVec::::new(); for s in symbols { let s = s.as_ref().trim(); diff --git a/src/dynamic.rs b/src/dynamic.rs index a71ecca6..075ddab9 100644 --- a/src/dynamic.rs +++ b/src/dynamic.rs @@ -148,7 +148,8 @@ impl dyn Variant { } } -/// Modes of access. +/// _(internals)_ Modes of access. +/// Exported under the `internals` feature only. #[derive(Debug, Eq, PartialEq, Hash, Copy, Clone)] pub enum AccessMode { /// Mutable. diff --git a/src/engine.rs b/src/engine.rs index abe08f76..51391745 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,11 +1,12 @@ //! Main module defining the script evaluation [`Engine`]. -use crate::ast::{Expr, FnCallExpr, Ident, OpAssignment, ReturnType, Stmt, AST_OPTION_FLAGS::*}; +use crate::ast::{Expr, FnCallExpr, Ident, OpAssignment, Stmt, AST_OPTION_FLAGS::*}; use crate::custom_syntax::CustomSyntax; use crate::dynamic::{map_std_type_name, AccessMode, Union, Variant}; use crate::fn_hash::get_hasher; use crate::fn_native::{ - CallableFunction, IteratorFn, OnDebugCallback, OnPrintCallback, OnVarCallback, + CallableFunction, IteratorFn, OnDebugCallback, OnParseTokenCallback, OnPrintCallback, + OnVarCallback, }; use crate::module::NamespaceRef; use crate::optimize::OptimizationLevel; @@ -180,7 +181,7 @@ impl Imports { impl IntoIterator for Imports { type Item = (Identifier, Shared); type IntoIter = - Zip>, Rev; 4]>>>; + Zip>, Rev; 3]>>>; #[inline] fn into_iter(self) -> Self::IntoIter { @@ -662,18 +663,18 @@ pub struct EvalState { pub num_operations: u64, /// Number of modules loaded. pub num_modules: usize, + /// Stack of function resolution caches. + fn_resolution_caches: StaticVec, /// Embedded module resolver. #[cfg(not(feature = "no_module"))] pub embedded_module_resolver: Option>, - /// Stack of function resolution caches. - fn_resolution_caches: Vec, } impl EvalState { /// Create a new [`EvalState`]. #[inline(always)] #[must_use] - pub const fn new() -> Self { + pub fn new() -> Self { Self { source: None, always_search_scope: false, @@ -682,7 +683,7 @@ impl EvalState { num_modules: 0, #[cfg(not(feature = "no_module"))] embedded_module_resolver: None, - fn_resolution_caches: Vec::new(), + fn_resolution_caches: StaticVec::new(), } } /// Is the state currently at global (root) level? @@ -697,7 +698,7 @@ impl EvalState { pub fn fn_resolution_cache_mut(&mut self) -> &mut FnResolutionCache { if self.fn_resolution_caches.is_empty() { // Push a new function resolution cache if the stack is empty - self.fn_resolution_caches.push(Default::default()); + self.push_fn_resolution_cache(); } self.fn_resolution_caches .last_mut() @@ -713,7 +714,7 @@ impl EvalState { /// /// # Panics /// - /// Panics if there are no more function resolution cache in the stack. + /// Panics if there is no more function resolution cache in the stack. #[inline(always)] pub fn pop_fn_resolution_cache(&mut self) { self.fn_resolution_caches @@ -921,6 +922,8 @@ pub struct Engine { pub(crate) custom_syntax: BTreeMap>, /// Callback closure for resolving variable access. pub(crate) resolve_var: Option, + /// Callback closure to remap tokens during parsing. + pub(crate) token_mapper: Option>, /// Callback closure for implementing the `print` command. pub(crate) print: Option, @@ -1045,6 +1048,7 @@ impl Engine { custom_syntax: Default::default(), resolve_var: None, + token_mapper: None, print: None, debug: None, @@ -2704,11 +2708,10 @@ impl Engine { } } - // Continue statement - Stmt::Continue(pos) => EvalAltResult::LoopBreak(false, *pos).into(), - - // Break statement - Stmt::Break(pos) => EvalAltResult::LoopBreak(true, *pos).into(), + // Continue/Break statement + Stmt::BreakLoop(options, pos) => { + EvalAltResult::LoopBreak(options.contains(AST_OPTION_BREAK_OUT), *pos).into() + } // Namespace-qualified function call Stmt::FnCall(x, pos) if x.is_qualified() => { @@ -2767,7 +2770,7 @@ impl Engine { } #[cfg(not(feature = "no_object"))] _ => { - let mut err_map: Map = Default::default(); + let mut err_map = Map::new(); let err_pos = err.take_position(); err_map.insert("message".into(), err.to_string().into()); @@ -2828,8 +2831,23 @@ impl Engine { } } + // Throw value + Stmt::Return(options, Some(expr), pos) if options.contains(AST_OPTION_BREAK_OUT) => { + EvalAltResult::ErrorRuntime( + self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)? + .flatten(), + *pos, + ) + .into() + } + + // Empty throw + Stmt::Return(options, None, pos) if options.contains(AST_OPTION_BREAK_OUT) => { + EvalAltResult::ErrorRuntime(Dynamic::UNIT, *pos).into() + } + // Return value - Stmt::Return(ReturnType::Return, Some(expr), pos) => EvalAltResult::Return( + Stmt::Return(_, Some(expr), pos) => EvalAltResult::Return( self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)? .flatten(), *pos, @@ -2837,22 +2855,7 @@ impl Engine { .into(), // Empty return - Stmt::Return(ReturnType::Return, None, pos) => { - EvalAltResult::Return(Dynamic::UNIT, *pos).into() - } - - // Throw value - Stmt::Return(ReturnType::Exception, Some(expr), pos) => EvalAltResult::ErrorRuntime( - self.eval_expr(scope, mods, state, lib, this_ptr, expr, level)? - .flatten(), - *pos, - ) - .into(), - - // Empty throw - Stmt::Return(ReturnType::Exception, None, pos) => { - EvalAltResult::ErrorRuntime(Dynamic::UNIT, *pos).into() - } + Stmt::Return(_, None, pos) => EvalAltResult::Return(Dynamic::UNIT, *pos).into(), // Let/const statement Stmt::Var(expr, x, options, _) => { @@ -2862,7 +2865,7 @@ impl Engine { } else { AccessMode::ReadWrite }; - let export = options.contains(AST_OPTION_EXPORTED); + let export = options.contains(AST_OPTION_PUBLIC); let value = self .eval_expr(scope, mods, state, lib, this_ptr, expr, level)? diff --git a/src/engine_api.rs b/src/engine_api.rs index 18918215..ece66787 100644 --- a/src/engine_api.rs +++ b/src/engine_api.rs @@ -961,7 +961,7 @@ impl Engine { let remainder = iter.next().expect("name contains separator").trim(); if !root.contains_key(sub_module) { - let mut m: Module = Default::default(); + let mut m = Module::new(); register_static_module_raw(m.sub_modules_mut(), remainder, module); m.build_index(); root.insert(sub_module.into(), m.into()); @@ -1181,7 +1181,8 @@ impl Engine { scripts: &[&str], optimization_level: OptimizationLevel, ) -> Result { - let (stream, tokenizer_control) = self.lex_raw(scripts, None); + let (stream, tokenizer_control) = + self.lex_raw(scripts, self.token_mapper.as_ref().map(Box::as_ref)); let mut state = ParseState::new(self, tokenizer_control); self.parse( &mut stream.peekable(), @@ -1338,6 +1339,7 @@ impl Engine { /// # } /// ``` #[cfg(not(feature = "no_object"))] + #[inline(always)] pub fn parse_json( &self, json: impl AsRef, @@ -1345,52 +1347,51 @@ impl Engine { ) -> Result> { use crate::token::Token; - let json = json.as_ref(); - let mut scope = Default::default(); - - // Trims the JSON string and add a '#' in front - let json_text = json.trim_start(); - let scripts = if json_text.starts_with(Token::MapStart.literal_syntax()) { - [json_text, ""] - } else if json_text.starts_with(Token::LeftBrace.literal_syntax()) { - ["#", json_text] - } else { - return Err(crate::ParseErrorType::MissingToken( - Token::LeftBrace.syntax().into(), - "to start a JSON object hash".into(), - ) - .into_err(Position::new(1, (json.len() - json_text.len() + 1) as u16)) - .into()); - }; - - let (stream, tokenizer_control) = self.lex_raw( - &scripts, - Some(if has_null { - |token| match token { - // If `null` is present, make sure `null` is treated as a variable - Token::Reserved(s) if s == "null" => Token::Identifier(s), - _ => token, - } + fn parse_json_inner( + engine: &Engine, + json: &str, + has_null: bool, + ) -> Result> { + let mut scope = Scope::new(); + let json_text = json.trim_start(); + let scripts = if json_text.starts_with(Token::MapStart.literal_syntax()) { + [json_text, ""] + } else if json_text.starts_with(Token::LeftBrace.literal_syntax()) { + ["#", json_text] } else { - |t| t - }), - ); - - let mut state = ParseState::new(self, tokenizer_control); - - let ast = self.parse_global_expr( - &mut stream.peekable(), - &mut state, - &scope, - OptimizationLevel::None, - )?; - - // Handle null - map to () - if has_null { - scope.push_constant("null", ()); + return Err(crate::ParseErrorType::MissingToken( + Token::LeftBrace.syntax().into(), + "to start a JSON object hash".into(), + ) + .into_err(Position::new(1, (json.len() - json_text.len() + 1) as u16)) + .into()); + }; + let (stream, tokenizer_control) = engine.lex_raw( + &scripts, + Some(if has_null { + &|token| match token { + // If `null` is present, make sure `null` is treated as a variable + Token::Reserved(s) if s == "null" => Token::Identifier(s), + _ => token, + } + } else { + &|t| t + }), + ); + let mut state = ParseState::new(engine, tokenizer_control); + let ast = engine.parse_global_expr( + &mut stream.peekable(), + &mut state, + &scope, + OptimizationLevel::None, + )?; + if has_null { + scope.push_constant("null", ()); + } + engine.eval_ast_with_scope(&mut scope, &ast) } - self.eval_ast_with_scope(&mut scope, &ast) + parse_json_inner(self, json.as_ref(), has_null) } /// Compile a string containing an expression into an [`AST`], /// which can be used later for evaluation. @@ -1462,7 +1463,8 @@ impl Engine { script: &str, ) -> Result { let scripts = [script]; - let (stream, tokenizer_control) = self.lex_raw(&scripts, None); + let (stream, tokenizer_control) = + self.lex_raw(&scripts, self.token_mapper.as_ref().map(Box::as_ref)); let mut peekable = stream.peekable(); let mut state = ParseState::new(self, tokenizer_control); @@ -1624,7 +1626,8 @@ impl Engine { script: &str, ) -> Result> { let scripts = [script]; - let (stream, tokenizer_control) = self.lex_raw(&scripts, None); + let (stream, tokenizer_control) = + self.lex_raw(&scripts, self.token_mapper.as_ref().map(Box::as_ref)); let mut state = ParseState::new(self, tokenizer_control); // No need to optimize a lone expression @@ -1769,7 +1772,8 @@ impl Engine { script: &str, ) -> Result<(), Box> { let scripts = [script]; - let (stream, tokenizer_control) = self.lex_raw(&scripts, None); + let (stream, tokenizer_control) = + self.lex_raw(&scripts, self.token_mapper.as_ref().map(Box::as_ref)); let mut state = ParseState::new(self, tokenizer_control); let ast = self.parse( @@ -1860,7 +1864,7 @@ impl Engine { name: impl AsRef, args: impl crate::FuncArgs, ) -> Result> { - let mut arg_values: crate::StaticVec<_> = Default::default(); + let mut arg_values = crate::StaticVec::new(); args.parse(&mut arg_values); let mut args: crate::StaticVec<_> = arg_values.iter_mut().collect(); let name = name.as_ref(); @@ -2040,7 +2044,7 @@ impl Engine { let lib = Default::default(); let stmt = std::mem::take(ast.statements_mut()); - crate::optimize::optimize_into_ast(self, scope, stmt.into_vec(), lib, optimization_level) + crate::optimize::optimize_into_ast(self, scope, stmt, lib, optimization_level) } /// _(metadata)_ Generate a list of all registered functions. /// Exported under the `metadata` feature only. @@ -2053,7 +2057,7 @@ impl Engine { #[inline] #[must_use] pub fn gen_fn_signatures(&self, include_packages: bool) -> Vec { - let mut signatures: Vec<_> = Default::default(); + let mut signatures = Vec::with_capacity(64); signatures.extend(self.global_namespace().gen_fn_signatures()); @@ -2114,6 +2118,46 @@ impl Engine { self.resolve_var = Some(Box::new(callback)); self } + /// _(internals)_ Provide a callback that will be invoked during parsing to remap certain tokens. + /// Exported under the `internals` feature only. + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), Box> { + /// use rhai::{Engine, Token}; + /// + /// let mut engine = Engine::new(); + /// + /// // Register a token mapper. + /// engine.on_parse_token(|token| { + /// match token { + /// // Convert all integer literals to strings + /// Token::IntegerConstant(n) => Token::StringConstant(n.to_string()), + /// // Convert 'begin' .. 'end' to '{' .. '}' + /// Token::Identifier(s) if &s == "begin" => Token::LeftBrace, + /// Token::Identifier(s) if &s == "end" => Token::RightBrace, + /// // Pass through all other tokens unchanged + /// _ => token + /// } + /// }); + /// + /// assert_eq!(engine.eval::("42")?, "42"); + /// assert_eq!(engine.eval::("true")?, true); + /// assert_eq!(engine.eval::("let x = 42; begin let x = 0; end; x")?, "42"); + /// + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "internals")] + #[inline(always)] + pub fn on_parse_token( + &mut self, + callback: impl Fn(crate::token::Token) -> crate::token::Token + SendSync + 'static, + ) -> &mut Self { + self.token_mapper = Some(Box::new(callback)); + self + } /// Register a callback for script evaluation progress. /// /// Not available under `unchecked`. diff --git a/src/fn_native.rs b/src/fn_native.rs index cf7b9d1c..60f46274 100644 --- a/src/fn_native.rs +++ b/src/fn_native.rs @@ -4,6 +4,7 @@ use crate::ast::{FnAccess, FnCallHashes}; use crate::engine::Imports; use crate::fn_call::FnCallArgs; use crate::plugin::PluginFunction; +use crate::token::Token; use crate::{ calc_fn_hash, Dynamic, Engine, EvalAltResult, EvalContext, Module, Position, RhaiResult, }; @@ -192,39 +193,46 @@ impl<'a> NativeCallContext<'a> { /// /// If `is_method` is [`true`], the first argument is assumed to be passed /// by reference and is not consumed. - #[inline] + #[inline(always)] pub fn call_fn_dynamic_raw( &self, fn_name: impl AsRef, is_method_call: bool, args: &mut [&mut Dynamic], ) -> RhaiResult { - let fn_name = fn_name.as_ref(); + fn call_fn_dynamic_inner( + context: &NativeCallContext, + is_method_call: bool, + fn_name: &str, + args: &mut [&mut Dynamic], + ) -> Result> { + let hash = if is_method_call { + FnCallHashes::from_script_and_native( + calc_fn_hash(fn_name, args.len() - 1), + calc_fn_hash(fn_name, args.len()), + ) + } else { + FnCallHashes::from_script(calc_fn_hash(fn_name, args.len())) + }; + context + .engine() + .exec_fn_call( + &mut context.mods.cloned().unwrap_or_default(), + &mut Default::default(), + context.lib, + fn_name, + hash, + args, + is_method_call, + is_method_call, + Position::NONE, + None, + 0, + ) + .map(|(r, _)| r) + } - let hash = if is_method_call { - FnCallHashes::from_script_and_native( - calc_fn_hash(fn_name, args.len() - 1), - calc_fn_hash(fn_name, args.len()), - ) - } else { - FnCallHashes::from_script(calc_fn_hash(fn_name, args.len())) - }; - - self.engine() - .exec_fn_call( - &mut self.mods.cloned().unwrap_or_default(), - &mut Default::default(), - self.lib, - fn_name, - hash, - args, - is_method_call, - is_method_call, - Position::NONE, - None, - 0, - ) - .map(|(r, _)| r) + call_fn_dynamic_inner(self, is_method_call, fn_name.as_ref(), args) } } @@ -300,6 +308,13 @@ pub type OnDebugCallback = Box, Position) + 'static>; #[cfg(feature = "sync")] pub type OnDebugCallback = Box, Position) + Send + Sync + 'static>; +/// A standard callback function for mapping tokens during parsing. +#[cfg(not(feature = "sync"))] +pub type OnParseTokenCallback = dyn Fn(Token) -> Token; +/// A standard callback function for mapping tokens during parsing. +#[cfg(feature = "sync")] +pub type OnParseTokenCallback = dyn Fn(Token) -> Token + Send + Sync + 'static; + /// A standard callback function for variable access. #[cfg(not(feature = "sync"))] pub type OnVarCallback = diff --git a/src/lib.rs b/src/lib.rs index 1466bbb7..bb8e1ee9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,6 +93,7 @@ pub mod packages; mod parse; pub mod plugin; mod scope; +mod tests; mod token; mod r#unsafe; @@ -213,7 +214,7 @@ pub use optimize::OptimizationLevel; #[cfg(feature = "internals")] #[deprecated = "this type is volatile and may change"] -pub use dynamic::{DynamicReadLock, DynamicWriteLock, Variant}; +pub use dynamic::{AccessMode, DynamicReadLock, DynamicWriteLock, Variant}; // Expose internal data structures. #[cfg(feature = "internals")] @@ -223,15 +224,27 @@ pub use token::{get_next_token, parse_string_literal}; // Expose internal data structures. #[cfg(feature = "internals")] #[deprecated = "this type is volatile and may change"] -pub use token::{InputStream, Token, TokenizeState, TokenizerControl, TokenizerControlBlock}; +pub use token::{ + InputStream, MultiInputsStream, Token, TokenIterator, TokenizeState, TokenizerControl, + TokenizerControlBlock, +}; + +#[cfg(feature = "internals")] +#[deprecated = "this type is volatile and may change"] +pub use parse::{IdentifierBuilder, ParseState}; #[cfg(feature = "internals")] #[deprecated = "this type is volatile and may change"] pub use ast::{ - ASTNode, BinaryExpr, CustomExpr, Expr, FloatWrapper, FnCallExpr, FnCallHashes, Ident, - OpAssignment, OptionFlags, ReturnType, ScriptFnDef, Stmt, StmtBlock, AST_OPTION_FLAGS::*, + ASTNode, BinaryExpr, CustomExpr, Expr, FnCallExpr, FnCallHashes, Ident, OpAssignment, + OptionFlags, ScriptFnDef, Stmt, StmtBlock, AST_OPTION_FLAGS::*, }; +#[cfg(feature = "internals")] +#[cfg(not(feature = "no_float"))] +#[deprecated = "this type is volatile and may change"] +pub use ast::FloatWrapper; + #[cfg(feature = "internals")] #[deprecated = "this type is volatile and may change"] pub use engine::{EvalState, FnResolutionCache, FnResolutionCacheEntry, Imports}; @@ -245,8 +258,8 @@ pub use engine::Limits; #[deprecated = "this type is volatile and may change"] pub use module::NamespaceRef; -/// Alias to [`smallvec::SmallVec<[T; 4]>`](https://crates.io/crates/smallvec), which is a -/// specialized [`Vec`] backed by a small, inline, fixed-size array when there are ≤ 4 items stored. +/// Alias to [`smallvec::SmallVec<[T; 3]>`](https://crates.io/crates/smallvec), which is a +/// specialized [`Vec`] backed by a small, inline, fixed-size array when there are ≤ 3 items stored. /// /// # History /// @@ -257,30 +270,30 @@ pub use module::NamespaceRef; /// and orangutans and breakfast cereals and fruit bats and large chu... /// /// And the Lord spake, saying, "First shalt thou depend on the [`smallvec`](https://crates.io/crates/smallvec) crate. -/// Then, shalt thou keep four inline. No more. No less. Four shalt be the number thou shalt keep inline, -/// and the number to keep inline shalt be four. Five shalt thou not keep inline, nor either keep inline -/// thou two or three, excepting that thou then proceed to four. Six is right out. Once the number four, -/// being the forth number, be reached, then, lobbest thou thy `SmallVec` towards thy heap, who, +/// Then, shalt thou keep three inline. No more. No less. Three shalt be the number thou shalt keep inline, +/// and the number to keep inline shalt be three. Four shalt thou not keep inline, nor either keep inline +/// thou two, excepting that thou then proceed to three. Five is right out. Once the number three, +/// being the third number, be reached, then, lobbest thou thy `SmallVec` towards thy heap, who, /// being slow and cache-naughty in My sight, shall snuff it." /// -/// # Explanation on the Number Four +/// # Why Three /// /// `StaticVec` is used frequently to keep small lists of items in inline (non-heap) storage in /// order to improve cache friendliness and reduce indirections. /// -/// The number 4, other than being the holy number, is carefully chosen for a balance between +/// The number 3, other than being the holy number, is carefully chosen for a balance between /// storage space and reduce allocations. That is because most function calls (and most functions, -/// in that matter) contain fewer than 5 arguments, the exception being closures that capture a +/// for that matter) contain fewer than 4 arguments, the exception being closures that capture a /// large number of external variables. /// -/// In addition, most script blocks either contain many statements, or just a few lines; -/// most scripts load fewer than 5 external modules; most module paths contain fewer than 5 levels -/// (e.g. `std::collections::map::HashMap` is 4 levels, and that's already quite long). +/// In addition, most script blocks either contain many statements, or just one or two lines; +/// most scripts load fewer than 4 external modules; most module paths contain fewer than 4 levels +/// (e.g. `std::collections::map::HashMap` is 4 levels and it is just about as long as they get). #[cfg(not(feature = "internals"))] -type StaticVec = smallvec::SmallVec<[T; 4]>; +type StaticVec = smallvec::SmallVec<[T; 3]>; -/// _(internals)_ Alias to [`smallvec`](https://crates.io/crates/smallvec), which is a specialized -/// [`Vec`] backed by a small, inline, fixed-size array when there are ≤ 4 items stored. +/// _(internals)_ Alias to [`smallvec::SmallVec<[T; 3]>`](https://crates.io/crates/smallvec), +/// which is a [`Vec`] backed by a small, inline, fixed-size array when there are ≤ 3 items stored. /// Exported under the `internals` feature only. /// /// # History @@ -292,30 +305,30 @@ type StaticVec = smallvec::SmallVec<[T; 4]>; /// and orangutans and breakfast cereals and fruit bats and large chu... /// /// And the Lord spake, saying, "First shalt thou depend on the [`smallvec`](https://crates.io/crates/smallvec) crate. -/// Then, shalt thou keep four inline. No more. No less. Four shalt be the number thou shalt keep inline, -/// and the number to keep inline shalt be four. Five shalt thou not keep inline, nor either keep inline -/// thou two or three, excepting that thou then proceed to four. Six is right out. Once the number four, -/// being the forth number, be reached, then, lobbest thou thy `SmallVec` towards thy heap, who, +/// Then, shalt thou keep three inline. No more. No less. Three shalt be the number thou shalt keep inline, +/// and the number to keep inline shalt be three. Four shalt thou not keep inline, nor either keep inline +/// thou two, excepting that thou then proceed to three. Five is right out. Once the number three, +/// being the third number, be reached, then, lobbest thou thy `SmallVec` towards thy heap, who, /// being slow and cache-naughty in My sight, shall snuff it." /// -/// # Explanation on the Number Four +/// # Why Three /// /// `StaticVec` is used frequently to keep small lists of items in inline (non-heap) storage in /// order to improve cache friendliness and reduce indirections. /// -/// The number 4, other than being the holy number, is carefully chosen for a balance between +/// The number 3, other than being the holy number, is carefully chosen for a balance between /// storage space and reduce allocations. That is because most function calls (and most functions, -/// in that matter) contain fewer than 5 arguments, the exception being closures that capture a +/// for that matter) contain fewer than 4 arguments, the exception being closures that capture a /// large number of external variables. /// -/// In addition, most script blocks either contain many statements, or just a few lines; -/// most scripts load fewer than 5 external modules; most module paths contain fewer than 5 levels -/// (e.g. `std::collections::map::HashMap` is 4 levels, and that's already quite long). +/// In addition, most script blocks either contain many statements, or just one or two lines; +/// most scripts load fewer than 4 external modules; most module paths contain fewer than 4 levels +/// (e.g. `std::collections::map::HashMap` is 4 levels and it is just about as long as they get). #[cfg(feature = "internals")] -pub type StaticVec = smallvec::SmallVec<[T; 4]>; +pub type StaticVec = smallvec::SmallVec<[T; 3]>; #[cfg(not(feature = "no_smartstring"))] -pub(crate) type SmartString = smartstring::SmartString; +pub(crate) type SmartString = smartstring::SmartString; #[cfg(feature = "no_smartstring")] pub(crate) type SmartString = String; @@ -324,24 +337,32 @@ pub(crate) type SmartString = String; #[cfg(feature = "no_float")] #[cfg(feature = "f32_float")] -compile_error!("'f32_float' cannot be used with 'no_float'"); +compile_error!("`f32_float` cannot be used with `no_float`"); + +#[cfg(feature = "only_i32")] +#[cfg(feature = "only_i64")] +compile_error!("`only_i32` and `only_i64` cannot be used together"); #[cfg(feature = "no_std")] #[cfg(feature = "wasm-bindgen")] -compile_error!("'wasm-bindgen' cannot be used with 'no-std'"); +compile_error!("`wasm-bindgen` cannot be used with `no-std`"); #[cfg(feature = "no_std")] #[cfg(feature = "stdweb")] -compile_error!("'stdweb' cannot be used with 'no-std'"); +compile_error!("`stdweb` cannot be used with `no-std`"); #[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))] #[cfg(feature = "no_std")] -compile_error!("'no_std' cannot be used for WASM target"); +compile_error!("`no_std` cannot be used for WASM target"); #[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))] #[cfg(feature = "wasm-bindgen")] -compile_error!("'wasm-bindgen' should not be used non-WASM target"); +compile_error!("`wasm-bindgen` cannot be used for non-WASM target"); #[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))] #[cfg(feature = "stdweb")] -compile_error!("'stdweb' should not be used non-WASM target"); +compile_error!("`stdweb` cannot be used non-WASM target"); + +#[cfg(feature = "wasm-bindgen")] +#[cfg(feature = "stdweb")] +compile_error!("`wasm-bindgen` and `stdweb` cannot be used together"); diff --git a/src/module/mod.rs b/src/module/mod.rs index f02391d7..baa9c7fb 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -1415,7 +1415,7 @@ impl Module { ast: &crate::AST, engine: &crate::Engine, ) -> Result> { - let mut mods: crate::engine::Imports = Default::default(); + let mut mods = crate::engine::Imports::new(); let orig_mods_len = mods.len(); // Run the script @@ -1439,7 +1439,7 @@ impl Module { }); // Extra modules left in the scope become sub-modules - let mut func_mods: crate::engine::Imports = Default::default(); + let mut func_mods = crate::engine::Imports::new(); mods.into_iter().skip(orig_mods_len).for_each(|(alias, m)| { func_mods.push(alias.clone(), m.clone()); @@ -1720,7 +1720,7 @@ impl NamespaceRef { /// Create a new [`NamespaceRef`]. #[inline(always)] #[must_use] - pub fn new(&self) -> Self { + pub fn new() -> Self { Self { index: None, path: StaticVec::new(), diff --git a/src/optimize.rs b/src/optimize.rs index 32c90183..616f0db5 100644 --- a/src/optimize.rs +++ b/src/optimize.rs @@ -16,6 +16,7 @@ use std::{ any::TypeId, hash::{Hash, Hasher}, mem, + ops::DerefMut, }; #[cfg(not(feature = "no_closure"))] @@ -50,7 +51,7 @@ struct OptimizerState<'a> { /// Has the [`AST`] been changed during this pass? changed: bool, /// Collection of constants to use for eager function evaluations. - variables: Vec<(String, AccessMode, Option)>, + variables: StaticVec<(String, AccessMode, Option)>, /// Activate constants propagation? propagate_constants: bool, /// An [`Engine`] instance for eager function evaluation. @@ -64,14 +65,14 @@ struct OptimizerState<'a> { impl<'a> OptimizerState<'a> { /// Create a new State. #[inline(always)] - pub const fn new( + pub fn new( engine: &'a Engine, lib: &'a [&'a Module], optimization_level: OptimizationLevel, ) -> Self { Self { changed: false, - variables: Vec::new(), + variables: StaticVec::new(), propagate_constants: true, engine, lib, @@ -157,12 +158,12 @@ impl<'a> OptimizerState<'a> { /// Optimize a block of [statements][Stmt]. fn optimize_stmt_block( - mut statements: Vec, + mut statements: StaticVec, state: &mut OptimizerState, preserve_result: bool, is_internal: bool, reduce_return: bool, -) -> Vec { +) -> StaticVec { if statements.is_empty() { return statements; } @@ -267,7 +268,9 @@ fn optimize_stmt_block( loop { match statements[..] { // { return; } -> {} - [Stmt::Return(crate::ast::ReturnType::Return, None, _)] if reduce_return => { + [Stmt::Return(options, None, _)] + if reduce_return && !options.contains(AST_OPTION_BREAK_OUT) => + { state.set_dirty(); statements.clear(); } @@ -276,8 +279,10 @@ fn optimize_stmt_block( statements.clear(); } // { ...; return; } -> { ... } - [.., ref last_stmt, Stmt::Return(crate::ast::ReturnType::Return, None, _)] - if reduce_return && !last_stmt.returns_value() => + [.., ref last_stmt, Stmt::Return(options, None, _)] + if reduce_return + && !options.contains(AST_OPTION_BREAK_OUT) + && !last_stmt.returns_value() => { state.set_dirty(); statements @@ -285,8 +290,8 @@ fn optimize_stmt_block( .expect("`statements` contains at least two elements"); } // { ...; return val; } -> { ...; val } - [.., Stmt::Return(crate::ast::ReturnType::Return, ref mut expr, pos)] - if reduce_return => + [.., Stmt::Return(options, ref mut expr, pos)] + if reduce_return && !options.contains(AST_OPTION_BREAK_OUT) => { state.set_dirty(); *statements @@ -332,8 +337,8 @@ fn optimize_stmt_block( statements.clear(); } // { ...; return; } -> { ... } - [.., Stmt::Return(crate::ast::ReturnType::Return, None, _)] - if reduce_return => + [.., Stmt::Return(options, None, _)] + if reduce_return && !options.contains(AST_OPTION_BREAK_OUT) => { state.set_dirty(); statements @@ -341,8 +346,10 @@ fn optimize_stmt_block( .expect("`statements` contains at least two elements"); } // { ...; return pure_val; } -> { ... } - [.., Stmt::Return(crate::ast::ReturnType::Return, Some(ref expr), _)] - if reduce_return && expr.is_pure() => + [.., Stmt::Return(options, Some(ref expr), _)] + if reduce_return + && !options.contains(AST_OPTION_BREAK_OUT) + && expr.is_pure() => { state.set_dirty(); statements @@ -449,7 +456,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // if false { if_block } else { else_block } -> else_block Stmt::If(Expr::BoolConstant(false, _), x, _) => { state.set_dirty(); - let else_block = mem::take(&mut *x.1).into_vec(); + let else_block = mem::take(&mut *x.1); *stmt = match optimize_stmt_block(else_block, state, preserve_result, true, false) { statements if statements.is_empty() => Stmt::Noop(x.1.position()), statements => Stmt::Block(statements.into_boxed_slice(), x.1.position()), @@ -458,7 +465,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // if true { if_block } else { else_block } -> if_block Stmt::If(Expr::BoolConstant(true, _), x, _) => { state.set_dirty(); - let if_block = mem::take(&mut *x.0).into_vec(); + let if_block = mem::take(&mut *x.0); *stmt = match optimize_stmt_block(if_block, state, preserve_result, true, false) { statements if statements.is_empty() => Stmt::Noop(x.0.position()), statements => Stmt::Block(statements.into_boxed_slice(), x.0.position()), @@ -467,12 +474,10 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // if expr { if_block } else { else_block } Stmt::If(condition, x, _) => { optimize_expr(condition, state, false); - let if_block = mem::take(x.0.statements_mut()).into_vec(); - *x.0.statements_mut() = - optimize_stmt_block(if_block, state, preserve_result, true, false).into(); - let else_block = mem::take(x.1.statements_mut()).into_vec(); - *x.1.statements_mut() = - optimize_stmt_block(else_block, state, preserve_result, true, false).into(); + let if_block = mem::take(x.0.deref_mut()); + *x.0 = optimize_stmt_block(if_block, state, preserve_result, true, false); + let else_block = mem::take(x.1.deref_mut()); + *x.1 = optimize_stmt_block(else_block, state, preserve_result, true, false); } // switch const { ... } @@ -492,7 +497,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // switch const { case if condition => stmt, _ => def } => if condition { stmt } else { def } optimize_expr(&mut condition, state, false); - let def_block = mem::take(&mut *x.1).into_vec(); + let def_block = mem::take(&mut *x.1); let def_stmt = optimize_stmt_block(def_block, state, true, true, false); let def_pos = if x.1.position().is_none() { *pos @@ -512,13 +517,12 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // Promote the matched case let new_pos = block.1.position(); let statements = mem::take(&mut *block.1); - let statements = - optimize_stmt_block(statements.into_vec(), state, true, true, false); + let statements = optimize_stmt_block(statements, state, true, true, false); *stmt = Stmt::Block(statements.into_boxed_slice(), new_pos); } } else { // Promote the default case - let def_block = mem::take(&mut *x.1).into_vec(); + let def_block = mem::take(&mut *x.1); let def_stmt = optimize_stmt_block(def_block, state, true, true, false); let def_pos = if x.1.position().is_none() { *pos @@ -545,14 +549,13 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b _ => { block.0 = Some(condition); - *block.1.statements_mut() = optimize_stmt_block( - mem::take(block.1.statements_mut()).into_vec(), + *block.1 = optimize_stmt_block( + mem::take(block.1.deref_mut()), state, preserve_result, true, false, - ) - .into(); + ); } } }); @@ -566,9 +569,8 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b x.0.remove(&key); } - let def_block = mem::take(x.1.statements_mut()).into_vec(); - *x.1.statements_mut() = - optimize_stmt_block(def_block, state, preserve_result, true, false).into(); + let def_block = mem::take(x.1.deref_mut()); + *x.1 = optimize_stmt_block(def_block, state, preserve_result, true, false); } // while false { block } -> Noop @@ -582,13 +584,13 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b if let Expr::BoolConstant(true, pos) = condition { *condition = Expr::Unit(*pos); } - let block = mem::take(body.statements_mut()).into_vec(); - *body.statements_mut() = optimize_stmt_block(block, state, false, true, false).into(); + let block = mem::take(body.as_mut().deref_mut()); + *body.as_mut().deref_mut() = optimize_stmt_block(block, state, false, true, false); if body.len() == 1 { match body[0] { // while expr { break; } -> { expr; } - Stmt::Break(pos) => { + Stmt::BreakLoop(options, pos) if options.contains(AST_OPTION_BREAK_OUT) => { // Only a single break statement - turn into running the guard expression once state.set_dirty(); if !condition.is_unit() { @@ -611,7 +613,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b { state.set_dirty(); let block_pos = body.position(); - let block = mem::take(body.statements_mut()).into_vec(); + let block = mem::take(body.as_mut().deref_mut()); *stmt = Stmt::Block( optimize_stmt_block(block, state, false, true, false).into_boxed_slice(), block_pos, @@ -620,14 +622,14 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // do { block } while|until expr Stmt::Do(body, condition, _, _) => { optimize_expr(condition, state, false); - let block = mem::take(body.statements_mut()).into_vec(); - *body.statements_mut() = optimize_stmt_block(block, state, false, true, false).into(); + let block = mem::take(body.as_mut().deref_mut()); + *body.as_mut().deref_mut() = optimize_stmt_block(block, state, false, true, false); } // for id in expr { block } Stmt::For(iterable, x, _) => { optimize_expr(iterable, state, false); - let body = mem::take(x.2.statements_mut()).into_vec(); - *x.2.statements_mut() = optimize_stmt_block(body, state, false, true, false).into(); + let body = mem::take(x.2.deref_mut()); + *x.2 = optimize_stmt_block(body, state, false, true, false); } // let id = expr; Stmt::Var(expr, _, options, _) if !options.contains(AST_OPTION_CONSTANT) => { @@ -638,7 +640,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b Stmt::Import(expr, _, _) => optimize_expr(expr, state, false), // { block } Stmt::Block(statements, pos) => { - let statements = mem::take(statements).into_vec(); + let statements = mem::take(statements).into_vec().into(); let mut block = optimize_stmt_block(statements, state, preserve_result, true, false); match block.as_mut_slice() { @@ -659,7 +661,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // If try block is pure, there will never be any exceptions state.set_dirty(); let try_pos = x.0.position(); - let try_block = mem::take(&mut *x.0).into_vec(); + let try_block = mem::take(&mut *x.0); *stmt = Stmt::Block( optimize_stmt_block(try_block, state, false, true, false).into_boxed_slice(), try_pos, @@ -667,12 +669,10 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b } // try { try_block } catch ( var ) { catch_block } Stmt::TryCatch(x, _) => { - let try_block = mem::take(x.0.statements_mut()).into_vec(); - *x.0.statements_mut() = - optimize_stmt_block(try_block, state, false, true, false).into(); - let catch_block = mem::take(x.2.statements_mut()).into_vec(); - *x.2.statements_mut() = - optimize_stmt_block(catch_block, state, false, true, false).into(); + let try_block = mem::take(x.0.deref_mut()); + *x.0 = optimize_stmt_block(try_block, state, false, true, false); + let catch_block = mem::take(x.2.deref_mut()); + *x.2 = optimize_stmt_block(catch_block, state, false, true, false); } // func(...) Stmt::Expr(expr @ Expr::FnCall(_, _)) => { @@ -721,7 +721,8 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, chaining: bool) { Expr::Stmt(x) if x.is_empty() => { state.set_dirty(); *expr = Expr::Unit(x.position()) } // { stmt; ... } - do not count promotion as dirty because it gets turned back into an array Expr::Stmt(x) => { - *x.statements_mut() = optimize_stmt_block(mem::take(x.statements_mut()).into_vec(), state, true, true, false).into(); + *x.as_mut().deref_mut() = + optimize_stmt_block(mem::take(x.as_mut().deref_mut()), state, true, true, false); // { Stmt(Expr) } - promote match x.as_mut().as_mut() { @@ -830,6 +831,8 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, chaining: bool) { } // `... ${ ... } ...` Expr::InterpolatedString(x, _) => { + x.iter_mut().for_each(|expr| optimize_expr(expr, state, false)); + let mut n = 0; // Merge consecutive strings @@ -1086,12 +1089,12 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, chaining: bool) { /// Optimize a block of [statements][Stmt] at top level. fn optimize_top_level( - statements: Vec, + statements: StaticVec, engine: &Engine, scope: &Scope, lib: &[&Module], optimization_level: OptimizationLevel, -) -> Vec { +) -> StaticVec { let mut statements = statements; // If optimization level is None then skip optimizing @@ -1120,8 +1123,8 @@ fn optimize_top_level( pub fn optimize_into_ast( engine: &Engine, scope: &Scope, - statements: Vec, - functions: Vec>, + statements: StaticVec, + functions: StaticVec>, optimization_level: OptimizationLevel, ) -> AST { let level = if cfg!(feature = "no_optimize") { @@ -1171,10 +1174,9 @@ pub fn optimize_into_ast( // Optimize the function body let state = &mut OptimizerState::new(engine, lib2, level); - let body = mem::take(fn_def.body.statements_mut()).into_vec(); + let body = mem::take(fn_def.body.deref_mut()); - *fn_def.body.statements_mut() = - optimize_stmt_block(body, state, true, true, true).into(); + *fn_def.body = optimize_stmt_block(body, state, true, true, true); fn_def }) diff --git a/src/packages/arithmetic.rs b/src/packages/arithmetic.rs index 074b19cf..66c05978 100644 --- a/src/packages/arithmetic.rs +++ b/src/packages/arithmetic.rs @@ -163,13 +163,7 @@ macro_rules! gen_signed_functions { } } pub fn sign(x: $arg_type) -> INT { - if x == 0 { - 0 - } else if x < 0 { - -1 - } else { - 1 - } + x.signum() as INT } } })* } @@ -249,6 +243,8 @@ gen_signed_functions!(signed_num_128 => i128); #[cfg(not(feature = "no_float"))] #[export_module] mod f32_functions { + use crate::EvalAltResult; + #[cfg(not(feature = "f32_float"))] pub mod basic_arithmetic { #[rhai_fn(name = "+")] @@ -329,13 +325,15 @@ mod f32_functions { pub fn abs(x: f32) -> f32 { x.abs() } - pub fn sign(x: f32) -> INT { + #[rhai_fn(return_raw)] + pub fn sign(x: f32) -> Result> { if x == 0.0 { - 0 - } else if x < 0.0 { - -1 + Ok(0) } else { - 1 + match x.signum() { + x if x.is_nan() => Err(make_err("Sign of NaN is undefined")), + x => Ok(x as INT), + } } } pub fn is_zero(x: f32) -> bool { @@ -437,13 +435,15 @@ mod f64_functions { pub fn abs(x: f64) -> f64 { x.abs() } - pub fn sign(x: f64) -> INT { + #[rhai_fn(return_raw)] + pub fn sign(x: f64) -> Result> { if x == 0.0 { - 0 - } else if x < 0.0 { - -1 + Ok(0) } else { - 1 + match x.signum() { + x if x.is_nan() => Err(make_err("Sign of NaN is undefined")), + x => Ok(x as INT), + } } } pub fn is_zero(x: f64) -> bool { diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index a8422e98..f119bc0e 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -27,15 +27,29 @@ mod array_functions { } #[rhai_fn(name = "append", name = "+=")] pub fn append(array: &mut Array, y: Array) { - array.extend(y); + if !y.is_empty() { + if array.is_empty() { + *array = y; + } else { + array.extend(y); + } + } } #[rhai_fn(name = "+")] pub fn concat(mut array: Array, y: Array) -> Array { - array.extend(y); + if !y.is_empty() { + if array.is_empty() { + array = y; + } else { + array.extend(y); + } + } array } pub fn insert(array: &mut Array, position: INT, item: Dynamic) { - if position < 0 { + if array.is_empty() { + array.push(item); + } else if position < 0 { if let Some(n) = position.checked_abs() { if n as usize > array.len() { array.insert(0, item); @@ -46,7 +60,7 @@ mod array_functions { array.insert(0, item); } } else if (position as usize) >= array.len() { - push(array, item); + array.push(item); } else { array.insert(position as usize, item); } @@ -58,61 +72,78 @@ mod array_functions { len: INT, item: Dynamic, ) -> Result<(), Box> { + if len <= 0 { + return Ok(()); + } + // Check if array will be over max size limit #[cfg(not(feature = "unchecked"))] - if _ctx.engine().max_array_size() > 0 - && len > 0 - && (len as usize) > _ctx.engine().max_array_size() - { + if _ctx.engine().max_array_size() > 0 && (len as usize) > _ctx.engine().max_array_size() { return EvalAltResult::ErrorDataTooLarge("Size of array".to_string(), Position::NONE) .into(); } - if len > 0 && len as usize > array.len() { + if len as usize > array.len() { array.resize(len as usize, item); } Ok(()) } pub fn pop(array: &mut Array) -> Dynamic { - array.pop().unwrap_or_else(|| ().into()) + if array.is_empty() { + Dynamic::UNIT + } else { + array.pop().unwrap_or_else(|| Dynamic::UNIT) + } } pub fn shift(array: &mut Array) -> Dynamic { if array.is_empty() { - ().into() + Dynamic::UNIT } else { array.remove(0) } } pub fn remove(array: &mut Array, len: INT) -> Dynamic { if len < 0 || (len as usize) >= array.len() { - ().into() + Dynamic::UNIT } else { array.remove(len as usize) } } pub fn clear(array: &mut Array) { - array.clear(); + if !array.is_empty() { + array.clear(); + } } pub fn truncate(array: &mut Array, len: INT) { - if len >= 0 { - array.truncate(len as usize); - } else { - array.clear(); + if !array.is_empty() { + if len >= 0 { + array.truncate(len as usize); + } else { + array.clear(); + } } } pub fn chop(array: &mut Array, len: INT) { - if len as usize >= array.len() { - } else if len >= 0 { - array.drain(0..array.len() - len as usize); - } else { - array.clear(); + if !array.is_empty() && len as usize >= array.len() { + if len >= 0 { + array.drain(0..array.len() - len as usize); + } else { + array.clear(); + } } } pub fn reverse(array: &mut Array) { - array.reverse(); + if !array.is_empty() { + array.reverse(); + } } pub fn splice(array: &mut Array, start: INT, len: INT, replace: Array) { + if array.is_empty() { + *array = replace; + return; + } + let start = if start < 0 { let arr_len = array.len(); start @@ -136,18 +167,22 @@ mod array_functions { array.splice(start..start + len, replace.into_iter()); } pub fn extract(array: &mut Array, start: INT, len: INT) -> Array { + if array.is_empty() || len <= 0 { + return Array::new(); + } + let start = if start < 0 { let arr_len = array.len(); start .checked_abs() .map_or(0, |n| arr_len - (n as usize).min(arr_len)) } else if start as usize >= array.len() { - return Default::default(); + return Array::new(); } else { start as usize }; - let len = if len < 0 { + let len = if len <= 0 { 0 } else if len as usize > array.len() - start { array.len() - start @@ -155,17 +190,25 @@ mod array_functions { len as usize }; - array[start..start + len].to_vec() + if len == 0 { + Array::new() + } else { + array[start..start + len].to_vec() + } } #[rhai_fn(name = "extract")] pub fn extract_tail(array: &mut Array, start: INT) -> Array { + if array.is_empty() { + return Array::new(); + } + let start = if start < 0 { let arr_len = array.len(); start .checked_abs() .map_or(0, |n| arr_len - (n as usize).min(arr_len)) } else if start as usize >= array.len() { - return Default::default(); + return Array::new(); } else { start as usize }; @@ -174,12 +217,14 @@ mod array_functions { } #[rhai_fn(name = "split")] pub fn split_at(array: &mut Array, start: INT) -> Array { - if start < 0 { + if array.is_empty() { + Array::new() + } else if start < 0 { if let Some(n) = start.checked_abs() { if n as usize > array.len() { mem::take(array) } else { - let mut result: Array = Default::default(); + let mut result = Array::new(); result.extend(array.drain(array.len() - n as usize..)); result } @@ -187,9 +232,9 @@ mod array_functions { mem::take(array) } } else if start as usize >= array.len() { - Default::default() + Array::new() } else { - let mut result: Array = Default::default(); + let mut result = Array::new(); result.extend(array.drain(start as usize..)); result } @@ -200,6 +245,10 @@ mod array_functions { array: &mut Array, mapper: FnPtr, ) -> Result> { + if array.is_empty() { + return Ok(array.clone()); + } + let mut ar = Array::with_capacity(array.len()); for (i, item) in array.iter().enumerate() { @@ -233,6 +282,10 @@ mod array_functions { array: &mut Array, filter: FnPtr, ) -> Result> { + if array.is_empty() { + return Ok(array.clone()); + } + let mut ar = Array::new(); for (i, item) in array.iter().enumerate() { @@ -269,6 +322,10 @@ mod array_functions { array: &mut Array, value: Dynamic, ) -> Result> { + if array.is_empty() { + return Ok(false); + } + for item in array.iter_mut() { if ctx .call_fn_dynamic_raw(OP_EQUALS, true, &mut [item, &mut value.clone()]) @@ -300,7 +357,11 @@ mod array_functions { array: &mut Array, value: Dynamic, ) -> Result> { - index_of_starting_from(ctx, array, value, 0) + if array.is_empty() { + Ok(-1) + } else { + index_of_starting_from(ctx, array, value, 0) + } } #[rhai_fn(name = "index_of", return_raw, pure)] pub fn index_of_starting_from( @@ -309,6 +370,10 @@ mod array_functions { value: Dynamic, start: INT, ) -> Result> { + if array.is_empty() { + return Ok(-1); + } + let start = if start < 0 { let arr_len = array.len(); start @@ -351,7 +416,11 @@ mod array_functions { array: &mut Array, filter: FnPtr, ) -> Result> { - index_of_filter_starting_from(ctx, array, filter, 0) + if array.is_empty() { + Ok(-1) + } else { + index_of_filter_starting_from(ctx, array, filter, 0) + } } #[rhai_fn(name = "index_of", return_raw, pure)] pub fn index_of_filter_starting_from( @@ -360,6 +429,10 @@ mod array_functions { filter: FnPtr, start: INT, ) -> Result> { + if array.is_empty() { + return Ok(-1); + } + let start = if start < 0 { let arr_len = array.len(); start @@ -405,6 +478,10 @@ mod array_functions { array: &mut Array, filter: FnPtr, ) -> Result> { + if array.is_empty() { + return Ok(false); + } + for (i, item) in array.iter().enumerate() { if filter .call_dynamic(&ctx, None, [item.clone()]) @@ -439,6 +516,10 @@ mod array_functions { array: &mut Array, filter: FnPtr, ) -> Result> { + if array.is_empty() { + return Ok(true); + } + for (i, item) in array.iter().enumerate() { if !filter .call_dynamic(&ctx, None, [item.clone()]) @@ -473,6 +554,10 @@ mod array_functions { array: &mut Array, reducer: FnPtr, ) -> Result> { + if array.is_empty() { + return Ok(Dynamic::UNIT); + } + let mut result = Dynamic::UNIT; for (i, item) in array.iter().enumerate() { @@ -505,6 +590,10 @@ mod array_functions { reducer: FnPtr, initial: Dynamic, ) -> Result> { + if array.is_empty() { + return Ok(initial); + } + let mut result = initial; for (i, item) in array.iter().enumerate() { @@ -536,6 +625,10 @@ mod array_functions { array: &mut Array, reducer: FnPtr, ) -> Result> { + if array.is_empty() { + return Ok(Dynamic::UNIT); + } + let mut result = Dynamic::UNIT; for (i, item) in array.iter().enumerate().rev() { @@ -568,6 +661,10 @@ mod array_functions { reducer: FnPtr, initial: Dynamic, ) -> Result> { + if array.is_empty() { + return Ok(initial); + } + let mut result = initial; for (i, item) in array.iter().enumerate().rev() { @@ -599,6 +696,10 @@ mod array_functions { array: &mut Array, comparer: FnPtr, ) -> Result<(), Box> { + if array.is_empty() { + return Ok(()); + } + array.sort_by(|x, y| { comparer .call_dynamic(&ctx, None, [x.clone(), y.clone()]) @@ -621,6 +722,10 @@ mod array_functions { array: &mut Array, filter: FnPtr, ) -> Result> { + if array.is_empty() { + return Ok(Array::new()); + } + let mut drained = Array::with_capacity(array.len()); let mut i = 0; @@ -660,18 +765,22 @@ mod array_functions { } #[rhai_fn(name = "drain")] pub fn drain_range(array: &mut Array, start: INT, len: INT) -> Array { + if array.is_empty() || len <= 0 { + return Array::new(); + } + let start = if start < 0 { let arr_len = array.len(); start .checked_abs() .map_or(0, |n| arr_len - (n as usize).min(arr_len)) } else if start as usize >= array.len() { - return Default::default(); + return Array::new(); } else { start as usize }; - let len = if len < 0 { + let len = if len <= 0 { 0 } else if len as usize > array.len() - start { array.len() - start @@ -687,6 +796,10 @@ mod array_functions { array: &mut Array, filter: FnPtr, ) -> Result> { + if array.is_empty() { + return Ok(Array::new()); + } + let mut drained = Array::new(); let mut i = 0; @@ -726,6 +839,10 @@ mod array_functions { } #[rhai_fn(name = "retain")] pub fn retain_range(array: &mut Array, start: INT, len: INT) -> Array { + if array.is_empty() || len <= 0 { + return Array::new(); + } + let start = if start < 0 { let arr_len = array.len(); start diff --git a/src/packages/fn_basic.rs b/src/packages/fn_basic.rs index e4496762..4a92e1cc 100644 --- a/src/packages/fn_basic.rs +++ b/src/packages/fn_basic.rs @@ -92,7 +92,7 @@ fn collect_fn_metadata(ctx: NativeCallContext) -> crate::Array { .map(|&s| s.into()) .collect(); - let mut list: Array = Default::default(); + let mut list = Array::new(); ctx.iter_namespaces() .flat_map(|m| m.iter_script_fn()) diff --git a/src/packages/map_basic.rs b/src/packages/map_basic.rs index b7e52bc3..b042d9e1 100644 --- a/src/packages/map_basic.rs +++ b/src/packages/map_basic.rs @@ -17,32 +17,56 @@ def_package!(crate:BasicMapPackage:"Basic object map utilities.", lib, { mod map_functions { #[rhai_fn(name = "has", pure)] pub fn contains(map: &mut Map, prop: ImmutableString) -> bool { - map.contains_key(prop.as_str()) + if map.is_empty() { + false + } else { + map.contains_key(prop.as_str()) + } } #[rhai_fn(pure)] pub fn len(map: &mut Map) -> INT { map.len() as INT } pub fn clear(map: &mut Map) { - map.clear(); + if !map.is_empty() { + map.clear(); + } } pub fn remove(map: &mut Map, name: ImmutableString) -> Dynamic { - map.remove(name.as_str()).unwrap_or_else(|| ().into()) + if !map.is_empty() { + map.remove(name.as_str()).unwrap_or_else(|| Dynamic::UNIT) + } else { + Dynamic::UNIT + } } #[rhai_fn(name = "mixin", name = "+=")] pub fn mixin(map: &mut Map, map2: Map) { - map.extend(map2.into_iter()); + if !map2.is_empty() { + map.extend(map2.into_iter()); + } } #[rhai_fn(name = "+")] pub fn merge(map1: Map, map2: Map) -> Map { - let mut map1 = map1; - map1.extend(map2.into_iter()); - map1 + if map2.is_empty() { + map1 + } else if map1.is_empty() { + map2 + } else { + let mut map1 = map1; + map1.extend(map2.into_iter()); + map1 + } } pub fn fill_with(map: &mut Map, map2: Map) { - map2.into_iter().for_each(|(key, value)| { - map.entry(key).or_insert(value); - }); + if !map2.is_empty() { + if map.is_empty() { + *map = map2; + } else { + map2.into_iter().for_each(|(key, value)| { + map.entry(key).or_insert(value); + }); + } + } } #[rhai_fn(name = "==", return_raw, pure)] pub fn equals( @@ -53,23 +77,22 @@ mod map_functions { if map1.len() != map2.len() { return Ok(false); } - if map1.is_empty() { - return Ok(true); - } - let mut map2 = map2; + if !map1.is_empty() { + let mut map2 = map2; - for (m1, v1) in map1.iter_mut() { - if let Some(v2) = map2.get_mut(m1) { - let equals = ctx - .call_fn_dynamic_raw(OP_EQUALS, true, &mut [v1, v2]) - .map(|v| v.as_bool().unwrap_or(false))?; + for (m1, v1) in map1.iter_mut() { + if let Some(v2) = map2.get_mut(m1) { + let equals = ctx + .call_fn_dynamic_raw(OP_EQUALS, true, &mut [v1, v2]) + .map(|v| v.as_bool().unwrap_or(false))?; - if !equals { + if !equals { + return Ok(false); + } + } else { return Ok(false); } - } else { - return Ok(false); } } @@ -88,11 +111,19 @@ mod map_functions { pub mod indexing { #[rhai_fn(pure)] pub fn keys(map: &mut Map) -> Array { - map.iter().map(|(k, _)| k.clone().into()).collect() + if map.is_empty() { + Array::new() + } else { + map.keys().cloned().map(Into::::into).collect() + } } #[rhai_fn(pure)] pub fn values(map: &mut Map) -> Array { - map.iter().map(|(_, v)| v.clone()).collect() + if map.is_empty() { + Array::new() + } else { + map.values().cloned().collect() + } } } } diff --git a/src/packages/math_basic.rs b/src/packages/math_basic.rs index f3c9c5ea..0f9f6a88 100644 --- a/src/packages/math_basic.rs +++ b/src/packages/math_basic.rs @@ -325,6 +325,15 @@ mod decimal_functions { } } + pub fn sin(x: Decimal) -> Decimal { + x.sin() + } + pub fn cos(x: Decimal) -> Decimal { + x.cos() + } + pub fn tan(x: Decimal) -> Decimal { + x.tan() + } #[rhai_fn(return_raw)] pub fn sqrt(x: Decimal) -> Result> { x.sqrt() diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 65980026..58a94777 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -14,7 +14,7 @@ def_package!(crate:MoreStringPackage:"Additional string utilities, including str #[export_module] mod string_functions { - use crate::ImmutableString; + use crate::{ImmutableString, SmartString}; #[rhai_fn(name = "+", name = "append")] pub fn add_append( @@ -66,11 +66,19 @@ mod string_functions { #[rhai_fn(name = "len", get = "len")] pub fn len(string: &str) -> INT { - string.chars().count() as INT + if string.is_empty() { + 0 + } else { + string.chars().count() as INT + } } #[rhai_fn(name = "bytes", get = "bytes")] pub fn bytes(string: &str) -> INT { - string.len() as INT + if string.is_empty() { + 0 + } else { + string.len() as INT + } } pub fn remove(string: &mut ImmutableString, sub_string: ImmutableString) { *string -= sub_string; @@ -80,7 +88,9 @@ mod string_functions { *string -= character; } pub fn clear(string: &mut ImmutableString) { - string.make_mut().clear(); + if !string.is_empty() { + string.make_mut().clear(); + } } pub fn truncate(string: &mut ImmutableString, len: INT) { if len > 0 { @@ -88,7 +98,7 @@ mod string_functions { let copy = string.make_mut(); copy.clear(); copy.extend(chars.into_iter().take(len as usize)); - } else { + } else if !string.is_empty() { string.make_mut().clear(); } } @@ -99,18 +109,61 @@ mod string_functions { *string = trimmed.to_string().into(); } } + pub fn pop(string: &mut ImmutableString) -> Dynamic { + if string.is_empty() { + Dynamic::UNIT + } else { + match string.make_mut().pop() { + Some(c) => c.into(), + None => Dynamic::UNIT, + } + } + } + #[rhai_fn(name = "pop")] + pub fn pop_string( + ctx: NativeCallContext, + string: &mut ImmutableString, + len: INT, + ) -> ImmutableString { + if string.is_empty() || len <= 0 { + return ctx.engine().empty_string.clone(); + } - pub fn to_upper(string: &str) -> ImmutableString { - string.to_uppercase().into() + let mut chars = StaticVec::::with_capacity(len as usize); + + for _ in 0..len { + match string.make_mut().pop() { + Some(c) => chars.push(c), + None => break, + } + } + + chars.into_iter().rev().collect::().into() + } + + pub fn to_upper(string: ImmutableString) -> ImmutableString { + if string.is_empty() { + string + } else { + string.to_uppercase().into() + } } pub fn make_upper(string: &mut ImmutableString) { - *string = to_upper(string); + if !string.is_empty() { + *string = string.to_uppercase().into(); + } } - pub fn to_lower(string: &str) -> ImmutableString { - string.to_lowercase().into() + pub fn to_lower(string: ImmutableString) -> ImmutableString { + if string.is_empty() { + string + } else { + string.to_lowercase().into() + } } pub fn make_lower(string: &mut ImmutableString) { - *string = to_lower(string); + if !string.is_empty() { + *string = string.to_lowercase().into(); + } } #[rhai_fn(name = "to_upper")] @@ -146,6 +199,10 @@ mod string_functions { #[rhai_fn(name = "index_of")] pub fn index_of_char_starting_from(string: &str, character: char, start: INT) -> INT { + if string.is_empty() { + return -1; + } + let start = if start < 0 { if let Some(n) = start.checked_abs() { let chars: Vec<_> = string.chars().collect(); @@ -181,13 +238,21 @@ mod string_functions { } #[rhai_fn(name = "index_of")] pub fn index_of_char(string: &str, character: char) -> INT { - string - .find(character) - .map(|index| string[0..index].chars().count() as INT) - .unwrap_or(-1 as INT) + if string.is_empty() { + -1 + } else { + string + .find(character) + .map(|index| string[0..index].chars().count() as INT) + .unwrap_or(-1 as INT) + } } #[rhai_fn(name = "index_of")] pub fn index_of_string_starting_from(string: &str, find_string: &str, start: INT) -> INT { + if string.is_empty() { + return -1; + } + let start = if start < 0 { if let Some(n) = start.checked_abs() { let chars = string.chars().collect::>(); @@ -223,10 +288,14 @@ mod string_functions { } #[rhai_fn(name = "index_of")] pub fn index_of(string: &str, find_string: &str) -> INT { - string - .find(find_string) - .map(|index| string[0..index].chars().count() as INT) - .unwrap_or(-1 as INT) + if string.is_empty() { + -1 + } else { + string + .find(find_string) + .map(|index| string[0..index].chars().count() as INT) + .unwrap_or(-1 as INT) + } } pub fn sub_string( @@ -235,6 +304,10 @@ mod string_functions { start: INT, len: INT, ) -> ImmutableString { + if string.is_empty() { + return ctx.engine().empty_string.clone(); + } + let mut chars = StaticVec::with_capacity(string.len()); let offset = if string.is_empty() || len <= 0 { @@ -280,12 +353,20 @@ mod string_functions { string: &str, start: INT, ) -> ImmutableString { - let len = string.len() as INT; - sub_string(ctx, string, start, len) + if string.is_empty() { + ctx.engine().empty_string.clone() + } else { + let len = string.len() as INT; + sub_string(ctx, string, start, len) + } } #[rhai_fn(name = "crop")] pub fn crop(string: &mut ImmutableString, start: INT, len: INT) { + if string.is_empty() { + return; + } + let mut chars = StaticVec::with_capacity(string.len()); let offset = if string.is_empty() || len <= 0 { @@ -330,7 +411,9 @@ mod string_functions { #[rhai_fn(name = "replace")] pub fn replace(string: &mut ImmutableString, find_string: &str, substitute_string: &str) { - *string = string.replace(find_string, substitute_string).into(); + if !string.is_empty() { + *string = string.replace(find_string, substitute_string).into(); + } } #[rhai_fn(name = "replace")] pub fn replace_string_with_char( @@ -338,9 +421,11 @@ mod string_functions { find_string: &str, substitute_character: char, ) { - *string = string - .replace(find_string, &substitute_character.to_string()) - .into(); + if !string.is_empty() { + *string = string + .replace(find_string, &substitute_character.to_string()) + .into(); + } } #[rhai_fn(name = "replace")] pub fn replace_char_with_string( @@ -348,9 +433,11 @@ mod string_functions { find_character: char, substitute_string: &str, ) { - *string = string - .replace(&find_character.to_string(), substitute_string) - .into(); + if !string.is_empty() { + *string = string + .replace(&find_character.to_string(), substitute_string) + .into(); + } } #[rhai_fn(name = "replace")] pub fn replace_char( @@ -358,12 +445,14 @@ mod string_functions { find_character: char, substitute_character: char, ) { - *string = string - .replace( - &find_character.to_string(), - &substitute_character.to_string(), - ) - .into(); + if !string.is_empty() { + *string = string + .replace( + &find_character.to_string(), + &substitute_character.to_string(), + ) + .into(); + } } #[rhai_fn(return_raw)] @@ -373,6 +462,10 @@ mod string_functions { len: INT, character: char, ) -> Result<(), Box> { + if len <= 0 { + return Ok(()); + } + let _ctx = ctx; // Check if string will be over max size limit @@ -385,26 +478,23 @@ mod string_functions { .into(); } - if len > 0 { - let orig_len = string.chars().count(); + let orig_len = string.chars().count(); - if len as usize > orig_len { - let p = string.make_mut(); + if len as usize > orig_len { + let p = string.make_mut(); - for _ in 0..(len as usize - orig_len) { - p.push(character); - } + for _ in 0..(len as usize - orig_len) { + p.push(character); + } - #[cfg(not(feature = "unchecked"))] - if _ctx.engine().max_string_size() > 0 - && string.len() > _ctx.engine().max_string_size() - { - return crate::EvalAltResult::ErrorDataTooLarge( - "Length of string".to_string(), - crate::Position::NONE, - ) - .into(); - } + #[cfg(not(feature = "unchecked"))] + if _ctx.engine().max_string_size() > 0 && string.len() > _ctx.engine().max_string_size() + { + return crate::EvalAltResult::ErrorDataTooLarge( + "Length of string".to_string(), + crate::Position::NONE, + ) + .into(); } } @@ -417,6 +507,10 @@ mod string_functions { len: INT, padding: &str, ) -> Result<(), Box> { + if len <= 0 { + return Ok(()); + } + let _ctx = ctx; // Check if string will be over max size limit @@ -429,33 +523,30 @@ mod string_functions { .into(); } - if len > 0 { - let mut str_len = string.chars().count(); - let padding_len = padding.chars().count(); + let mut str_len = string.chars().count(); + let padding_len = padding.chars().count(); - if len as usize > str_len { - let p = string.make_mut(); + if len as usize > str_len { + let p = string.make_mut(); - while str_len < len as usize { - if str_len + padding_len <= len as usize { - p.push_str(padding); - str_len += padding_len; - } else { - p.extend(padding.chars().take(len as usize - str_len)); - str_len = len as usize; - } + while str_len < len as usize { + if str_len + padding_len <= len as usize { + p.push_str(padding); + str_len += padding_len; + } else { + p.extend(padding.chars().take(len as usize - str_len)); + str_len = len as usize; } + } - #[cfg(not(feature = "unchecked"))] - if _ctx.engine().max_string_size() > 0 - && string.len() > _ctx.engine().max_string_size() - { - return crate::EvalAltResult::ErrorDataTooLarge( - "Length of string".to_string(), - crate::Position::NONE, - ) - .into(); - } + #[cfg(not(feature = "unchecked"))] + if _ctx.engine().max_string_size() > 0 && string.len() > _ctx.engine().max_string_size() + { + return crate::EvalAltResult::ErrorDataTooLarge( + "Length of string".to_string(), + crate::Position::NONE, + ) + .into(); } } @@ -468,7 +559,11 @@ mod string_functions { #[rhai_fn(name = "split")] pub fn chars(string: &str) -> Array { - string.chars().map(Into::::into).collect() + if string.is_empty() { + Array::new() + } else { + string.chars().map(Into::::into).collect() + } } #[rhai_fn(name = "split")] pub fn split_at(ctx: NativeCallContext, string: ImmutableString, start: INT) -> Array { diff --git a/src/parse.rs b/src/parse.rs index da378114..daa57566 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -1,8 +1,8 @@ //! Main module defining the lexer and parser. use crate::ast::{ - BinaryExpr, CustomExpr, Expr, FnCallExpr, FnCallHashes, Ident, OpAssignment, ReturnType, - ScriptFnDef, Stmt, StmtBlock, AST_OPTION_FLAGS::*, + BinaryExpr, CustomExpr, Expr, FnCallExpr, FnCallHashes, Ident, OpAssignment, ScriptFnDef, Stmt, + StmtBlock, AST_OPTION_FLAGS::*, }; use crate::custom_syntax::{markers::*, CustomSyntax}; use crate::dynamic::AccessMode; @@ -11,7 +11,8 @@ use crate::fn_hash::get_hasher; use crate::module::NamespaceRef; use crate::optimize::{optimize_into_ast, OptimizationLevel}; use crate::token::{ - is_keyword_function, is_valid_identifier, Token, TokenStream, TokenizerControl, + is_keyword_function, is_valid_function_name, is_valid_identifier, Token, TokenStream, + TokenizerControl, }; use crate::{ calc_fn_hash, calc_qualified_fn_hash, calc_qualified_var_hash, Engine, Identifier, @@ -41,7 +42,8 @@ const SCOPE_SEARCH_BARRIER_MARKER: &str = "$BARRIER$"; /// The message: `TokenStream` never ends const NEVER_ENDS: &str = "`TokenStream` never ends"; -/// A factory of identifiers from text strings. +/// _(internals)_ A factory of identifiers from text strings. +/// Exported under the `internals` feature only. /// /// When [`SmartString`](https://crates.io/crates/smartstring) is used as [`Identifier`], /// this just returns a copy because most identifiers in Rhai are short and ASCII-based. @@ -71,38 +73,39 @@ impl IdentifierBuilder { } } -/// A type that encapsulates the current state of the parser. +/// _(internals)_ A type that encapsulates the current state of the parser. +/// Exported under the `internals` feature only. #[derive(Debug)] pub struct ParseState<'e> { /// Reference to the scripting [`Engine`]. - engine: &'e Engine, + pub engine: &'e Engine, /// Input stream buffer containing the next character to read. - tokenizer_control: TokenizerControl, + pub tokenizer_control: TokenizerControl, /// Interned strings. - interned_strings: IdentifierBuilder, + pub interned_strings: IdentifierBuilder, /// Encapsulates a local stack with variable names to simulate an actual runtime scope. - stack: Vec<(Identifier, AccessMode)>, + pub stack: StaticVec<(Identifier, AccessMode)>, /// Size of the local variables stack upon entry of the current block scope. - entry_stack_len: usize, + pub entry_stack_len: usize, /// Tracks a list of external variables (variables that are not explicitly declared in the scope). #[cfg(not(feature = "no_closure"))] - external_vars: BTreeMap, + pub external_vars: BTreeMap, /// An indicator that disables variable capturing into externals one single time /// up until the nearest consumed Identifier token. /// If set to false the next call to [`access_var`][ParseState::access_var] will not capture the variable. /// All consequent calls to [`access_var`][ParseState::access_var] will not be affected #[cfg(not(feature = "no_closure"))] - allow_capture: bool, + pub allow_capture: bool, /// Encapsulates a local stack with imported [module][crate::Module] names. #[cfg(not(feature = "no_module"))] - modules: StaticVec, + pub modules: StaticVec, /// Maximum levels of expression nesting. #[cfg(not(feature = "unchecked"))] - max_expr_depth: Option, + pub max_expr_depth: Option, /// Maximum levels of expression nesting in functions. #[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "no_function"))] - max_function_expr_depth: Option, + pub max_function_expr_depth: Option, } impl<'e> ParseState<'e> { @@ -123,7 +126,7 @@ impl<'e> ParseState<'e> { #[cfg(not(feature = "no_closure"))] allow_capture: true, interned_strings: Default::default(), - stack: Vec::with_capacity(16), + stack: Default::default(), entry_stack_len: 0, #[cfg(not(feature = "no_module"))] modules: Default::default(), @@ -478,7 +481,7 @@ fn parse_fn_call( }, ); - let hashes = if is_valid_identifier(id.chars()) { + let hashes = if is_valid_function_name(&id) { FnCallHashes::from_script(hash) } else { FnCallHashes::from_native(hash) @@ -528,7 +531,7 @@ fn parse_fn_call( }, ); - let hashes = if is_valid_identifier(id.chars()) { + let hashes = if is_valid_function_name(&id) { FnCallHashes::from_script(hash) } else { FnCallHashes::from_native(hash) @@ -827,8 +830,8 @@ fn parse_map_literal( // #{ ... settings.pos = eat_token(input, Token::MapStart); - let mut map: StaticVec<(Ident, Expr)> = Default::default(); - let mut template: BTreeMap = Default::default(); + let mut map = StaticVec::<(Ident, Expr)>::new(); + let mut template = BTreeMap::::new(); loop { const MISSING_RBRACE: &str = "to end this object map literal"; @@ -1178,7 +1181,7 @@ fn parse_primary( // Interpolated string Token::InterpolatedString(_) => { - let mut segments: StaticVec = Default::default(); + let mut segments = StaticVec::::new(); if let (Token::InterpolatedString(s), pos) = input.next().expect(NEVER_ENDS) { segments.push(Expr::StringConstant(s.into(), pos)); @@ -1388,7 +1391,7 @@ fn parse_primary( if let Some((ref mut namespace, _)) = namespace { namespace.push(var_name_def); } else { - let mut ns: NamespaceRef = Default::default(); + let mut ns = NamespaceRef::new(); ns.push(var_name_def); namespace = Some((ns, 42)); } @@ -1949,7 +1952,7 @@ fn parse_binary_op( let hash = calc_fn_hash(&s, 2); FnCallExpr { - hashes: if is_valid_identifier(s.chars()) { + hashes: if is_valid_function_name(&s) { FnCallHashes::from_script(hash) } else { FnCallHashes::from_native(hash) @@ -1976,9 +1979,9 @@ fn parse_custom_syntax( pos: Position, ) -> Result { let mut settings = settings; - let mut keywords: StaticVec = Default::default(); - let mut segments: StaticVec<_> = Default::default(); - let mut tokens: StaticVec<_> = Default::default(); + let mut keywords = StaticVec::::new(); + let mut segments = StaticVec::new(); + let mut tokens = StaticVec::new(); // Adjust the variables stack if syntax.scope_may_be_changed { @@ -2427,7 +2430,7 @@ fn parse_let( state.stack.push((name, var_type)); let export = if is_export { - AST_OPTION_EXPORTED + AST_OPTION_PUBLIC } else { AST_OPTION_NONE }; @@ -2698,7 +2701,7 @@ fn parse_stmt( #[cfg(not(feature = "no_function"))] #[cfg(feature = "metadata")] let comments = { - let mut comments: StaticVec = Default::default(); + let mut comments = StaticVec::::new(); let mut comments_pos = Position::NONE; // Handle doc-comments. @@ -2828,11 +2831,11 @@ fn parse_stmt( Token::Continue if settings.is_breakable => { let pos = eat_token(input, Token::Continue); - Ok(Stmt::Continue(pos)) + Ok(Stmt::BreakLoop(AST_OPTION_NONE, pos)) } Token::Break if settings.is_breakable => { let pos = eat_token(input, Token::Break); - Ok(Stmt::Break(pos)) + Ok(Stmt::BreakLoop(AST_OPTION_BREAK_OUT, pos)) } Token::Continue | Token::Break => Err(PERR::LoopBreak.into_err(settings.pos)), @@ -2842,8 +2845,8 @@ fn parse_stmt( .map(|(token, pos)| { ( match token { - Token::Return => ReturnType::Return, - Token::Throw => ReturnType::Exception, + Token::Return => AST_OPTION_NONE, + Token::Throw => AST_OPTION_BREAK_OUT, _ => unreachable!(), }, pos, @@ -2915,7 +2918,7 @@ fn parse_try_catch( } // try { body } catch ( - let var_def = if match_token(input, Token::LeftParen).0 { + let err_var = if match_token(input, Token::LeftParen).0 { let (name, pos) = parse_var_name(input)?; let (matched, err_pos) = match_token(input, Token::RightParen); @@ -2928,6 +2931,7 @@ fn parse_try_catch( } let name = state.get_identifier(name); + state.stack.push((name.clone(), AccessMode::ReadWrite)); Some(Ident { name, pos }) } else { None @@ -2936,8 +2940,16 @@ fn parse_try_catch( // try { body } catch ( var ) { catch_block } let catch_body = parse_block(input, state, lib, settings.level_up())?; + if err_var.is_some() { + // Remove the error variable from the stack + state + .stack + .pop() + .expect("stack contains at least one entry"); + } + Ok(Stmt::TryCatch( - (body.into(), var_def, catch_body.into()).into(), + (body.into(), err_var, catch_body.into()).into(), settings.pos, )) } @@ -2972,7 +2984,7 @@ fn parse_fn( (_, pos) => return Err(PERR::FnMissingParams(name).into_err(*pos)), }; - let mut params: StaticVec<_> = Default::default(); + let mut params = StaticVec::new(); if !match_token(input, Token::RightParen).0 { let sep_err = format!("to separate the parameters of function '{}'", name); @@ -3105,7 +3117,7 @@ fn parse_anon_fn( #[cfg(not(feature = "unchecked"))] settings.ensure_level_within_max_limit(state.max_expr_depth)?; - let mut params_list: StaticVec<_> = Default::default(); + let mut params_list = StaticVec::new(); if input.next().expect(NEVER_ENDS).0 != Token::Or && !match_token(input, Token::Pipe).0 { loop { @@ -3234,11 +3246,18 @@ impl Engine { } } - let expr = vec![Stmt::Expr(expr)]; + let mut statements = StaticVec::new(); + statements.push(Stmt::Expr(expr)); Ok( // Optimize AST - optimize_into_ast(self, scope, expr, Default::default(), optimization_level), + optimize_into_ast( + self, + scope, + statements, + Default::default(), + optimization_level, + ), ) } @@ -3247,8 +3266,8 @@ impl Engine { &self, input: &mut TokenStream, state: &mut ParseState, - ) -> Result<(Vec, Vec>), ParseError> { - let mut statements = Vec::with_capacity(16); + ) -> Result<(StaticVec, StaticVec>), ParseError> { + let mut statements = StaticVec::new(); let mut functions = BTreeMap::new(); while !input.peek().expect(NEVER_ENDS).0.is_eof() { diff --git a/src/scope.rs b/src/scope.rs index 323d78fd..028287a5 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -464,6 +464,9 @@ impl<'a> Scope<'a> { /// *ptr = 123_i64.into(); /// /// assert_eq!(my_scope.get_value::("x").expect("x should exist"), 123); + /// + /// my_scope.push_constant("Z", 1_i64); + /// assert!(my_scope.get_mut("Z").is_none()); /// ``` #[inline] #[must_use] diff --git a/src/serde/deserialize.rs b/src/serde/deserialize.rs index 78bc91b6..de970d54 100644 --- a/src/serde/deserialize.rs +++ b/src/serde/deserialize.rs @@ -142,7 +142,7 @@ impl<'d> Visitor<'d> for DynamicVisitor { #[cfg(not(feature = "no_index"))] fn visit_seq>(self, mut seq: A) -> Result { - let mut arr: Array = Default::default(); + let mut arr = Array::new(); while let Some(v) = seq.next_element()? { arr.push(v); @@ -153,7 +153,7 @@ impl<'d> Visitor<'d> for DynamicVisitor { #[cfg(not(feature = "no_object"))] fn visit_map>(self, mut map: M) -> Result { - let mut m: Map = Default::default(); + let mut m = Map::new(); while let Some((k, v)) = map.next_entry::<&str, _>()? { m.insert(k.into(), v); diff --git a/src/serde/metadata.rs b/src/serde/metadata.rs index 68a96e34..2ff11a9f 100644 --- a/src/serde/metadata.rs +++ b/src/serde/metadata.rs @@ -207,6 +207,13 @@ struct ModuleMetadata { pub functions: Vec, } +impl ModuleMetadata { + #[inline(always)] + pub fn new() -> Self { + Default::default() + } +} + impl From<&crate::Module> for ModuleMetadata { fn from(module: &crate::Module) -> Self { let mut functions: Vec<_> = module.iter_fn().map(|f| f.into()).collect(); @@ -239,7 +246,7 @@ impl Engine { include_global: bool, ) -> serde_json::Result { let _ast = ast; - let mut global: ModuleMetadata = Default::default(); + let mut global = ModuleMetadata::new(); if include_global { self.global_modules diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 00000000..98f23fdc --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,39 @@ +//! Module containing unit tests. +#![cfg(test)] + +/// This test is to make sure no code changes increase the sizes of critical data structures. +#[test] +fn check_struct_sizes() { + use crate::*; + use std::mem::size_of; + + const PACKED: bool = cfg!(all( + target_pointer_width = "32", + feature = "only_i32", + feature = "no_float" + )); + + assert_eq!(size_of::(), if PACKED { 8 } else { 16 }); + assert_eq!(size_of::>(), if PACKED { 8 } else { 16 }); + #[cfg(not(feature = "no_position"))] + 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::(), + if cfg!(feature = "no_smartstring") { + 64 + } else { + 80 + } + ); + assert_eq!(size_of::(), 464); + assert_eq!(size_of::(), 56); + assert_eq!( + size_of::(), + if cfg!(feature = "no_position") { 8 } else { 16 } + ); + assert_eq!(size_of::(), 72); +} diff --git a/src/token.rs b/src/token.rs index c52a3eb2..9baaf22a 100644 --- a/src/token.rs +++ b/src/token.rs @@ -4,6 +4,7 @@ use crate::engine::{ Precedence, KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_FN_PTR_CALL, KEYWORD_FN_PTR_CURRY, KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, KEYWORD_THIS, KEYWORD_TYPE_OF, }; +use crate::fn_native::OnParseTokenCallback; use crate::{Engine, LexError, StaticVec, INT}; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -960,7 +961,7 @@ impl Token { #[inline] pub(crate) fn into_function_name_for_override(self) -> Result { match self { - Self::Custom(s) | Self::Identifier(s) if is_valid_identifier(s.chars()) => Ok(s), + Self::Custom(s) | Self::Identifier(s) if is_valid_function_name(&s) => Ok(s), _ => Err(self), } } @@ -1421,7 +1422,7 @@ fn get_next_token_inner( // digit ... ('0'..='9', _) => { - let mut result: smallvec::SmallVec<[char; 16]> = Default::default(); + let mut result = smallvec::SmallVec::<[char; 16]>::new(); let mut radix_base: Option = None; let mut valid: fn(char) -> bool = is_numeric_digit; result.push(c); @@ -1951,7 +1952,7 @@ fn get_identifier( start_pos: Position, first_char: char, ) -> Option<(Token, Position)> { - let mut result: smallvec::SmallVec<[char; 8]> = Default::default(); + let mut result = smallvec::SmallVec::<[char; 8]>::new(); result.push(first_char); while let Some(next_char) = stream.peek_next() { @@ -2015,6 +2016,13 @@ pub fn is_valid_identifier(name: impl Iterator) -> bool { first_alphabetic } +/// Is a text string a valid scripted function name? +#[inline(always)] +#[must_use] +pub fn is_valid_function_name(name: &str) -> bool { + is_valid_identifier(name.chars()) +} + /// Is a character valid to start an identifier? #[cfg(feature = "unicode-xid-ident")] #[inline(always)] @@ -2047,15 +2055,17 @@ pub fn is_id_continue(x: char) -> bool { x.is_ascii_alphanumeric() || x == '_' } -/// A type that implements the [`InputStream`] trait. +/// _(internals)_ A type that implements the [`InputStream`] trait. +/// Exported under the `internals` feature only. +/// /// Multiple character streams are jointed together to form one single stream. pub struct MultiInputsStream<'a> { /// Buffered character, if any. - buf: Option, + pub buf: Option, /// The current stream index. - index: usize, + pub index: usize, /// The input character streams. - streams: StaticVec>>, + pub streams: StaticVec>>, } impl InputStream for MultiInputsStream<'_> { @@ -2105,20 +2115,21 @@ impl InputStream for MultiInputsStream<'_> { } } -/// An iterator on a [`Token`] stream. +/// _(internals)_ An iterator on a [`Token`] stream. +/// Exported under the `internals` feature only. pub struct TokenIterator<'a> { /// Reference to the scripting `Engine`. - engine: &'a Engine, + pub engine: &'a Engine, /// Current state. - state: TokenizeState, + pub state: TokenizeState, /// Current position. - pos: Position, - /// External buffer containing the next character to read, if any. - tokenizer_control: TokenizerControl, + pub pos: Position, + /// Shared object to allow controlling the tokenizer externally. + pub tokenizer_control: TokenizerControl, /// Input character stream. - stream: MultiInputsStream<'a>, + pub stream: MultiInputsStream<'a>, /// A processor function that maps a token to another. - map: Option Token>, + pub token_mapper: Option<&'a OnParseTokenCallback>, } impl<'a> Iterator for TokenIterator<'a> { @@ -2212,7 +2223,7 @@ impl<'a> Iterator for TokenIterator<'a> { }; // Run the mapper, if any - let token = if let Some(map_func) = self.map { + let token = if let Some(map_func) = self.token_mapper { map_func(token) } else { token @@ -2244,9 +2255,9 @@ impl Engine { pub fn lex_with_map<'a>( &'a self, input: impl IntoIterator, - map: fn(Token) -> Token, + token_mapper: &'a OnParseTokenCallback, ) -> (TokenIterator<'a>, TokenizerControl) { - self.lex_raw(input, Some(map)) + self.lex_raw(input, Some(token_mapper)) } /// Tokenize an input text stream with an optional mapping function. #[inline] @@ -2254,7 +2265,7 @@ impl Engine { pub(crate) fn lex_raw<'a>( &'a self, input: impl IntoIterator, - map: Option Token>, + token_mapper: Option<&'a OnParseTokenCallback>, ) -> (TokenIterator<'a>, TokenizerControl) { let buffer: TokenizerControl = Default::default(); let buffer2 = buffer.clone(); @@ -2279,7 +2290,7 @@ impl Engine { streams: input.into_iter().map(|s| s.chars().peekable()).collect(), index: 0, }, - map, + token_mapper, }, buffer2, ) diff --git a/tests/string.rs b/tests/string.rs index d60be0dc..62f00b86 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -100,6 +100,11 @@ fn test_string_mut() -> Result<(), Box> { engine.register_fn("bar", |s: String| s.len() as INT); engine.register_fn("baz", |s: &mut String| s.len()); + assert_eq!(engine.eval::(r#"pop("hello")"#)?, 'o'); + assert_eq!(engine.eval::(r#"pop("hello", 3)"#)?, "llo"); + assert_eq!(engine.eval::(r#"pop("hello", 10)"#)?, "hello"); + assert_eq!(engine.eval::(r#"pop("hello", -42)"#)?, ""); + assert_eq!(engine.eval::(r#"foo("hello")"#)?, 5); assert_eq!(engine.eval::(r#"bar("hello")"#)?, 5); assert!( diff --git a/tests/throw.rs b/tests/throw.rs index 0b8b717f..2ef82dd2 100644 --- a/tests/throw.rs +++ b/tests/throw.rs @@ -29,6 +29,62 @@ fn test_try_catch() -> Result<(), Box> { 123 ); + #[cfg(not(feature = "no_function"))] + assert_eq!( + engine.eval::( + " + fn foo(x) { try { throw 42; } catch (x) { return x; } } + foo(0) + " + )?, + 42 + ); + + assert_eq!( + engine.eval::( + " + let err = 123; + let x = 0; + try { throw 42; } catch(err) { return err; } + " + )?, + 42 + ); + + assert_eq!( + engine.eval::( + " + let err = 123; + let x = 0; + try { throw 42; } catch(err) { print(err); } + err + " + )?, + 123 + ); + + assert_eq!( + engine.eval::( + " + let foo = 123; + let x = 0; + try { throw 42; } catch(err) { return foo; } + " + )?, + 123 + ); + + assert_eq!( + engine.eval::( + " + let foo = 123; + let x = 0; + try { throw 42; } catch(err) { return err; } + " + )?, + 42 + ); + #[cfg(not(feature = "unchecked"))] assert!(matches!( *engine