diff --git a/CHANGELOG.md b/CHANGELOG.md index 08fc72b3..ac63f7ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,20 @@ Bug fixes * Invalid property or method access such as `a.b::c.d` or `a.b::func()` no longer panics but properly returns a syntax error. * `Scope::is_constant` now returns the correct value. +* Exporting a variable that contains a local function pointer (including anonymous function or closure) now raises a runtime error. +* Full optimization is now skipped for method calls. + +Breaking changes +---------------- + +* `ScriptFnMetadata` fields `params` and `comments` are changed to boxed slices instead of `Vec`. In the vast majority of cases this should not cause code breakage. Enhancements ------------ * Variable definitions are optimized so that shadowed variables are reused as much as possible to reduce memory consumption. +* `FnAccess` and `FnNamespace` now implement `Ord` and `PartialOrd`. +* The `event_handler_map` example is enhanced to prevent shadowing of the state object map. Version 1.5.0 diff --git a/Cargo.toml b/Cargo.toml index 4cc1409d..03b3fe9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,8 @@ categories = ["no-std", "embedded", "wasm", "parser-implementations"] smallvec = { version = "1.7", default-features = false, features = ["union", "const_new" ] } ahash = { version = "0.7", default-features = false } num-traits = { version = "0.2", default-features = false } -smartstring = { version = "0.2.8", default-features = false } +bitflags = { version = "1", default-features = false } +smartstring = { version = "1", default-features = false } rhai_codegen = { version = "1.2", path = "codegen", default-features = false } no-std-compat = { version = "0.4", default-features = false, features = ["alloc"], optional = true } diff --git a/README.md b/README.md index cf33fd5d..a9c52df4 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Standard features * Built-in support for most common [data types](https://rhai.rs/book/language/values-and-types.html) including booleans, [integers](https://rhai.rs/book/language/numbers.html), [floating-point numbers](https://rhai.rs/book/language/numbers.html) (including [`Decimal`](https://crates.io/crates/rust_decimal)), [strings](https://rhai.rs/book/language/strings-chars.html), [Unicode characters](https://rhai.rs/book/language/strings-chars.html), [arrays](https://rhai.rs/book/language/arrays.html) (including packed [byte arrays](https://rhai.rs/book/language/blobs.html)) and [object maps](https://rhai.rs/book/language/object-maps.html). * Easily [call a script-defined function](https://rhai.rs/book/engine/call-fn.html) from Rust. * Relatively little `unsafe` code (yes there are some for performance reasons). -* Few dependencies - currently only [`smallvec`](https://crates.io/crates/smallvec), [`num-traits`](https://crates.io/crates/num-traits), [`ahash`](https://crates.io/crates/ahash) and [`smartstring`](https://crates.io/crates/smartstring). +* Few dependencies - currently only [`smallvec`](https://crates.io/crates/smallvec), [`num-traits`](https://crates.io/crates/num-traits), [`ahash`](https://crates.io/crates/ahash), [`bitflags`](https://crates.io/crates/bitflags) and [`smartstring`](https://crates.io/crates/smartstring). * Re-entrant scripting engine can be made `Send + Sync` (via the `sync` feature). * Compile once to [AST](https://rhai.rs/book/engine/compile.html) form for repeated evaluations. * Scripts are [optimized](https://rhai.rs/book/engine/optimize.html) (useful for template-based machine-generated scripts). diff --git a/examples/event_handler_map/main.rs b/examples/event_handler_map/main.rs index 7227fcc7..24f9df2a 100644 --- a/examples/event_handler_map/main.rs +++ b/examples/event_handler_map/main.rs @@ -56,7 +56,11 @@ pub fn main() { }; // Create Engine - let engine = Engine::new(); + let mut engine = Engine::new(); + + // Prevent shadowing of `state` + #[allow(deprecated)] + engine.on_def_var(|_, info, _| Ok(info.name != "state")); // Create a custom 'Scope' to hold state let mut scope = Scope::new(); diff --git a/src/api/compile.rs b/src/api/compile.rs index 6d491da6..39a6ed79 100644 --- a/src/api/compile.rs +++ b/src/api/compile.rs @@ -101,7 +101,8 @@ impl Engine { // Collect all `import` statements with a string constant path ASTNode::Stmt(Stmt::Import(x, ..)) => match x.0 { Expr::StringConstant(ref s, ..) - if !resolver.contains_path(s) && !imports.contains(s.as_str()) => + if !resolver.contains_path(s) + && (imports.is_empty() || !imports.contains(s.as_str())) => { imports.insert(s.clone().into()); true diff --git a/src/api/custom_syntax.rs b/src/api/custom_syntax.rs index cf5713a7..1fcb0455 100644 --- a/src/api/custom_syntax.rs +++ b/src/api/custom_syntax.rs @@ -227,9 +227,10 @@ impl Engine { // Standard or reserved keyword/symbol not in first position _ if !segments.is_empty() && token.is_some() => { // Make it a custom keyword/symbol if it is disabled or reserved - if (self.disabled_symbols.contains(s) + if ((!self.disabled_symbols.is_empty() && self.disabled_symbols.contains(s)) || token.map_or(false, |v| v.is_reserved())) - && !self.custom_keywords.contains_key(s) + && (self.custom_keywords.is_empty() + || !self.custom_keywords.contains_key(s)) { self.custom_keywords.insert(s.into(), None); } @@ -238,7 +239,7 @@ impl Engine { // Standard keyword in first position but not disabled _ if segments.is_empty() && token.as_ref().map_or(false, |v| v.is_standard_keyword()) - && !self.disabled_symbols.contains(s) => + && (self.disabled_symbols.is_empty() || !self.disabled_symbols.contains(s)) => { return Err(LexError::ImproperSymbol( s.to_string(), @@ -253,9 +254,11 @@ impl Engine { // Identifier in first position _ if segments.is_empty() && is_valid_identifier(s.chars()) => { // Make it a custom keyword/symbol if it is disabled or reserved - if self.disabled_symbols.contains(s) || token.map_or(false, |v| v.is_reserved()) + if (!self.disabled_symbols.is_empty() && self.disabled_symbols.contains(s)) + || token.map_or(false, |v| v.is_reserved()) { - if !self.custom_keywords.contains_key(s) { + if self.custom_keywords.is_empty() || !self.custom_keywords.contains_key(s) + { self.custom_keywords.insert(s.into(), None); } } diff --git a/src/api/mod.rs b/src/api/mod.rs index 17730cce..2b94013a 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -16,6 +16,8 @@ pub mod call_fn; pub mod options; +pub mod optimize; + pub mod limits; pub mod events; @@ -59,83 +61,6 @@ pub mod default_limits { pub const MAX_DYNAMIC_PARAMETERS: usize = 16; } -/// Script optimization API. -#[cfg(not(feature = "no_optimize"))] -impl Engine { - /// Control whether and how the [`Engine`] will optimize an [`AST`][crate::AST] after compilation. - /// - /// Not available under `no_optimize`. - #[inline(always)] - pub fn set_optimization_level( - &mut self, - optimization_level: crate::OptimizationLevel, - ) -> &mut Self { - self.optimization_level = optimization_level; - self - } - /// The current optimization level. - /// It controls whether and how the [`Engine`] will optimize an [`AST`][crate::AST] after compilation. - /// - /// Not available under `no_optimize`. - #[inline(always)] - #[must_use] - pub const fn optimization_level(&self) -> crate::OptimizationLevel { - self.optimization_level - } - /// Optimize the [`AST`][crate::AST] with constants defined in an external Scope. - /// An optimized copy of the [`AST`][crate::AST] is returned while the original [`AST`][crate::AST] - /// is consumed. - /// - /// Not available under `no_optimize`. - /// - /// Although optimization is performed by default during compilation, sometimes it is necessary - /// to _re_-optimize an [`AST`][crate::AST]. - /// - /// For example, when working with constants that are passed in via an external scope, it will - /// be more efficient to optimize the [`AST`][crate::AST] once again to take advantage of the - /// new constants. - /// - /// With this method, it is no longer necessary to recompile a large script. The script - /// [`AST`][crate::AST] can be compiled just once. - /// - /// Before evaluation, constants are passed into the [`Engine`] via an external scope (i.e. with - /// [`Scope::push_constant`][crate::Scope::push_constant]). - /// - /// Then, the [`AST`][crate::AST] is cloned and the copy re-optimized before running. - #[inline] - #[must_use] - pub fn optimize_ast( - &self, - scope: &crate::Scope, - ast: crate::AST, - optimization_level: crate::OptimizationLevel, - ) -> crate::AST { - let mut ast = ast; - - #[cfg(not(feature = "no_function"))] - let lib = ast - .shared_lib() - .iter_fn() - .filter(|f| f.func.is_script()) - .map(|f| { - f.func - .get_script_fn_def() - .expect("script-defined function") - .clone() - }) - .collect(); - - crate::optimizer::optimize_into_ast( - self, - scope, - ast.take_statements(), - #[cfg(not(feature = "no_function"))] - lib, - optimization_level, - ) - } -} - impl Engine { /// Set the module resolution service used by the [`Engine`]. /// @@ -149,6 +74,7 @@ impl Engine { self.module_resolver = Some(Box::new(resolver)); self } + /// Disable a particular keyword or operator in the language. /// /// # Examples @@ -190,6 +116,7 @@ impl Engine { self.disabled_symbols.insert(symbol.into()); self } + /// Register a custom operator with a precedence into the language. /// /// The operator can be a valid identifier, a reserved symbol, a disabled operator or a disabled keyword. @@ -235,18 +162,25 @@ impl Engine { // Active standard keywords cannot be made custom // Disabled keywords are OK Some(token) if token.is_standard_keyword() => { - if !self.disabled_symbols.contains(&*token.syntax()) { + if self.disabled_symbols.is_empty() + || !self.disabled_symbols.contains(&*token.syntax()) + { return Err(format!("'{}' is a reserved keyword", keyword.as_ref())); } } // Active standard symbols cannot be made custom Some(token) if token.is_standard_symbol() => { - if !self.disabled_symbols.contains(&*token.syntax()) { + if self.disabled_symbols.is_empty() + || !self.disabled_symbols.contains(&*token.syntax()) + { return Err(format!("'{}' is a reserved operator", keyword.as_ref())); } } // Active standard symbols cannot be made custom - Some(token) if !self.disabled_symbols.contains(&*token.syntax()) => { + Some(token) + if self.disabled_symbols.is_empty() + || !self.disabled_symbols.contains(&*token.syntax()) => + { return Err(format!("'{}' is a reserved symbol", keyword.as_ref())) } // Disabled symbols are OK diff --git a/src/api/optimize.rs b/src/api/optimize.rs new file mode 100644 index 00000000..7589c2fd --- /dev/null +++ b/src/api/optimize.rs @@ -0,0 +1,76 @@ +//! Module that defines the script optimization API of [`Engine`]. +#![cfg(not(feature = "no_optimize"))] + +use crate::{Engine, OptimizationLevel, Scope, AST}; + +impl Engine { + /// Control whether and how the [`Engine`] will optimize an [`AST`] after compilation. + /// + /// Not available under `no_optimize`. + #[inline(always)] + pub fn set_optimization_level(&mut self, optimization_level: OptimizationLevel) -> &mut Self { + self.optimization_level = optimization_level; + self + } + + /// The current optimization level. + /// It controls whether and how the [`Engine`] will optimize an [`AST`] after compilation. + /// + /// Not available under `no_optimize`. + #[inline(always)] + #[must_use] + pub const fn optimization_level(&self) -> OptimizationLevel { + self.optimization_level + } + + /// Optimize the [`AST`] with constants defined in an external Scope. + /// An optimized copy of the [`AST`] is returned while the original [`AST`] is consumed. + /// + /// Not available under `no_optimize`. + /// + /// Although optimization is performed by default during compilation, sometimes it is necessary + /// to _re_-optimize an [`AST`]. + /// + /// For example, when working with constants that are passed in via an external scope, + /// it will be more efficient to optimize the [`AST`] once again to take advantage of the new constants. + /// + /// With this method, it is no longer necessary to recompile a large script. + /// The script [`AST`] can be compiled just once. + /// + /// Before evaluation, constants are passed into the [`Engine`] via an external scope + /// (i.e. with [`Scope::push_constant`][Scope::push_constant]). + /// + /// Then, the [`AST`] is cloned and the copy re-optimized before running. + #[inline] + #[must_use] + pub fn optimize_ast( + &self, + scope: &Scope, + ast: AST, + optimization_level: OptimizationLevel, + ) -> AST { + let mut ast = ast; + + #[cfg(not(feature = "no_function"))] + let lib = ast + .shared_lib() + .iter_fn() + .filter(|f| f.func.is_script()) + .map(|f| { + f.func + .get_script_fn_def() + .expect("script-defined function") + .clone() + }) + .collect(); + + crate::optimizer::optimize_into_ast( + self, + scope, + ast.take_statements(), + #[cfg(not(feature = "no_function"))] + lib, + optimization_level, + ) + } +} diff --git a/src/api/register.rs b/src/api/register.rs index 8dc9ce0c..acd35914 100644 --- a/src/api/register.rs +++ b/src/api/register.rs @@ -1003,7 +1003,7 @@ impl Engine { let sub_module = iter.next().expect("contains separator").trim(); let remainder = iter.next().expect("contains separator").trim(); - if !root.contains_key(sub_module) { + if root.is_empty() || !root.contains_key(sub_module) { let mut m = Module::new(); register_static_module_raw(m.sub_modules_mut(), remainder, module); m.build_index(); diff --git a/src/ast/ast.rs b/src/ast/ast.rs index 40119fe5..7c0115e5 100644 --- a/src/ast/ast.rs +++ b/src/ast/ast.rs @@ -1,6 +1,6 @@ //! Module defining the AST (abstract syntax tree). -use super::{Expr, FnAccess, Stmt, StmtBlock, StmtBlockContainer, AST_OPTION_FLAGS::*}; +use super::{ASTFlags, Expr, FnAccess, Stmt, StmtBlock, StmtBlockContainer}; use crate::{Dynamic, FnNamespace, Identifier, Position}; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -745,12 +745,12 @@ impl AST { ) -> impl Iterator { self.statements().iter().filter_map(move |stmt| match stmt { Stmt::Var(x, options, ..) - if options.contains(AST_OPTION_CONSTANT) && include_constants - || !options.contains(AST_OPTION_CONSTANT) && include_variables => + if options.contains(ASTFlags::CONSTANT) && include_constants + || !options.contains(ASTFlags::CONSTANT) && include_variables => { let (name, expr, ..) = x.as_ref(); if let Some(value) = expr.get_literal_value() { - Some((name.as_str(), options.contains(AST_OPTION_CONSTANT), value)) + Some((name.as_str(), options.contains(ASTFlags::CONSTANT), value)) } else { None } diff --git a/src/ast/expr.rs b/src/ast/expr.rs index af1fd390..af352bdf 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -1,6 +1,6 @@ //! Module defining script expressions. -use super::{ASTNode, Ident, Stmt, StmtBlock}; +use super::{ASTFlags, ASTNode, Ident, Stmt, StmtBlock}; use crate::engine::{KEYWORD_FN_PTR, OP_EXCLUSIVE_RANGE, OP_INCLUSIVE_RANGE}; use crate::func::hashing::ALT_ZERO_HASH; use crate::tokenizer::Token; @@ -168,8 +168,6 @@ impl FnCallHashes { #[derive(Clone, Default, Hash)] pub struct FnCallExpr { /// Namespace of the function, if any. - /// - /// Not available under `no_module`. #[cfg(not(feature = "no_module"))] pub namespace: Option, /// Function name. @@ -199,16 +197,18 @@ impl fmt::Debug for FnCallExpr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut ff = f.debug_struct("FnCallExpr"); #[cfg(not(feature = "no_module"))] - self.namespace.as_ref().map(|ns| ff.field("namespace", ns)); - ff.field("name", &self.name) - .field("hash", &self.hashes) - .field("arg_exprs", &self.args); - if !self.constants.is_empty() { - ff.field("constant_args", &self.constants); + if let Some(ref ns) = self.namespace { + ff.field("namespace", ns); } if self.capture_parent_scope { ff.field("capture_parent_scope", &self.capture_parent_scope); } + ff.field("hash", &self.hashes) + .field("name", &self.name) + .field("args", &self.args); + if !self.constants.is_empty() { + ff.field("constants", &self.constants); + } ff.field("pos", &self.pos); ff.finish() } @@ -369,8 +369,6 @@ pub enum Expr { /// Integer constant. IntegerConstant(INT, Position), /// Floating-point constant. - /// - /// Not available under `no_float`. #[cfg(not(feature = "no_float"))] FloatConstant(FloatWrapper, Position), /// Character constant. @@ -409,6 +407,8 @@ pub enum Expr { Box<((Identifier, u64), (Identifier, u64), ImmutableString)>, Position, ), + /// xxx `.` method `(` expr `,` ... `)` + MethodCall(Box, Position), /// Stack slot for function calls. See [`FnCallExpr`] for more details. /// /// This variant does not map to any language structure. It is used in function calls with @@ -419,10 +419,15 @@ pub enum Expr { Stmt(Box), /// func `(` expr `,` ... `)` FnCall(Box, Position), - /// lhs `.` rhs - boolean variable is a dummy - Dot(Box, bool, Position), - /// lhs `[` rhs `]` - boolean indicates whether the dotting/indexing chain stops - Index(Box, bool, Position), + /// lhs `.` rhs + Dot(Box, ASTFlags, Position), + /// lhs `[` rhs `]` + /// + /// ### Flags + /// + /// [`NONE`][ASTFlags::NONE] = recurse into the indexing chain + /// [`BREAK`][ASTFlags::BREAK] = terminate the indexing chain + Index(Box, ASTFlags, Position), /// lhs `&&` rhs And(Box, Position), /// lhs `||` rhs @@ -484,6 +489,7 @@ impl fmt::Debug for Expr { f.write_str(")") } Self::Property(x, ..) => write!(f, "Property({})", x.2), + Self::MethodCall(x, ..) => f.debug_tuple("MethodCall").field(x).finish(), Self::Stack(x, ..) => write!(f, "ConstantArg[{}]", x), Self::Stmt(x) => { let pos = x.span(); @@ -570,11 +576,7 @@ impl Expr { if !x.is_qualified() && x.args.len() == 1 && x.name == KEYWORD_FN_PTR => { if let Expr::StringConstant(ref s, ..) = x.args[0] { - if let Ok(fn_ptr) = FnPtr::new(s) { - fn_ptr.into() - } else { - return None; - } + FnPtr::new(s).ok()?.into() } else { return None; } @@ -711,7 +713,7 @@ impl Expr { | Self::InterpolatedString(.., pos) | Self::Property(.., pos) => *pos, - Self::FnCall(x, ..) => x.pos, + Self::FnCall(x, ..) | Self::MethodCall(x, ..) => x.pos, Self::Stmt(x) => x.position(), } @@ -759,6 +761,7 @@ impl Expr { | Self::Variable(.., pos, _) | Self::Stack(.., pos) | Self::FnCall(.., pos) + | Self::MethodCall(.., pos) | Self::Custom(.., pos) | Self::InterpolatedString(.., pos) | Self::Property(.., pos) => *pos = new_pos, @@ -842,6 +845,7 @@ impl Expr { | Self::StringConstant(..) | Self::InterpolatedString(..) | Self::FnCall(..) + | Self::MethodCall(..) | Self::Stmt(..) | Self::Dot(..) | Self::Index(..) diff --git a/src/ast/flags.rs b/src/ast/flags.rs index 1e0fdc63..264a6374 100644 --- a/src/ast/flags.rs +++ b/src/ast/flags.rs @@ -1,175 +1,31 @@ //! Module defining script options. -use std::ops::{Add, AddAssign, BitAnd, BitAndAssign, BitOr, BitOrAssign, Not, Sub, SubAssign}; +use bitflags::bitflags; #[cfg(feature = "no_std")] use std::prelude::v1::*; /// A type representing the access mode of a function. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum FnAccess { - /// Public function. - Public, /// Private function. Private, + /// Public function. + Public, } -/// _(internals)_ A type that holds a configuration option with bit-flags. -/// Exported under the `internals` feature only. -/// -/// Functionality-wise, this type is a naive and simplistic implementation of -/// [`bit_flags`](https://crates.io/crates/bitflags). It is re-implemented to avoid pulling in yet -/// one more dependency. -#[derive(PartialEq, Eq, Copy, Clone, Hash, Default)] -pub struct OptionFlags(u8); - -impl OptionFlags { - /// Does this [`OptionFlags`] contain a particular option flag? - #[inline(always)] - #[must_use] - pub const fn contains(self, flag: Self) -> bool { - self.0 & flag.0 != 0 - } -} - -impl Not for OptionFlags { - type Output = Self; - - /// Return the negation of the [`OptionFlags`]. - #[inline(always)] - fn not(self) -> Self::Output { - 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) - } -} - -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) - } -} - -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 - } -} - -/// _(internals)_ Option bit-flags for [`AST`][super::AST] nodes. -/// Exported under the `internals` feature only. -#[allow(non_snake_case)] -pub mod AST_OPTION_FLAGS { - use super::OptionFlags; - - /// _(internals)_ No options for the [`AST`][crate::AST] node. +bitflags! { + /// _(internals)_ A type that holds a configuration option with bit-flags. /// Exported under the `internals` feature only. - pub const AST_OPTION_NONE: OptionFlags = OptionFlags(0b0000_0000); - /// _(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 to the outside (i.e. public). - /// Exported under the `internals` feature only. - pub const AST_OPTION_EXPORTED: OptionFlags = OptionFlags(0b0000_0010); - /// _(internals)_ The [`AST`][crate::AST] node is in negated mode - /// (meaning whatever information is the opposite). - /// 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: 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_EXPORTED.0 | AST_OPTION_NEGATED.0 | AST_OPTION_BREAK.0, - ); - - impl std::fmt::Debug for OptionFlags { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - fn write_option( - options: &OptionFlags, - f: &mut std::fmt::Formatter<'_>, - num_flags: &mut usize, - flag: OptionFlags, - name: &str, - ) -> std::fmt::Result { - if options.contains(flag) { - if *num_flags > 0 { - f.write_str("+")?; - } - f.write_str(name)?; - *num_flags += 1; - } - Ok(()) - } - - let num_flags = &mut 0; - - 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_NEGATED, "Negated")?; - write_option(self, f, num_flags, AST_OPTION_BREAK, "Break")?; - f.write_str(")")?; - - Ok(()) - } + pub struct ASTFlags: u8 { + /// No options for the [`AST`][crate::AST] node. + const NONE = 0b0000_0000; + /// The [`AST`][crate::AST] node is read-only. + const CONSTANT = 0b0000_0001; + /// The [`AST`][crate::AST] node is exposed to the outside (i.e. public). + const EXPORTED = 0b0000_0010; + /// The [`AST`][crate::AST] node is negated (i.e. whatever information is the opposite). + const NEGATED = 0b0000_0100; + /// The [`AST`][crate::AST] node breaks out of normal control flow. + const BREAK = 0b0000_1000; } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3bfe2389..16be3bb9 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -9,7 +9,7 @@ pub mod stmt; pub use ast::{ASTNode, AST}; pub use expr::{BinaryExpr, CustomExpr, Expr, FnCallExpr, FnCallHashes}; -pub use flags::{FnAccess, OptionFlags, AST_OPTION_FLAGS}; +pub use flags::{ASTFlags, FnAccess}; pub use ident::Ident; #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_function"))] diff --git a/src/ast/script_fn.rs b/src/ast/script_fn.rs index f01bc8eb..3d810492 100644 --- a/src/ast/script_fn.rs +++ b/src/ast/script_fn.rs @@ -34,8 +34,6 @@ pub struct ScriptFnDef { /// Function body. pub body: StmtBlock, /// Encapsulated AST environment, if any. - /// - /// Not available under `no_module` or `no_function`. #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_function"))] pub environ: Option, @@ -75,8 +73,15 @@ impl fmt::Display for ScriptFnDef { /// Not available under `no_function`. /// /// Created by [`AST::iter_functions`][super::AST::iter_functions]. -#[derive(Debug, Eq, PartialEq, Clone, Hash)] +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Hash)] +#[non_exhaustive] pub struct ScriptFnMetadata<'a> { + /// Function name. + pub name: &'a str, + /// Function parameters (if any). + pub params: Box<[&'a str]>, + /// Function access mode. + pub access: FnAccess, /// _(metadata)_ Function doc-comments (if any). /// Exported under the `metadata` feature only. /// @@ -86,14 +91,9 @@ pub struct ScriptFnMetadata<'a> { /// /// Leading white-spaces are stripped, and each string slice always starts with the /// corresponding doc-comment leader: `///` or `/**`. - #[cfg(feature = "metadata")] - pub comments: Vec<&'a str>, /// Function access mode. - pub access: FnAccess, - /// Function name. - pub name: &'a str, - /// Function parameters (if any). - pub params: Vec<&'a str>, + #[cfg(feature = "metadata")] + pub comments: Box<[&'a str]>, } impl fmt::Display for ScriptFnMetadata<'_> { @@ -119,29 +119,24 @@ impl<'a> From<&'a ScriptFnDef> for ScriptFnMetadata<'a> { #[inline] fn from(value: &'a ScriptFnDef) -> Self { Self { - #[cfg(feature = "metadata")] - comments: value - .comments - .as_ref() - .map_or_else(|| Vec::new(), |v| v.iter().map(Box::as_ref).collect()), - access: value.access, name: &value.name, - params: value.params.iter().map(|s| s.as_str()).collect(), - } - } -} - -impl std::cmp::PartialOrd for ScriptFnMetadata<'_> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl std::cmp::Ord for ScriptFnMetadata<'_> { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match self.name.cmp(other.name) { - std::cmp::Ordering::Equal => self.params.len().cmp(&other.params.len()), - cmp => cmp, + params: value + .params + .iter() + .map(|s| s.as_str()) + .collect::>() + .into_boxed_slice(), + access: value.access, + #[cfg(feature = "metadata")] + comments: value.comments.as_ref().map_or_else( + || Vec::new().into_boxed_slice(), + |v| { + v.iter() + .map(Box::as_ref) + .collect::>() + .into_boxed_slice() + }, + ), } } } diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index 2a4e60bc..ae6d5353 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -1,6 +1,6 @@ //! Module defining script statements. -use super::{ASTNode, BinaryExpr, Expr, FnCallExpr, Ident, OptionFlags, AST_OPTION_FLAGS::*}; +use super::{ASTFlags, ASTNode, BinaryExpr, Expr, FnCallExpr, Ident}; use crate::engine::KEYWORD_EVAL; use crate::tokenizer::{Span, Token}; use crate::{calc_fn_hash, Position, StaticVec, INT}; @@ -340,24 +340,20 @@ pub enum Stmt { While(Box<(Expr, StmtBlock)>, Position), /// `do` `{` stmt `}` `while`|`until` expr /// - /// ### Option Flags + /// ### Flags /// - /// * [`AST_OPTION_NONE`] = `while` - /// * [`AST_OPTION_NEGATED`] = `until` - Do(Box<(Expr, StmtBlock)>, OptionFlags, Position), + /// * [`NONE`][ASTFlags::NONE] = `while` + /// * [`NEGATED`][ASTFlags::NEGATED] = `until` + Do(Box<(Expr, StmtBlock)>, ASTFlags, Position), /// `for` `(` id `,` counter `)` `in` expr `{` stmt `}` For(Box<(Ident, Option, Expr, StmtBlock)>, Position), /// \[`export`\] `let`|`const` id `=` expr /// - /// ### Option Flags + /// ### Flags /// - /// * [`AST_OPTION_EXPORTED`] = `export` - /// * [`AST_OPTION_CONSTANT`] = `const` - Var( - Box<(Ident, Expr, Option)>, - OptionFlags, - Position, - ), + /// * [`EXPORTED`][ASTFlags::EXPORTED] = `export` + /// * [`CONSTANT`][ASTFlags::CONSTANT] = `const` + Var(Box<(Ident, Expr, Option)>, ASTFlags, Position), /// expr op`=` expr Assignment(Box<(Option>, BinaryExpr)>, Position), /// func `(` expr `,` ... `)` @@ -373,18 +369,18 @@ pub enum Stmt { Expr(Box), /// `continue`/`break` /// - /// ### Option Flags + /// ### Flags /// - /// * [`AST_OPTION_NONE`] = `continue` - /// * [`AST_OPTION_BREAK`] = `break` - BreakLoop(OptionFlags, Position), + /// * [`NONE`][ASTFlags::NONE] = `continue` + /// * [`BREAK`][ASTFlags::BREAK] = `break` + BreakLoop(ASTFlags, Position), /// `return`/`throw` /// - /// ### Option Flags + /// ### Flags /// - /// * [`AST_OPTION_NONE`] = `return` - /// * [`AST_OPTION_BREAK`] = `throw` - Return(Option>, OptionFlags, Position), + /// * [`NONE`][ASTFlags::NONE] = `return` + /// * [`BREAK`][ASTFlags::BREAK] = `throw` + Return(Option>, ASTFlags, Position), /// `import` expr `as` alias /// /// Not available under `no_module`. @@ -590,7 +586,7 @@ impl Stmt { // Loops that exit can be pure because it can never be infinite. Self::While(x, ..) if matches!(x.0, Expr::BoolConstant(false, ..)) => true, Self::Do(x, options, ..) if matches!(x.0, Expr::BoolConstant(..)) => match x.0 { - Expr::BoolConstant(cond, ..) if cond == options.contains(AST_OPTION_NEGATED) => { + Expr::BoolConstant(cond, ..) if cond == options.contains(ASTFlags::NEGATED) => { x.1.iter().all(Stmt::is_pure) } _ => false, diff --git a/src/bin/rhai-run.rs b/src/bin/rhai-run.rs index 257a7b08..b6e4e6fa 100644 --- a/src/bin/rhai-run.rs +++ b/src/bin/rhai-run.rs @@ -47,6 +47,7 @@ fn main() { }, }; + // Initialize scripting engine let mut engine = Engine::new(); #[cfg(not(feature = "no_optimize"))] diff --git a/src/eval/chaining.rs b/src/eval/chaining.rs index dcf50ab1..8b87e79c 100644 --- a/src/eval/chaining.rs +++ b/src/eval/chaining.rs @@ -2,7 +2,7 @@ #![cfg(any(not(feature = "no_index"), not(feature = "no_object")))] use super::{EvalState, GlobalRuntimeState, Target}; -use crate::ast::{Expr, OpAssignment}; +use crate::ast::{ASTFlags, Expr, OpAssignment}; use crate::types::dynamic::Union; use crate::{Dynamic, Engine, Module, Position, RhaiResult, RhaiResultOf, Scope, StaticVec, ERR}; use std::hash::Hash; @@ -127,15 +127,15 @@ impl Engine { root: (&str, Position), parent: &Expr, rhs: &Expr, - terminate_chaining: bool, + parent_options: ASTFlags, idx_values: &mut StaticVec, chain_type: ChainType, level: usize, new_val: Option<((Dynamic, Position), (Option, Position))>, ) -> RhaiResultOf<(Dynamic, bool)> { let _parent = parent; + let _parent_options = parent_options; let is_ref_mut = target.is_ref(); - let _terminate_chaining = terminate_chaining; // Pop the last index value let idx_val = idx_values.pop().unwrap(); @@ -151,8 +151,8 @@ impl Engine { match rhs { // xxx[idx].expr... | xxx[idx][expr]... - Expr::Dot(x, term, x_pos) | Expr::Index(x, term, x_pos) - if !_terminate_chaining => + Expr::Dot(x, options, x_pos) | Expr::Index(x, options, x_pos) + if !_parent_options.contains(ASTFlags::BREAK) => { #[cfg(feature = "debugging")] self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?; @@ -169,7 +169,7 @@ impl Engine { let obj_ptr = &mut obj; match self.eval_dot_index_chain_helper( - global, state, lib, this_ptr, obj_ptr, root, rhs, &x.rhs, *term, + global, state, lib, this_ptr, obj_ptr, root, rhs, &x.rhs, *options, idx_values, rhs_chain, level, new_val, ) { Ok((result, true)) if is_obj_temp_val => { @@ -252,7 +252,7 @@ impl Engine { ChainType::Dotting => { match rhs { // xxx.fn_name(arg_expr_list) - Expr::FnCall(x, pos) if !x.is_qualified() && new_val.is_none() => { + Expr::MethodCall(x, pos) if !x.is_qualified() && new_val.is_none() => { let crate::ast::FnCallExpr { name, hashes, .. } = x.as_ref(); let call_args = &mut idx_val.into_fn_call_args(); @@ -271,11 +271,11 @@ impl Engine { result } // xxx.fn_name(...) = ??? - Expr::FnCall(..) if new_val.is_some() => { + Expr::MethodCall(..) if new_val.is_some() => { unreachable!("method call cannot be assigned to") } // xxx.module::fn_name(...) - syntax error - Expr::FnCall(..) => { + Expr::MethodCall(..) => { unreachable!("function call in dot chain should not be namespace-qualified") } // {xxx:map}.id op= ??? @@ -410,7 +410,7 @@ impl Engine { ) } // {xxx:map}.sub_lhs[expr] | {xxx:map}.sub_lhs.expr - Expr::Index(x, term, x_pos) | Expr::Dot(x, term, x_pos) + Expr::Index(x, options, x_pos) | Expr::Dot(x, options, x_pos) if target.is::() => { let _node = &x.lhs; @@ -428,7 +428,7 @@ impl Engine { )? } // {xxx:map}.fn_name(arg_expr_list)[expr] | {xxx:map}.fn_name(arg_expr_list).expr - Expr::FnCall(ref x, pos) if !x.is_qualified() => { + Expr::MethodCall(ref x, pos) if !x.is_qualified() => { let crate::ast::FnCallExpr { name, hashes, .. } = x.as_ref(); let call_args = &mut idx_val.into_fn_call_args(); @@ -448,7 +448,7 @@ impl Engine { result?.0.into() } // {xxx:map}.module::fn_name(...) - syntax error - Expr::FnCall(..) => unreachable!( + Expr::MethodCall(..) => unreachable!( "function call in dot chain should not be namespace-qualified" ), // Others - syntax error @@ -457,13 +457,13 @@ impl Engine { let rhs_chain = rhs.into(); self.eval_dot_index_chain_helper( - global, state, lib, this_ptr, val_target, root, rhs, &x.rhs, *term, + global, state, lib, this_ptr, val_target, root, rhs, &x.rhs, *options, idx_values, rhs_chain, level, new_val, ) .map_err(|err| err.fill_position(*x_pos)) } // xxx.sub_lhs[expr] | xxx.sub_lhs.expr - Expr::Index(x, term, x_pos) | Expr::Dot(x, term, x_pos) => { + Expr::Index(x, options, x_pos) | Expr::Dot(x, options, x_pos) => { let _node = &x.lhs; match x.lhs { @@ -509,7 +509,7 @@ impl Engine { let (result, may_be_changed) = self .eval_dot_index_chain_helper( global, state, lib, this_ptr, val, root, rhs, &x.rhs, - *term, idx_values, rhs_chain, level, new_val, + *options, idx_values, rhs_chain, level, new_val, ) .map_err(|err| err.fill_position(*x_pos))?; @@ -549,7 +549,7 @@ impl Engine { Ok((result, may_be_changed)) } // xxx.fn_name(arg_expr_list)[expr] | xxx.fn_name(arg_expr_list).expr - Expr::FnCall(ref f, pos) if !f.is_qualified() => { + Expr::MethodCall(ref f, pos) if !f.is_qualified() => { let crate::ast::FnCallExpr { name, hashes, .. } = f.as_ref(); let rhs_chain = rhs.into(); let args = &mut idx_val.into_fn_call_args(); @@ -570,13 +570,13 @@ impl Engine { let val = &mut val.into(); self.eval_dot_index_chain_helper( - global, state, lib, this_ptr, val, root, rhs, &x.rhs, *term, + global, state, lib, this_ptr, val, root, rhs, &x.rhs, *options, idx_values, rhs_chain, level, new_val, ) .map_err(|err| err.fill_position(pos)) } // xxx.module::fn_name(...) - syntax error - Expr::FnCall(..) => unreachable!( + Expr::MethodCall(..) => unreachable!( "function call in dot chain should not be namespace-qualified" ), // Others - syntax error @@ -602,18 +602,18 @@ impl Engine { level: usize, new_val: Option<((Dynamic, Position), (Option, Position))>, ) -> RhaiResult { - let (crate::ast::BinaryExpr { lhs, rhs }, chain_type, term, op_pos) = match expr { + let (crate::ast::BinaryExpr { lhs, rhs }, chain_type, options, op_pos) = match expr { #[cfg(not(feature = "no_index"))] - Expr::Index(x, term, pos) => (x.as_ref(), ChainType::Indexing, *term, *pos), + Expr::Index(x, options, pos) => (x.as_ref(), ChainType::Indexing, *options, *pos), #[cfg(not(feature = "no_object"))] - Expr::Dot(x, term, pos) => (x.as_ref(), ChainType::Dotting, *term, *pos), + Expr::Dot(x, options, pos) => (x.as_ref(), ChainType::Dotting, *options, *pos), expr => unreachable!("Expr::Index or Expr::Dot expected but gets {:?}", expr), }; let idx_values = &mut StaticVec::new_const(); self.eval_dot_index_chain_arguments( - scope, global, state, lib, this_ptr, rhs, term, chain_type, idx_values, 0, level, + scope, global, state, lib, this_ptr, rhs, options, chain_type, idx_values, 0, level, )?; let is_assignment = new_val.is_some(); @@ -634,7 +634,7 @@ impl Engine { let root = (x.2.as_str(), *var_pos); self.eval_dot_index_chain_helper( - global, state, lib, &mut None, obj_ptr, root, expr, rhs, term, idx_values, + global, state, lib, &mut None, obj_ptr, root, expr, rhs, options, idx_values, chain_type, level, new_val, ) .map(|(v, ..)| v) @@ -648,7 +648,7 @@ impl Engine { let obj_ptr = &mut value.into(); let root = ("", expr.start_position()); self.eval_dot_index_chain_helper( - global, state, lib, this_ptr, obj_ptr, root, expr, rhs, term, idx_values, + global, state, lib, this_ptr, obj_ptr, root, expr, rhs, options, idx_values, chain_type, level, new_val, ) .map(|(v, ..)| if is_assignment { Dynamic::UNIT } else { v }) @@ -668,7 +668,7 @@ impl Engine { lib: &[&Module], this_ptr: &mut Option<&mut Dynamic>, expr: &Expr, - terminate_chaining: bool, + parent_options: ASTFlags, parent_chain_type: ChainType, idx_values: &mut StaticVec, size: usize, @@ -681,7 +681,7 @@ impl Engine { match expr { #[cfg(not(feature = "no_object"))] - Expr::FnCall(x, ..) + Expr::MethodCall(x, ..) if _parent_chain_type == ChainType::Dotting && !x.is_qualified() => { let crate::ast::FnCallExpr { @@ -705,7 +705,7 @@ impl Engine { idx_values.push(super::ChainArgument::from_fn_call_args(values, pos)); } #[cfg(not(feature = "no_object"))] - Expr::FnCall(..) if _parent_chain_type == ChainType::Dotting => { + Expr::MethodCall(..) if _parent_chain_type == ChainType::Dotting => { unreachable!("function call in dot chain should not be namespace-qualified") } @@ -715,7 +715,9 @@ impl Engine { } Expr::Property(..) => unreachable!("unexpected Expr::Property for indexing"), - Expr::Index(x, term, ..) | Expr::Dot(x, term, ..) if !terminate_chaining => { + Expr::Index(x, options, ..) | Expr::Dot(x, options, ..) + if !parent_options.contains(ASTFlags::BREAK) => + { let crate::ast::BinaryExpr { lhs, rhs, .. } = x.as_ref(); // Evaluate in left-to-right order @@ -727,7 +729,7 @@ impl Engine { Expr::Property(..) => unreachable!("unexpected Expr::Property for indexing"), #[cfg(not(feature = "no_object"))] - Expr::FnCall(x, ..) + Expr::MethodCall(x, ..) if _parent_chain_type == ChainType::Dotting && !x.is_qualified() => { let crate::ast::FnCallExpr { @@ -750,7 +752,7 @@ impl Engine { super::ChainArgument::from_fn_call_args(values, pos) } #[cfg(not(feature = "no_object"))] - Expr::FnCall(..) if _parent_chain_type == ChainType::Dotting => { + Expr::MethodCall(..) if _parent_chain_type == ChainType::Dotting => { unreachable!("function call in dot chain should not be namespace-qualified") } #[cfg(not(feature = "no_object"))] @@ -773,8 +775,8 @@ impl Engine { let chain_type = expr.into(); self.eval_dot_index_chain_arguments( - scope, global, state, lib, this_ptr, rhs, *term, chain_type, idx_values, size, - level, + scope, global, state, lib, this_ptr, rhs, *options, chain_type, idx_values, + size, level, )?; idx_values.push(lhs_arg_val); @@ -905,7 +907,7 @@ impl Engine { self.make_type_mismatch_err::(idx.type_name(), idx_pos) })?; - if _add_if_not_found && !map.contains_key(index.as_str()) { + if _add_if_not_found && (map.is_empty() || !map.contains_key(index.as_str())) { map.insert(index.clone().into(), Dynamic::UNIT); } diff --git a/src/eval/expr.rs b/src/eval/expr.rs index d6ea6025..b2a155b3 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -20,7 +20,7 @@ impl Engine { state: &mut EvalState, namespace: &crate::module::Namespace, ) -> Option> { - let root = &namespace[0].name; + let root = namespace.root(); // Qualified - check if the root module is directly indexed let index = if state.always_search_scope { @@ -72,32 +72,24 @@ impl Engine { (_, Some((namespace, hash_var)), var_name) => { // foo:bar::baz::VARIABLE if let Some(module) = self.search_imports(global, state, namespace) { - return match module.get_qualified_var(*hash_var) { - Ok(target) => { - let mut target = target.clone(); - // Module variables are constant - target.set_access_mode(AccessMode::ReadOnly); - Ok((target.into(), *_var_pos)) - } - Err(err) => Err(match *err { - ERR::ErrorVariableNotFound(..) => ERR::ErrorVariableNotFound( - format!( - "{}{}{}", - namespace, - crate::tokenizer::Token::DoubleColon.literal_syntax(), - var_name - ), - namespace[0].pos, - ) - .into(), - _ => err.fill_position(*_var_pos), - }), + return if let Some(mut target) = module.get_qualified_var(*hash_var) { + // Module variables are constant + target.set_access_mode(AccessMode::ReadOnly); + Ok((target.into(), *_var_pos)) + } else { + let sep = crate::tokenizer::Token::DoubleColon.literal_syntax(); + + Err(ERR::ErrorVariableNotFound( + format!("{}{}{}", namespace, sep, var_name), + namespace.position(), + ) + .into()) }; } // global::VARIABLE #[cfg(not(feature = "no_function"))] - if namespace.len() == 1 && namespace[0].name == crate::engine::KEYWORD_GLOBAL { + if namespace.len() == 1 && namespace.root() == crate::engine::KEYWORD_GLOBAL { if let Some(ref constants) = global.constants { if let Some(value) = crate::func::locked_write(constants).get_mut(var_name) @@ -109,19 +101,19 @@ impl Engine { } } + let sep = crate::tokenizer::Token::DoubleColon.literal_syntax(); + return Err(ERR::ErrorVariableNotFound( - format!( - "{}{}{}", - namespace, - crate::tokenizer::Token::DoubleColon.literal_syntax(), - var_name - ), - namespace[0].pos, + format!("{}{}{}", namespace, sep, var_name), + namespace.position(), ) .into()); } - Err(ERR::ErrorModuleNotFound(namespace.to_string(), namespace[0].pos).into()) + Err( + ERR::ErrorModuleNotFound(namespace.to_string(), namespace.position()) + .into(), + ) } }, _ => unreachable!("Expr::Variable expected but gets {:?}", expr), diff --git a/src/eval/global_state.rs b/src/eval/global_state.rs index b15fa6ab..1363f108 100644 --- a/src/eval/global_state.rs +++ b/src/eval/global_state.rs @@ -24,13 +24,9 @@ pub type GlobalConstants = #[derive(Clone)] pub struct GlobalRuntimeState<'a> { /// Stack of module names. - /// - /// Not available under `no_module`. #[cfg(not(feature = "no_module"))] keys: crate::StaticVec, /// Stack of imported [modules][crate::Module]. - /// - /// Not available under `no_module`. #[cfg(not(feature = "no_module"))] modules: crate::StaticVec>, /// Source of the current context. diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index 8079cb58..a0861c88 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -3,7 +3,7 @@ use super::{EvalContext, EvalState, GlobalRuntimeState, Target}; use crate::api::events::VarDefInfo; use crate::ast::{ - BinaryExpr, Expr, Ident, OpAssignment, Stmt, SwitchCases, TryCatchBlock, AST_OPTION_FLAGS::*, + ASTFlags, BinaryExpr, Expr, Ident, OpAssignment, Stmt, SwitchCases, TryCatchBlock, }; use crate::func::get_hasher; use crate::types::dynamic::{AccessMode, Union}; @@ -537,7 +537,7 @@ impl Engine { // Do loop Stmt::Do(x, options, ..) => loop { let (expr, body) = x.as_ref(); - let is_while = !options.contains(AST_OPTION_NEGATED); + let is_while = !options.contains(ASTFlags::NEGATED); if !body.is_empty() { match self @@ -700,7 +700,7 @@ impl Engine { // Continue/Break statement Stmt::BreakLoop(options, pos) => { - Err(ERR::LoopBreak(options.contains(AST_OPTION_BREAK), *pos).into()) + Err(ERR::LoopBreak(options.contains(ASTFlags::BREAK), *pos).into()) } // Try/Catch statement @@ -790,12 +790,12 @@ impl Engine { } // Throw value - Stmt::Return(Some(expr), options, pos) if options.contains(AST_OPTION_BREAK) => self + Stmt::Return(Some(expr), options, pos) if options.contains(ASTFlags::BREAK) => self .eval_expr(scope, global, state, lib, this_ptr, expr, level) .and_then(|v| Err(ERR::ErrorRuntime(v.flatten(), *pos).into())), // Empty throw - Stmt::Return(None, options, pos) if options.contains(AST_OPTION_BREAK) => { + Stmt::Return(None, options, pos) if options.contains(ASTFlags::BREAK) => { Err(ERR::ErrorRuntime(Dynamic::UNIT, *pos).into()) } @@ -815,12 +815,12 @@ impl Engine { Stmt::Var(x, options, pos) => { let (Ident { name: var_name, .. }, expr, index) = x.as_ref(); - let access = if options.contains(AST_OPTION_CONSTANT) { + let access = if options.contains(ASTFlags::CONSTANT) { AccessMode::ReadOnly } else { AccessMode::ReadWrite }; - let export = options.contains(AST_OPTION_EXPORTED); + let export = options.contains(ASTFlags::EXPORTED); // Check variable definition filter let result = if let Some(ref filter) = self.def_var_filter { diff --git a/src/func/builtin.rs b/src/func/builtin.rs index 1da1d250..5477893c 100644 --- a/src/func/builtin.rs +++ b/src/func/builtin.rs @@ -288,7 +288,7 @@ pub fn get_builtin_binary_op_fn(op: &str, x: &Dynamic, y: &Dynamic) -> Option Some(|_, args| { let blob = &*args[0].read_lock::().expect(BUILTIN); let x = (args[1].as_int().expect("`INT`") & 0x000000ff) as u8; - Ok(blob.contains(&x).into()) + Ok((!blob.is_empty() && blob.contains(&x)).into()) }), _ => None, }; diff --git a/src/func/call.rs b/src/func/call.rs index da327258..dc3090af 100644 --- a/src/func/call.rs +++ b/src/func/call.rs @@ -1353,7 +1353,7 @@ impl Engine { let module = self .search_imports(global, state, namespace) - .ok_or_else(|| ERR::ErrorModuleNotFound(namespace.to_string(), namespace[0].pos))?; + .ok_or_else(|| ERR::ErrorModuleNotFound(namespace.to_string(), namespace.position()))?; // First search in script-defined functions (can override built-in) let func = match module.get_qualified_fn(hash) { diff --git a/src/func/callable_function.rs b/src/func/callable_function.rs index 86e4d4f3..1480840d 100644 --- a/src/func/callable_function.rs +++ b/src/func/callable_function.rs @@ -21,8 +21,6 @@ pub enum CallableFunction { /// A plugin function, Plugin(Shared), /// A script-defined function. - /// - /// Not available under `no_function`. #[cfg(not(feature = "no_function"))] Script(Shared), } diff --git a/src/func/native.rs b/src/func/native.rs index 66e793a3..c3932824 100644 --- a/src/func/native.rs +++ b/src/func/native.rs @@ -41,20 +41,30 @@ pub use std::sync::Arc as Shared; #[allow(dead_code)] pub use std::cell::RefCell as Locked; -/// Lock guard for synchronized shared object. +/// Read-only lock guard for synchronized shared object. #[cfg(not(feature = "sync"))] #[allow(dead_code)] -pub type LockGuard<'a, T> = std::cell::RefMut<'a, T>; +pub type LockGuard<'a, T> = std::cell::Ref<'a, T>; + +/// Mutable lock guard for synchronized shared object. +#[cfg(not(feature = "sync"))] +#[allow(dead_code)] +pub type LockGuardMut<'a, T> = std::cell::RefMut<'a, T>; /// Synchronized shared object. #[cfg(feature = "sync")] #[allow(dead_code)] pub use std::sync::RwLock as Locked; -/// Lock guard for synchronized shared object. +/// Read-only lock guard for synchronized shared object. #[cfg(feature = "sync")] #[allow(dead_code)] -pub type LockGuard<'a, T> = std::sync::RwLockWriteGuard<'a, T>; +pub type LockGuard<'a, T> = std::sync::RwLockReadGuard<'a, T>; + +/// Mutable lock guard for synchronized shared object. +#[cfg(feature = "sync")] +#[allow(dead_code)] +pub type LockGuardMut<'a, T> = std::sync::RwLockWriteGuard<'a, T>; /// Context of a native Rust function call. #[derive(Debug)] @@ -374,11 +384,23 @@ pub fn shared_take(value: Shared) -> T { shared_try_take(value).ok().expect("not shared") } -/// Lock a [`Locked`] resource. +/// Lock a [`Locked`] resource for mutable access. #[inline(always)] #[must_use] #[allow(dead_code)] -pub fn locked_write<'a, T>(value: &'a Locked) -> LockGuard<'a, T> { +pub fn locked_read<'a, T>(value: &'a Locked) -> LockGuard<'a, T> { + #[cfg(not(feature = "sync"))] + return value.borrow(); + + #[cfg(feature = "sync")] + return value.read().unwrap(); +} + +/// Lock a [`Locked`] resource for mutable access. +#[inline(always)] +#[must_use] +#[allow(dead_code)] +pub fn locked_write<'a, T>(value: &'a Locked) -> LockGuardMut<'a, T> { #[cfg(not(feature = "sync"))] return value.borrow_mut(); diff --git a/src/lib.rs b/src/lib.rs index 07612641..0c089e60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -260,9 +260,8 @@ pub use parser::ParseState; #[cfg(feature = "internals")] pub use ast::{ - ASTNode, BinaryExpr, ConditionalStmtBlock, CustomExpr, Expr, FnCallExpr, FnCallHashes, Ident, - OpAssignment, OptionFlags, ScriptFnDef, Stmt, StmtBlock, SwitchCases, TryCatchBlock, - AST_OPTION_FLAGS, + ASTFlags, ASTNode, BinaryExpr, ConditionalStmtBlock, CustomExpr, Expr, FnCallExpr, + FnCallHashes, Ident, OpAssignment, ScriptFnDef, Stmt, StmtBlock, SwitchCases, TryCatchBlock, }; #[cfg(feature = "internals")] @@ -366,7 +365,7 @@ pub type StaticVec = smallvec::SmallVec<[T; 3]>; /// /// Under `no_closure`, this type aliases to [`StaticVec`][crate::StaticVec] instead. #[cfg(not(feature = "no_closure"))] -type FnArgsVec = smallvec::SmallVec<[T; 8]>; +type FnArgsVec = smallvec::SmallVec<[T; 5]>; /// Inline arguments storage for function calls. /// This type aliases to [`StaticVec`][crate::StaticVec]. diff --git a/src/module/mod.rs b/src/module/mod.rs index 8139a155..4c04142e 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -22,14 +22,14 @@ use std::{ }; /// A type representing the namespace of a function. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum FnNamespace { - /// Expose to global namespace. - Global, /// Module namespace only. /// /// Ignored under `no_module`. Internal, + /// Expose to global namespace. + Global, } /// A type containing all metadata for a registered function. @@ -488,7 +488,11 @@ impl Module { #[inline(always)] #[must_use] pub fn contains_var(&self, name: &str) -> bool { - self.variables.contains_key(name) + if !self.variables.is_empty() { + self.variables.contains_key(name) + } else { + false + } } /// Get the value of a [`Module`] variable. @@ -520,7 +524,11 @@ impl Module { #[inline(always)] #[must_use] pub fn get_var(&self, name: &str) -> Option { - self.variables.get(name).cloned() + if !self.variables.is_empty() { + self.variables.get(name).cloned() + } else { + None + } } /// Set a variable into the [`Module`]. @@ -552,14 +560,15 @@ impl Module { self } - /// Get a reference to a namespace-qualified variable. - /// Name and Position in [`EvalAltResult`] are [`None`] and [`NONE`][Position::NONE] and must be set afterwards. + /// Get a namespace-qualified [`Module`] variable as a [`Dynamic`]. #[cfg(not(feature = "no_module"))] #[inline] - pub(crate) fn get_qualified_var(&self, hash_var: u64) -> RhaiResultOf<&Dynamic> { - self.all_variables.get(&hash_var).ok_or_else(|| { - crate::ERR::ErrorVariableNotFound(String::new(), crate::Position::NONE).into() - }) + pub(crate) fn get_qualified_var(&self, hash_var: u64) -> Option { + if !self.all_variables.is_empty() { + self.all_variables.get(&hash_var).cloned() + } else { + None + } } /// Set a script-defined function into the [`Module`]. @@ -610,14 +619,14 @@ impl Module { name: impl AsRef, num_params: usize, ) -> Option<&Shared> { - if self.functions.is_empty() { - None - } else { + if !self.functions.is_empty() { let name = name.as_ref(); self.iter_fn() .find(|f| f.metadata.params == num_params && f.metadata.name == name) .and_then(|f| f.func.get_script_fn_def()) + } else { + None } } @@ -656,7 +665,11 @@ impl Module { #[inline(always)] #[must_use] pub fn contains_sub_module(&self, name: &str) -> bool { - self.modules.contains_key(name) + if !self.modules.is_empty() { + self.modules.contains_key(name) + } else { + false + } } /// Get a sub-module in the [`Module`]. @@ -673,7 +686,11 @@ impl Module { #[inline] #[must_use] pub fn get_sub_module(&self, name: &str) -> Option<&Module> { - self.modules.get(name).map(|m| m.as_ref()) + if !self.modules.is_empty() { + self.modules.get(name).map(|m| m.as_ref()) + } else { + None + } } /// Set a sub-module into the [`Module`]. @@ -716,7 +733,11 @@ impl Module { #[inline(always)] #[must_use] pub fn contains_fn(&self, hash_fn: u64) -> bool { - self.functions.contains_key(&hash_fn) + if !self.functions.is_empty() { + self.functions.contains_key(&hash_fn) + } else { + false + } } /// _(metadata)_ Update the metadata (parameter names/types and return type) of a registered function. @@ -1357,7 +1378,11 @@ impl Module { #[inline] #[must_use] pub(crate) fn get_fn(&self, hash_fn: u64) -> Option<&CallableFunction> { - self.functions.get(&hash_fn).map(|f| f.func.as_ref()) + if !self.functions.is_empty() { + self.functions.get(&hash_fn).map(|f| f.func.as_ref()) + } else { + None + } } /// Does the particular namespace-qualified function exist in the [`Module`]? @@ -1366,7 +1391,11 @@ impl Module { #[inline(always)] #[must_use] pub fn contains_qualified_fn(&self, hash_fn: u64) -> bool { - self.all_functions.contains_key(&hash_fn) + if !self.all_functions.is_empty() { + self.all_functions.contains_key(&hash_fn) + } else { + false + } } /// Get a namespace-qualified function. @@ -1376,9 +1405,13 @@ impl Module { #[inline] #[must_use] pub(crate) fn get_qualified_fn(&self, hash_qualified_fn: u64) -> Option<&CallableFunction> { - self.all_functions - .get(&hash_qualified_fn) - .map(|f| f.as_ref()) + if !self.all_functions.is_empty() { + self.all_functions + .get(&hash_qualified_fn) + .map(|f| f.as_ref()) + } else { + None + } } /// Combine another [`Module`] into this [`Module`]. @@ -1720,7 +1753,28 @@ impl Module { result?; // Variables with an alias left in the scope become module variables - for (.., value, mut aliases) in scope { + for (_name, value, mut aliases) in scope { + // It is an error to export function pointers that refer to encapsulated local functions + #[cfg(not(feature = "no_function"))] + if let Some(fn_ptr) = value.downcast_ref::() { + if ast.iter_fn_def().any(|f| f.name == fn_ptr.fn_name()) { + return Err(crate::ERR::ErrorMismatchDataType( + "".to_string(), + if fn_ptr.is_anonymous() { + format!("cannot export closure in variable {}", _name) + } else { + format!( + "cannot export function pointer to local function '{}' in variable {}", + fn_ptr.fn_name(), + _name + ) + }, + crate::Position::NONE, + ) + .into()); + } + } + match aliases.len() { 0 => (), 1 => { @@ -1885,14 +1939,22 @@ impl Module { #[inline(always)] #[must_use] pub fn contains_qualified_iter(&self, id: TypeId) -> bool { - self.all_type_iterators.contains_key(&id) + if !self.all_type_iterators.is_empty() { + self.all_type_iterators.contains_key(&id) + } else { + false + } } /// Does a type iterator exist in the module? #[inline(always)] #[must_use] pub fn contains_iter(&self, id: TypeId) -> bool { - self.type_iterators.contains_key(&id) + if !self.type_iterators.is_empty() { + self.type_iterators.contains_key(&id) + } else { + false + } } /// Set a type iterator into the [`Module`]. @@ -1958,14 +2020,22 @@ impl Module { #[inline] #[must_use] pub(crate) fn get_qualified_iter(&self, id: TypeId) -> Option<&IteratorFn> { - self.all_type_iterators.get(&id).map(|f| f.as_ref()) + if !self.all_type_iterators.is_empty() { + self.all_type_iterators.get(&id).map(|f| f.as_ref()) + } else { + None + } } /// Get the specified type iterator. #[inline] #[must_use] pub(crate) fn get_iter(&self, id: TypeId) -> Option<&IteratorFn> { - self.type_iterators.get(&id).map(|f| f.as_ref()) + if !self.type_iterators.is_empty() { + self.type_iterators.get(&id).map(|f| f.as_ref()) + } else { + None + } } } diff --git a/src/module/namespace.rs b/src/module/namespace.rs index c2977030..63595c29 100644 --- a/src/module/namespace.rs +++ b/src/module/namespace.rs @@ -24,8 +24,8 @@ use std::{ /// one level, and it is wasteful to always allocate a [`Vec`] with one element. #[derive(Clone, Eq, PartialEq, Default, Hash)] pub struct Namespace { - index: Option, path: StaticVec, + index: Option, } impl fmt::Debug for Namespace { @@ -114,12 +114,24 @@ impl Namespace { pub(crate) fn set_index(&mut self, index: Option) { self.index = index } - /// Get the [position][Position] of this [`NameSpace`]. + /// Get the [position][Position] of this [`Namespace`]. /// /// # Panics /// /// Panics if the path is empty. + #[inline(always)] + #[must_use] pub fn position(&self) -> Position { self.path[0].pos } + /// Get the first path segment of this [`Namespace`]. + /// + /// # Panics + /// + /// Panics if the path is empty. + #[inline(always)] + #[must_use] + pub fn root(&self) -> &str { + &self.path[0].name + } } diff --git a/src/module/resolvers/file.rs b/src/module/resolvers/file.rs index 8ed6c7e7..5de00416 100644 --- a/src/module/resolvers/file.rs +++ b/src/module/resolvers/file.rs @@ -2,7 +2,7 @@ #![cfg(not(target_family = "wasm"))] use crate::eval::GlobalRuntimeState; -use crate::func::native::locked_write; +use crate::func::native::{locked_read, locked_write}; use crate::{ Engine, Identifier, Module, ModuleResolver, Position, RhaiResultOf, Scope, Shared, ERR, }; @@ -208,7 +208,13 @@ impl FileModuleResolver { let file_path = self.get_file_path(path.as_ref(), source_path); - locked_write(&self.cache).contains_key(&file_path) + let cache = locked_read(&self.cache); + + if !cache.is_empty() { + cache.contains_key(&file_path) + } else { + false + } } /// Empty the internal cache. #[inline] diff --git a/src/module/resolvers/stat.rs b/src/module/resolvers/stat.rs index dc25c563..5648b5ef 100644 --- a/src/module/resolvers/stat.rs +++ b/src/module/resolvers/stat.rs @@ -62,7 +62,11 @@ impl StaticModuleResolver { #[inline(always)] #[must_use] pub fn contains_path(&self, path: &str) -> bool { - self.0.contains_key(path) + if !self.0.is_empty() { + self.0.contains_key(path) + } else { + false + } } /// Get an iterator of all the [modules][Module]. #[inline] diff --git a/src/optimizer.rs b/src/optimizer.rs index 412c0f39..fc37aebe 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -1,9 +1,7 @@ //! Module implementing the [`AST`] optimizer. #![cfg(not(feature = "no_optimize"))] -use crate::ast::{ - Expr, OpAssignment, Stmt, StmtBlock, StmtBlockContainer, SwitchCases, AST_OPTION_FLAGS::*, -}; +use crate::ast::{ASTFlags, Expr, OpAssignment, Stmt, StmtBlock, StmtBlockContainer, SwitchCases}; use crate::engine::{KEYWORD_DEBUG, KEYWORD_EVAL, KEYWORD_FN_PTR, KEYWORD_PRINT, KEYWORD_TYPE_OF}; use crate::eval::{EvalState, GlobalRuntimeState}; use crate::func::builtin::get_builtin_binary_op_fn; @@ -255,7 +253,7 @@ fn optimize_stmt_block( for stmt in statements.iter_mut() { match stmt { Stmt::Var(x, options, ..) => { - if options.contains(AST_OPTION_CONSTANT) { + if options.contains(ASTFlags::CONSTANT) { // Add constant literals into the state optimize_expr(&mut x.1, state, false); @@ -324,7 +322,7 @@ fn optimize_stmt_block( match statements[..] { // { return; } -> {} [Stmt::Return(None, options, ..)] - if reduce_return && !options.contains(AST_OPTION_BREAK) => + if reduce_return && !options.contains(ASTFlags::BREAK) => { state.set_dirty(); statements.clear(); @@ -336,7 +334,7 @@ fn optimize_stmt_block( // { ...; return; } -> { ... } [.., ref last_stmt, Stmt::Return(None, options, ..)] if reduce_return - && !options.contains(AST_OPTION_BREAK) + && !options.contains(ASTFlags::BREAK) && !last_stmt.returns_value() => { state.set_dirty(); @@ -344,7 +342,7 @@ fn optimize_stmt_block( } // { ...; return val; } -> { ...; val } [.., Stmt::Return(ref mut expr, options, pos)] - if reduce_return && !options.contains(AST_OPTION_BREAK) => + if reduce_return && !options.contains(ASTFlags::BREAK) => { state.set_dirty(); *statements.last_mut().unwrap() = expr @@ -381,7 +379,7 @@ fn optimize_stmt_block( } // { ...; return; } -> { ... } [.., Stmt::Return(None, options, ..)] - if reduce_return && !options.contains(AST_OPTION_BREAK) => + if reduce_return && !options.contains(ASTFlags::BREAK) => { state.set_dirty(); statements.pop().unwrap(); @@ -389,7 +387,7 @@ fn optimize_stmt_block( // { ...; return pure_val; } -> { ... } [.., Stmt::Return(Some(ref expr), options, ..)] if reduce_return - && !options.contains(AST_OPTION_BREAK) + && !options.contains(ASTFlags::BREAK) && expr.is_pure() => { state.set_dirty(); @@ -745,7 +743,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b if body.len() == 1 { match body[0] { // while expr { break; } -> { expr; } - Stmt::BreakLoop(options, pos) if options.contains(AST_OPTION_BREAK) => { + Stmt::BreakLoop(options, pos) if options.contains(ASTFlags::BREAK) => { // Only a single break statement - turn into running the guard expression once state.set_dirty(); if !condition.is_unit() { @@ -765,7 +763,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // do { block } until true -> { block } Stmt::Do(x, options, ..) if matches!(x.0, Expr::BoolConstant(true, ..)) - && options.contains(AST_OPTION_NEGATED) => + && options.contains(ASTFlags::NEGATED) => { state.set_dirty(); *stmt = ( @@ -777,7 +775,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // do { block } while false -> { block } Stmt::Do(x, options, ..) if matches!(x.0, Expr::BoolConstant(false, ..)) - && !options.contains(AST_OPTION_NEGATED) => + && !options.contains(ASTFlags::NEGATED) => { state.set_dirty(); *stmt = ( @@ -797,7 +795,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b *x.3 = optimize_stmt_block(mem::take(&mut *x.3), state, false, true, false); } // let id = expr; - Stmt::Var(x, options, ..) if !options.contains(AST_OPTION_CONSTANT) => { + Stmt::Var(x, options, ..) if !options.contains(ASTFlags::CONSTANT) => { optimize_expr(&mut x.1, state, false) } // import expr as var; @@ -1145,7 +1143,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, chaining: bool) { // Eagerly call functions Expr::FnCall(x, pos) - if !x.is_qualified() // Non-qualified + if !x.is_qualified() // non-qualified && state.optimization_level == OptimizationLevel::Full // full optimizations && x.args.iter().all(Expr::is_constant) // all arguments are constants => { @@ -1178,8 +1176,8 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, chaining: bool) { x.args.iter_mut().for_each(|a| optimize_expr(a, state, false)); } - // id(args ..) -> optimize function call arguments - Expr::FnCall(x, ..) => for arg in x.args.iter_mut() { + // id(args ..) or xxx.id(args ..) -> optimize function call arguments + Expr::FnCall(x, ..) | Expr::MethodCall(x, ..) => for arg in x.args.iter_mut() { optimize_expr(arg, state, false); // Move constant arguments diff --git a/src/packages/blob_basic.rs b/src/packages/blob_basic.rs index 611d0b83..2a21b0d2 100644 --- a/src/packages/blob_basic.rs +++ b/src/packages/blob_basic.rs @@ -241,7 +241,7 @@ pub mod blob_functions { /// print(b); // prints "[424242424268656c 6c6f]" /// ``` #[rhai_fn(name = "+=", name = "append")] - pub fn append_str(blob: &mut Blob, string: ImmutableString) { + pub fn append_str(blob: &mut Blob, string: &str) { if !string.is_empty() { blob.extend(string.as_bytes()); } diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index b8c3f4c6..79e392c9 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -22,16 +22,16 @@ def_package! { mod string_functions { use crate::{ImmutableString, SmartString}; - #[rhai_fn(name = "+")] + #[rhai_fn(name = "+", pure)] pub fn add_append( ctx: NativeCallContext, - string: ImmutableString, + string: &mut ImmutableString, mut item: Dynamic, ) -> ImmutableString { let s = print_with_func(FUNC_TO_STRING, &ctx, &mut item); if s.is_empty() { - string + string.clone() } else { format!("{}{}", string, s).into() } @@ -61,16 +61,16 @@ mod string_functions { // The following are needed in order to override the generic versions with `Dynamic` parameters. - #[rhai_fn(name = "+")] - pub fn add_append_str(string1: ImmutableString, string2: ImmutableString) -> ImmutableString { - string1 + string2 + #[rhai_fn(name = "+", pure)] + pub fn add_append_str(string1: &mut ImmutableString, string2: &str) -> ImmutableString { + &*string1 + string2 + } + #[rhai_fn(name = "+", pure)] + pub fn add_append_char(string: &mut ImmutableString, character: char) -> ImmutableString { + &*string + character } #[rhai_fn(name = "+")] - pub fn add_append_char(string: ImmutableString, character: char) -> ImmutableString { - string + character - } - #[rhai_fn(name = "+")] - pub fn add_prepend_char(character: char, string: ImmutableString) -> ImmutableString { + pub fn add_prepend_char(character: char, string: &str) -> ImmutableString { format!("{}{}", character, string).into() } @@ -86,14 +86,14 @@ mod string_functions { #[cfg(not(feature = "no_index"))] pub mod blob_functions { - #[rhai_fn(name = "+")] - pub fn add_append_blob(string: ImmutableString, utf8: Blob) -> ImmutableString { + #[rhai_fn(name = "+", pure)] + pub fn add_append_blob(string: &mut ImmutableString, utf8: Blob) -> ImmutableString { if utf8.is_empty() { - string + string.clone() } else if string.is_empty() { String::from_utf8_lossy(&utf8).into_owned().into() } else { - let mut s = crate::SmartString::from(string); + let mut s = crate::SmartString::from(string.as_str()); s.push_str(&String::from_utf8_lossy(&utf8)); s.into() } @@ -172,7 +172,7 @@ mod string_functions { /// /// print(text); // prints ", world! , foobar!" /// ``` - pub fn remove(string: &mut ImmutableString, sub_string: ImmutableString) { + pub fn remove(string: &mut ImmutableString, sub_string: &str) { *string -= sub_string; } /// Remove all occurrences of a character from the string. @@ -263,7 +263,7 @@ mod string_functions { } } } - /// Remove the a specified number of characters from the end of the string and return it as a + /// Remove a specified number of characters from the end of the string and return it as a /// new string. /// /// * If `len` ≤ 0, the string is not modified and an empty string is returned. @@ -311,9 +311,10 @@ mod string_functions { /// /// print(text); // prints "hello, world!" /// ``` - pub fn to_upper(string: ImmutableString) -> ImmutableString { - if string.is_empty() { - string + #[rhai_fn(pure)] + pub fn to_upper(string: &mut ImmutableString) -> ImmutableString { + if string.is_empty() || string.chars().all(char::is_uppercase) { + string.clone() } else { string.to_uppercase().into() } @@ -330,7 +331,7 @@ mod string_functions { /// print(text); // prints "HELLO, WORLD!"; /// ``` pub fn make_upper(string: &mut ImmutableString) { - if !string.is_empty() { + if !string.is_empty() && string.chars().any(|ch| !ch.is_uppercase()) { *string = string.to_uppercase().into(); } } @@ -345,9 +346,10 @@ mod string_functions { /// /// print(text); // prints "HELLO, WORLD!" /// ``` - pub fn to_lower(string: ImmutableString) -> ImmutableString { - if string.is_empty() { - string + #[rhai_fn(pure)] + pub fn to_lower(string: &mut ImmutableString) -> ImmutableString { + if string.is_empty() || string.chars().all(char::is_lowercase) { + string.clone() } else { string.to_lowercase().into() } @@ -364,7 +366,7 @@ mod string_functions { /// print(text); // prints "hello, world!"; /// ``` pub fn make_lower(string: &mut ImmutableString) { - if !string.is_empty() { + if !string.is_empty() && string.chars().any(|ch| !ch.is_lowercase()) { *string = string.to_lowercase().into(); } } @@ -1204,19 +1206,25 @@ mod string_functions { /// print(text.split(-99)); // prints ["", "hello, world!"] /// ``` #[rhai_fn(name = "split")] - pub fn split_at(ctx: NativeCallContext, string: ImmutableString, index: INT) -> Array { + pub fn split_at(ctx: NativeCallContext, string: &mut ImmutableString, index: INT) -> Array { if index <= 0 { if let Some(n) = index.checked_abs() { let num_chars = string.chars().count(); if n as usize > num_chars { - vec![ctx.engine().const_empty_string().into(), string.into()] + vec![ + ctx.engine().const_empty_string().into(), + string.as_str().into(), + ] } else { let prefix: String = string.chars().take(num_chars - n as usize).collect(); let prefix_len = prefix.len(); vec![prefix.into(), string[prefix_len..].into()] } } else { - vec![ctx.engine().const_empty_string().into(), string.into()] + vec![ + ctx.engine().const_empty_string().into(), + string.as_str().into(), + ] } } else { let prefix: String = string.chars().take(index as usize).collect(); diff --git a/src/parser.rs b/src/parser.rs index b61653e2..f96c2e1c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -4,9 +4,8 @@ use crate::api::custom_syntax::{markers::*, CustomSyntax}; use crate::api::events::VarDefInfo; use crate::api::options::LanguageOptions; use crate::ast::{ - BinaryExpr, ConditionalStmtBlock, CustomExpr, Expr, FnCallExpr, FnCallHashes, Ident, + ASTFlags, BinaryExpr, ConditionalStmtBlock, CustomExpr, Expr, FnCallExpr, FnCallHashes, Ident, OpAssignment, ScriptFnDef, Stmt, StmtBlock, StmtBlockContainer, SwitchCases, TryCatchBlock, - AST_OPTION_FLAGS::*, }; use crate::engine::{Precedence, KEYWORD_THIS, OP_CONTAINS}; use crate::eval::{EvalState, GlobalRuntimeState}; @@ -20,7 +19,7 @@ use crate::types::StringsInterner; use crate::{ calc_fn_hash, Dynamic, Engine, EvalAltResult, EvalContext, ExclusiveRange, Identifier, ImmutableString, InclusiveRange, LexError, OptimizationLevel, ParseError, Position, Scope, - Shared, StaticVec, AST, INT, PERR, + Shared, SmartString, StaticVec, AST, INT, PERR, }; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -35,21 +34,19 @@ pub type ParseResult = Result; type FnLib = BTreeMap>; /// Invalid variable name that acts as a search barrier in a [`Scope`]. -const SCOPE_SEARCH_BARRIER_MARKER: &str = "$BARRIER$"; +const SCOPE_SEARCH_BARRIER_MARKER: &str = "$ BARRIER $"; /// The message: `TokenStream` never ends -const NEVER_ENDS: &str = "`TokenStream` never ends"; +const NEVER_ENDS: &str = "`Token`"; /// _(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`]. - pub engine: &'e Engine, /// Input stream buffer containing the next character to read. pub tokenizer_control: TokenizerControl, /// Interned strings. - pub interned_strings: StringsInterner, + interned_strings: StringsInterner, /// Encapsulates a local stack with variable names to simulate an actual runtime scope. pub stack: Scope<'e>, /// Size of the local variables stack upon entry of the current block scope. @@ -66,28 +63,18 @@ pub struct ParseState<'e> { /// Encapsulates a local stack with imported [module][crate::Module] names. #[cfg(not(feature = "no_module"))] pub imports: StaticVec, - /// Maximum levels of expression nesting. + /// Maximum levels of expression nesting (0 for unlimited). #[cfg(not(feature = "unchecked"))] - pub max_expr_depth: Option, - /// Maximum levels of expression nesting in functions. - #[cfg(not(feature = "unchecked"))] - #[cfg(not(feature = "no_function"))] - pub max_function_expr_depth: Option, + pub max_expr_depth: usize, } impl<'e> ParseState<'e> { /// Create a new [`ParseState`]. #[inline(always)] #[must_use] - pub fn new(engine: &'e Engine, tokenizer_control: TokenizerControl) -> Self { + pub fn new(engine: &Engine, tokenizer_control: TokenizerControl) -> Self { Self { - engine, tokenizer_control, - #[cfg(not(feature = "unchecked"))] - max_expr_depth: NonZeroUsize::new(engine.max_expr_depth()), - #[cfg(not(feature = "unchecked"))] - #[cfg(not(feature = "no_function"))] - max_function_expr_depth: NonZeroUsize::new(engine.max_function_expr_depth()), #[cfg(not(feature = "no_closure"))] external_vars: Vec::new(), #[cfg(not(feature = "no_closure"))] @@ -97,12 +84,43 @@ impl<'e> ParseState<'e> { block_stack_len: 0, #[cfg(not(feature = "no_module"))] imports: StaticVec::new_const(), + #[cfg(not(feature = "unchecked"))] + max_expr_depth: engine.max_expr_depth(), } } /// Find explicitly declared variable by name in the [`ParseState`], searching in reverse order. /// - /// If the variable is not present in the scope adds it to the list of external variables + /// The first return value is the offset to be deducted from `ParseState::stack::len()`, + /// i.e. the top element of [`ParseState`]'s variables stack is offset 1. + /// + /// If the variable is not present in the scope, the first return value is zero. + /// + /// The second return value indicates whether the barrier has been hit before finding the variable. + pub fn find_var(&self, name: &str) -> (usize, bool) { + let mut hit_barrier = false; + + ( + self.stack + .iter_rev_raw() + .enumerate() + .find(|&(.., (n, ..))| { + if n == SCOPE_SEARCH_BARRIER_MARKER { + // Do not go beyond the barrier + hit_barrier = true; + false + } else { + n == name + } + }) + .map_or(0, |(i, ..)| i + 1), + hit_barrier, + ) + } + + /// Find explicitly declared variable by name in the [`ParseState`], searching in reverse order. + /// + /// If the variable is not present in the scope adds it to the list of external variables. /// /// The return value is the offset to be deducted from `ParseState::stack::len()`, /// i.e. the top element of [`ParseState`]'s variables stack is offset 1. @@ -111,27 +129,13 @@ impl<'e> ParseState<'e> { #[inline] #[must_use] pub fn access_var(&mut self, name: &str, pos: Position) -> Option { - let mut hit_barrier = false; let _pos = pos; - let index = self - .stack - .iter_rev_raw() - .enumerate() - .find(|&(.., (n, ..))| { - if n == SCOPE_SEARCH_BARRIER_MARKER { - // Do not go beyond the barrier - hit_barrier = true; - false - } else { - n == name - } - }) - .and_then(|(i, ..)| NonZeroUsize::new(i + 1)); + let (index, hit_barrier) = self.find_var(name); #[cfg(not(feature = "no_closure"))] if self.allow_capture { - if index.is_none() && !self.external_vars.iter().any(|v| v.name == name) { + if index == 0 && !self.external_vars.iter().any(|v| v.name == name) { self.external_vars.push(crate::ast::Ident { name: name.into(), pos: _pos, @@ -144,7 +148,7 @@ impl<'e> ParseState<'e> { if hit_barrier { None } else { - index + NonZeroUsize::new(index) } } @@ -193,8 +197,6 @@ impl<'e> ParseState<'e> { /// A type that encapsulates all the settings for a particular parsing function. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] struct ParseSettings { - /// Current position. - pos: Position, /// Is the construct being parsed located at global level? is_global: bool, /// Is the construct being parsed located at function definition level? @@ -203,24 +205,15 @@ struct ParseSettings { /// Is the construct being parsed located inside a closure? #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_closure"))] - is_closure: bool, + is_closure_scope: bool, /// Is the current position inside a loop? is_breakable: bool, - /// Default language options. - default_options: LanguageOptions, - /// Is strict variables mode enabled? - strict_var: bool, - /// Is anonymous function allowed? - #[cfg(not(feature = "no_function"))] - allow_anonymous_fn: bool, - /// Is `if`-expression allowed? - allow_if_expr: bool, - /// Is `switch` expression allowed? - allow_switch_expr: bool, - /// Is statement-expression allowed? - allow_stmt_expr: bool, + /// Language options in effect (overrides Engine options). + options: LanguageOptions, /// Current expression nesting level. level: usize, + /// Current position. + pos: Position, } impl ParseSettings { @@ -234,11 +227,13 @@ impl ParseSettings { } } /// Make sure that the current level of expression nesting is within the maximum limit. + /// + /// If `limit` is zero, then checking is disabled. #[cfg(not(feature = "unchecked"))] #[inline] - pub fn ensure_level_within_max_limit(&self, limit: Option) -> ParseResult<()> { - if let Some(limit) = limit { - if self.level > limit.get() { + pub fn ensure_level_within_max_limit(&self, limit: usize) -> ParseResult<()> { + if limit > 0 { + if self.level > limit { return Err(PERR::ExprTooDeep.into_err(self.pos)); } } @@ -388,7 +383,7 @@ fn match_token(input: &mut TokenStream, token: Token) -> (bool, Position) { } /// Parse a variable name. -fn parse_var_name(input: &mut TokenStream) -> ParseResult<(Box, Position)> { +fn parse_var_name(input: &mut TokenStream) -> ParseResult<(SmartString, Position)> { match input.next().expect(NEVER_ENDS) { // Variable name (Token::Identifier(s), pos) => Ok((s, pos)), @@ -404,175 +399,112 @@ fn parse_var_name(input: &mut TokenStream) -> ParseResult<(Box, Position)> } /// Parse a symbol. -fn parse_symbol(input: &mut TokenStream) -> ParseResult<(Box, Position)> { +fn parse_symbol(input: &mut TokenStream) -> ParseResult<(SmartString, Position)> { match input.next().expect(NEVER_ENDS) { // Symbol (token, pos) if token.is_standard_symbol() => Ok((token.literal_syntax().into(), pos)), // Reserved symbol (Token::Reserved(s), pos) if !is_valid_identifier(s.chars()) => Ok((s, pos)), - // Bad identifier + // Bad symbol (Token::LexError(err), pos) => Err(err.into_err(pos)), // Not a symbol (.., pos) => Err(PERR::MissingSymbol(String::new()).into_err(pos)), } } -/// Parse `(` expr `)` -fn parse_paren_expr( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; +impl Engine { + /// Parse `(` expr `)` + fn parse_paren_expr( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; - // ( ... - let mut settings = settings; - settings.pos = eat_token(input, Token::LeftParen); + // ( ... + let mut settings = settings; + settings.pos = eat_token(input, Token::LeftParen); - if match_token(input, Token::RightParen).0 { - return Ok(Expr::Unit(settings.pos)); - } + if match_token(input, Token::RightParen).0 { + return Ok(Expr::Unit(settings.pos)); + } - let expr = parse_expr(input, state, lib, settings.level_up())?; + let expr = self.parse_expr(input, state, lib, settings.level_up())?; - match input.next().expect(NEVER_ENDS) { - // ( xxx ) - (Token::RightParen, ..) => Ok(expr), - // ( - (Token::LexError(err), pos) => Err(err.into_err(pos)), - // ( xxx ??? - (.., pos) => Err(PERR::MissingToken( - Token::RightParen.into(), - "for a matching ( in this expression".into(), - ) - .into_err(pos)), - } -} - -/// Parse a function call. -fn parse_fn_call( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - id: Identifier, - capture_parent_scope: bool, - #[cfg(not(feature = "no_module"))] namespace: Option, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - let (token, token_pos) = input.peek().expect(NEVER_ENDS); - - #[cfg(not(feature = "no_module"))] - let mut namespace = namespace; - let mut args = StaticVec::new_const(); - - match token { - // id( - Token::EOF => { - return Err(PERR::MissingToken( + match input.next().expect(NEVER_ENDS) { + // ( ... ) + (Token::RightParen, ..) => Ok(expr), + // ( + (Token::LexError(err), pos) => Err(err.into_err(pos)), + // ( ... ??? + (.., pos) => Err(PERR::MissingToken( Token::RightParen.into(), - format!("to close the arguments list of this function call '{}'", id), + "for a matching ( in this expression".into(), ) - .into_err(*token_pos)) + .into_err(pos)), } - // id( - Token::LexError(err) => return Err(err.clone().into_err(*token_pos)), - // id() - Token::RightParen => { - eat_token(input, Token::RightParen); - - #[cfg(not(feature = "no_module"))] - let hash = if let Some(modules) = namespace.as_mut() { - let index = state.find_module(&modules[0].name); - - #[cfg(not(feature = "no_function"))] - let relax = settings.is_function_scope; - #[cfg(feature = "no_function")] - let relax = false; - - if !relax && settings.strict_var && index.is_none() { - return Err( - PERR::ModuleUndefined(modules[0].name.to_string()).into_err(modules[0].pos) - ); - } - - modules.set_index(index); - - crate::calc_qualified_fn_hash(modules.iter().map(|m| m.name.as_str()), &id, 0) - } else { - calc_fn_hash(&id, 0) - }; - #[cfg(feature = "no_module")] - let hash = calc_fn_hash(&id, 0); - - let hashes = if is_valid_function_name(&id) { - hash.into() - } else { - FnCallHashes::from_native(hash) - }; - - args.shrink_to_fit(); - - return Ok(FnCallExpr { - name: state.get_identifier("", id), - capture_parent_scope, - #[cfg(not(feature = "no_module"))] - namespace, - hashes, - args, - pos: settings.pos, - ..Default::default() - } - .into_fn_call_expr(settings.pos)); - } - // id... - _ => (), } - let settings = settings.level_up(); + /// Parse a function call. + fn parse_fn_call( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + id: Identifier, + capture_parent_scope: bool, + #[cfg(not(feature = "no_module"))] namespace: Option, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; - loop { - match input.peek().expect(NEVER_ENDS) { - // id(...args, ) - handle trailing comma - (Token::RightParen, ..) => (), - _ => args.push(parse_expr(input, state, lib, settings)?), - } + let (token, token_pos) = input.peek().expect(NEVER_ENDS); - match input.peek().expect(NEVER_ENDS) { - // id(...args) - (Token::RightParen, ..) => { + #[cfg(not(feature = "no_module"))] + let mut namespace = namespace; + let mut args = StaticVec::new_const(); + + match token { + // id( + Token::EOF => { + return Err(PERR::MissingToken( + Token::RightParen.into(), + format!("to close the arguments list of this function call '{}'", id), + ) + .into_err(*token_pos)) + } + // id( + Token::LexError(err) => return Err(err.clone().into_err(*token_pos)), + // id() + Token::RightParen => { eat_token(input, Token::RightParen); #[cfg(not(feature = "no_module"))] - let hash = if let Some(modules) = namespace.as_mut() { - let index = state.find_module(&modules[0].name); + let hash = if let Some(namespace) = namespace.as_mut() { + let index = state.find_module(namespace.root()); #[cfg(not(feature = "no_function"))] let relax = settings.is_function_scope; #[cfg(feature = "no_function")] let relax = false; - if !relax && settings.strict_var && index.is_none() { - return Err(PERR::ModuleUndefined(modules[0].name.to_string()) - .into_err(modules[0].pos)); + if !relax && settings.options.strict_var && index.is_none() { + return Err(PERR::ModuleUndefined(namespace.root().to_string()) + .into_err(namespace.position())); } - modules.set_index(index); + namespace.set_index(index); - crate::calc_qualified_fn_hash( - modules.iter().map(|m| m.name.as_str()), - &id, - args.len(), - ) + crate::calc_qualified_fn_hash(namespace.iter().map(|m| m.name.as_str()), &id, 0) } else { - calc_fn_hash(&id, args.len()) + calc_fn_hash(&id, 0) }; #[cfg(feature = "no_module")] - let hash = calc_fn_hash(&id, args.len()); + let hash = calc_fn_hash(&id, 0); let hashes = if is_valid_function_name(&id) { hash.into() @@ -589,2858 +521,3015 @@ fn parse_fn_call( namespace, hashes, args, + constants: StaticVec::new_const(), pos: settings.pos, - ..Default::default() } .into_fn_call_expr(settings.pos)); } - // id(...args, - (Token::Comma, ..) => { - eat_token(input, Token::Comma); - } - // id(...args - (Token::EOF, pos) => { - return Err(PERR::MissingToken( - Token::RightParen.into(), - format!("to close the arguments list of this function call '{}'", id), - ) - .into_err(*pos)) - } - // id(...args - (Token::LexError(err), pos) => return Err(err.clone().into_err(*pos)), - // id(...args ??? - (.., pos) => { - return Err(PERR::MissingToken( - Token::Comma.into(), - format!("to separate the arguments to function call '{}'", id), - ) - .into_err(*pos)) - } - } - } -} - -/// Parse an indexing chain. -/// Indexing binds to the right, so this call parses all possible levels of indexing following in the input. -#[cfg(not(feature = "no_index"))] -fn parse_index_chain( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - lhs: Expr, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - let mut settings = settings; - - let idx_expr = parse_expr(input, state, lib, settings.level_up())?; - - // Check type of indexing - must be integer or string - match idx_expr { - Expr::IntegerConstant(.., pos) => match lhs { - Expr::IntegerConstant(..) - | Expr::Array(..) - | Expr::StringConstant(..) - | Expr::InterpolatedString(..) => (), - - Expr::Map(..) => { - return Err(PERR::MalformedIndexExpr( - "Object map access expects string index, not a number".into(), - ) - .into_err(pos)) - } - - #[cfg(not(feature = "no_float"))] - Expr::FloatConstant(..) => { - return Err(PERR::MalformedIndexExpr( - "Only arrays, object maps and strings can be indexed".into(), - ) - .into_err(lhs.start_position())) - } - - Expr::CharConstant(..) - | Expr::And(..) - | Expr::Or(..) - | Expr::BoolConstant(..) - | Expr::Unit(..) => { - return Err(PERR::MalformedIndexExpr( - "Only arrays, object maps and strings can be indexed".into(), - ) - .into_err(lhs.start_position())) - } - + // id... _ => (), - }, - - // lhs[string] - Expr::StringConstant(..) | Expr::InterpolatedString(..) => match lhs { - Expr::Map(..) => (), - - Expr::Array(..) | Expr::StringConstant(..) | Expr::InterpolatedString(..) => { - return Err(PERR::MalformedIndexExpr( - "Array or string expects numeric index, not a string".into(), - ) - .into_err(idx_expr.start_position())) - } - - #[cfg(not(feature = "no_float"))] - Expr::FloatConstant(..) => { - return Err(PERR::MalformedIndexExpr( - "Only arrays, object maps and strings can be indexed".into(), - ) - .into_err(lhs.start_position())) - } - - Expr::CharConstant(..) - | Expr::And(..) - | Expr::Or(..) - | Expr::BoolConstant(..) - | Expr::Unit(..) => { - return Err(PERR::MalformedIndexExpr( - "Only arrays, object maps and strings can be indexed".into(), - ) - .into_err(lhs.start_position())) - } - - _ => (), - }, - - // lhs[float] - #[cfg(not(feature = "no_float"))] - x @ Expr::FloatConstant(..) => { - return Err(PERR::MalformedIndexExpr( - "Array access expects integer index, not a float".into(), - ) - .into_err(x.start_position())) } - // lhs[char] - x @ Expr::CharConstant(..) => { - return Err(PERR::MalformedIndexExpr( - "Array access expects integer index, not a character".into(), - ) - .into_err(x.start_position())) - } - // lhs[()] - x @ Expr::Unit(..) => { - return Err(PERR::MalformedIndexExpr( - "Array access expects integer index, not ()".into(), - ) - .into_err(x.start_position())) - } - // lhs[??? && ???], lhs[??? || ???] - x @ Expr::And(..) | x @ Expr::Or(..) => { - return Err(PERR::MalformedIndexExpr( - "Array access expects integer index, not a boolean".into(), - ) - .into_err(x.start_position())) - } - // lhs[true], lhs[false] - x @ Expr::BoolConstant(..) => { - return Err(PERR::MalformedIndexExpr( - "Array access expects integer index, not a boolean".into(), - ) - .into_err(x.start_position())) - } - // All other expressions - _ => (), - } - // Check if there is a closing bracket - match input.peek().expect(NEVER_ENDS) { - (Token::RightBracket, ..) => { - eat_token(input, Token::RightBracket); + let settings = settings.level_up(); - // Any more indexing following? + loop { match input.peek().expect(NEVER_ENDS) { - // If another indexing level, right-bind it - (Token::LeftBracket, ..) => { - let prev_pos = settings.pos; - settings.pos = eat_token(input, Token::LeftBracket); - // Recursively parse the indexing chain, right-binding each - let idx_expr = - parse_index_chain(input, state, lib, idx_expr, settings.level_up())?; - // Indexing binds to right - Ok(Expr::Index( - BinaryExpr { lhs, rhs: idx_expr }.into(), - false, - prev_pos, - )) + // id(...args, ) - handle trailing comma + (Token::RightParen, ..) => (), + _ => args.push(self.parse_expr(input, state, lib, settings)?), + } + + match input.peek().expect(NEVER_ENDS) { + // id(...args) + (Token::RightParen, ..) => { + eat_token(input, Token::RightParen); + + #[cfg(not(feature = "no_module"))] + let hash = if let Some(namespace) = namespace.as_mut() { + let index = state.find_module(namespace.root()); + + #[cfg(not(feature = "no_function"))] + let relax = settings.is_function_scope; + #[cfg(feature = "no_function")] + let relax = false; + + if !relax && settings.options.strict_var && index.is_none() { + return Err(PERR::ModuleUndefined(namespace.root().to_string()) + .into_err(namespace.position())); + } + + namespace.set_index(index); + + crate::calc_qualified_fn_hash( + namespace.iter().map(|m| m.name.as_str()), + &id, + args.len(), + ) + } else { + calc_fn_hash(&id, args.len()) + }; + #[cfg(feature = "no_module")] + let hash = calc_fn_hash(&id, args.len()); + + let hashes = if is_valid_function_name(&id) { + hash.into() + } else { + FnCallHashes::from_native(hash) + }; + + args.shrink_to_fit(); + + return Ok(FnCallExpr { + name: state.get_identifier("", id), + capture_parent_scope, + #[cfg(not(feature = "no_module"))] + namespace, + hashes, + args, + constants: StaticVec::new_const(), + pos: settings.pos, + } + .into_fn_call_expr(settings.pos)); + } + // id(...args, + (Token::Comma, ..) => { + eat_token(input, Token::Comma); + } + // id(...args + (Token::EOF, pos) => { + return Err(PERR::MissingToken( + Token::RightParen.into(), + format!("to close the arguments list of this function call '{}'", id), + ) + .into_err(*pos)) + } + // id(...args + (Token::LexError(err), pos) => return Err(err.clone().into_err(*pos)), + // id(...args ??? + (.., pos) => { + return Err(PERR::MissingToken( + Token::Comma.into(), + format!("to separate the arguments to function call '{}'", id), + ) + .into_err(*pos)) } - // Otherwise terminate the indexing chain - _ => Ok(Expr::Index( - BinaryExpr { lhs, rhs: idx_expr }.into(), - true, - settings.pos, - )), } } - (Token::LexError(err), pos) => Err(err.clone().into_err(*pos)), - (.., pos) => Err(PERR::MissingToken( - Token::RightBracket.into(), - "for a matching [ in this index expression".into(), - ) - .into_err(*pos)), } -} - -/// Parse an array literal. -#[cfg(not(feature = "no_index"))] -fn parse_array_literal( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - // [ ... - let mut settings = settings; - settings.pos = eat_token(input, Token::LeftBracket); - - let mut arr = StaticVec::new_const(); - - loop { - const MISSING_RBRACKET: &str = "to end this array literal"; + /// Parse an indexing chain. + /// Indexing binds to the right, so this call parses all possible levels of indexing following in the input. + #[cfg(not(feature = "no_index"))] + fn parse_index_chain( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + lhs: Expr, + settings: ParseSettings, + ) -> ParseResult { #[cfg(not(feature = "unchecked"))] - if state.engine.max_array_size() > 0 && arr.len() >= state.engine.max_array_size() { - return Err(PERR::LiteralTooLarge( - "Size of array literal".to_string(), - state.engine.max_array_size(), - ) - .into_err(input.peek().expect(NEVER_ENDS).1)); + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + let mut settings = settings; + + let idx_expr = self.parse_expr(input, state, lib, settings.level_up())?; + + // Check type of indexing - must be integer or string + match idx_expr { + Expr::IntegerConstant(.., pos) => match lhs { + Expr::IntegerConstant(..) + | Expr::Array(..) + | Expr::StringConstant(..) + | Expr::InterpolatedString(..) => (), + + Expr::Map(..) => { + return Err(PERR::MalformedIndexExpr( + "Object map access expects string index, not a number".into(), + ) + .into_err(pos)) + } + + #[cfg(not(feature = "no_float"))] + Expr::FloatConstant(..) => { + return Err(PERR::MalformedIndexExpr( + "Only arrays, object maps and strings can be indexed".into(), + ) + .into_err(lhs.start_position())) + } + + Expr::CharConstant(..) + | Expr::And(..) + | Expr::Or(..) + | Expr::BoolConstant(..) + | Expr::Unit(..) => { + return Err(PERR::MalformedIndexExpr( + "Only arrays, object maps and strings can be indexed".into(), + ) + .into_err(lhs.start_position())) + } + + _ => (), + }, + + // lhs[string] + Expr::StringConstant(..) | Expr::InterpolatedString(..) => match lhs { + Expr::Map(..) => (), + + Expr::Array(..) | Expr::StringConstant(..) | Expr::InterpolatedString(..) => { + return Err(PERR::MalformedIndexExpr( + "Array or string expects numeric index, not a string".into(), + ) + .into_err(idx_expr.start_position())) + } + + #[cfg(not(feature = "no_float"))] + Expr::FloatConstant(..) => { + return Err(PERR::MalformedIndexExpr( + "Only arrays, object maps and strings can be indexed".into(), + ) + .into_err(lhs.start_position())) + } + + Expr::CharConstant(..) + | Expr::And(..) + | Expr::Or(..) + | Expr::BoolConstant(..) + | Expr::Unit(..) => { + return Err(PERR::MalformedIndexExpr( + "Only arrays, object maps and strings can be indexed".into(), + ) + .into_err(lhs.start_position())) + } + + _ => (), + }, + + // lhs[float] + #[cfg(not(feature = "no_float"))] + x @ Expr::FloatConstant(..) => { + return Err(PERR::MalformedIndexExpr( + "Array access expects integer index, not a float".into(), + ) + .into_err(x.start_position())) + } + // lhs[char] + x @ Expr::CharConstant(..) => { + return Err(PERR::MalformedIndexExpr( + "Array access expects integer index, not a character".into(), + ) + .into_err(x.start_position())) + } + // lhs[()] + x @ Expr::Unit(..) => { + return Err(PERR::MalformedIndexExpr( + "Array access expects integer index, not ()".into(), + ) + .into_err(x.start_position())) + } + // lhs[??? && ???], lhs[??? || ???] + x @ Expr::And(..) | x @ Expr::Or(..) => { + return Err(PERR::MalformedIndexExpr( + "Array access expects integer index, not a boolean".into(), + ) + .into_err(x.start_position())) + } + // lhs[true], lhs[false] + x @ Expr::BoolConstant(..) => { + return Err(PERR::MalformedIndexExpr( + "Array access expects integer index, not a boolean".into(), + ) + .into_err(x.start_position())) + } + // All other expressions + _ => (), } + // Check if there is a closing bracket match input.peek().expect(NEVER_ENDS) { (Token::RightBracket, ..) => { eat_token(input, Token::RightBracket); - break; - } - (Token::EOF, pos) => { - return Err( - PERR::MissingToken(Token::RightBracket.into(), MISSING_RBRACKET.into()) - .into_err(*pos), - ) - } - _ => { - let expr = parse_expr(input, state, lib, settings.level_up())?; - arr.push(expr); - } - } - match input.peek().expect(NEVER_ENDS) { - (Token::Comma, ..) => { - eat_token(input, Token::Comma); - } - (Token::RightBracket, ..) => (), - (Token::EOF, pos) => { - return Err( - PERR::MissingToken(Token::RightBracket.into(), MISSING_RBRACKET.into()) - .into_err(*pos), - ) - } - (Token::LexError(err), pos) => return Err(err.clone().into_err(*pos)), - (.., pos) => { - return Err(PERR::MissingToken( - Token::Comma.into(), - "to separate the items of this array literal".into(), - ) - .into_err(*pos)) - } - }; - } - - arr.shrink_to_fit(); - - Ok(Expr::Array(arr.into(), settings.pos)) -} - -/// Parse a map literal. -#[cfg(not(feature = "no_object"))] -fn parse_map_literal( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - // #{ ... - let mut settings = settings; - settings.pos = eat_token(input, Token::MapStart); - - let mut map = StaticVec::<(Ident, Expr)>::new(); - let mut template = BTreeMap::::new(); - - loop { - const MISSING_RBRACE: &str = "to end this object map literal"; - - match input.peek().expect(NEVER_ENDS) { - (Token::RightBrace, ..) => { - eat_token(input, Token::RightBrace); - break; - } - (Token::EOF, pos) => { - return Err( - PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) - .into_err(*pos), - ) - } - _ => (), - } - - let (name, pos) = match input.next().expect(NEVER_ENDS) { - (Token::Identifier(s), pos) | (Token::StringConstant(s), pos) => { - if map.iter().any(|(p, ..)| p.name == &*s) { - return Err(PERR::DuplicatedProperty(s.to_string()).into_err(pos)); - } - (s, pos) - } - (Token::InterpolatedString(..), pos) => { - return Err(PERR::PropertyExpected.into_err(pos)) - } - (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { - return Err(PERR::Reserved(s.to_string()).into_err(pos)); - } - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (Token::EOF, pos) => { - return Err( - PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) - .into_err(pos), - ); - } - (.., pos) if map.is_empty() => { - return Err( - PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) - .into_err(pos), - ); - } - (.., pos) => return Err(PERR::PropertyExpected.into_err(pos)), - }; - - match input.next().expect(NEVER_ENDS) { - (Token::Colon, ..) => (), - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (.., pos) => { - return Err(PERR::MissingToken( - Token::Colon.into(), - format!( - "to follow the property '{}' in this object map literal", - name - ), - ) - .into_err(pos)) - } - }; - - #[cfg(not(feature = "unchecked"))] - if state.engine.max_map_size() > 0 && map.len() >= state.engine.max_map_size() { - return Err(PERR::LiteralTooLarge( - "Number of properties in object map literal".to_string(), - state.engine.max_map_size(), - ) - .into_err(input.peek().expect(NEVER_ENDS).1)); - } - - let expr = parse_expr(input, state, lib, settings.level_up())?; - let name = state.get_identifier("", name); - template.insert(name.clone(), crate::Dynamic::UNIT); - map.push((Ident { name, pos }, expr)); - - match input.peek().expect(NEVER_ENDS) { - (Token::Comma, ..) => { - eat_token(input, Token::Comma); - } - (Token::RightBrace, ..) => (), - (Token::Identifier(..), pos) => { - return Err(PERR::MissingToken( - Token::Comma.into(), - "to separate the items of this object map literal".into(), - ) - .into_err(*pos)) - } - (Token::LexError(err), pos) => return Err(err.clone().into_err(*pos)), - (.., pos) => { - return Err( - PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) - .into_err(*pos), - ) - } - } - } - - map.shrink_to_fit(); - - Ok(Expr::Map((map, template).into(), settings.pos)) -} - -/// Parse a switch expression. -fn parse_switch( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - // switch ... - let mut settings = settings; - settings.pos = eat_token(input, Token::Switch); - - let item = parse_expr(input, state, lib, settings.level_up())?; - - match input.next().expect(NEVER_ENDS) { - (Token::LeftBrace, ..) => (), - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (.., pos) => { - return Err(PERR::MissingToken( - Token::LeftBrace.into(), - "to start a switch block".into(), - ) - .into_err(pos)) - } - } - - let mut cases = BTreeMap::>::new(); - let mut ranges = StaticVec::<(INT, INT, bool, Box)>::new(); - let mut def_pos = Position::NONE; - let mut def_stmt = None; - - loop { - const MISSING_RBRACE: &str = "to end this switch block"; - - let (expr, condition) = match input.peek().expect(NEVER_ENDS) { - (Token::RightBrace, ..) => { - eat_token(input, Token::RightBrace); - break; - } - (Token::EOF, pos) => { - return Err( - PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) - .into_err(*pos), - ) - } - (Token::Underscore, pos) if def_stmt.is_none() => { - def_pos = *pos; - eat_token(input, Token::Underscore); - - let (if_clause, if_pos) = match_token(input, Token::If); - - if if_clause { - return Err(PERR::WrongSwitchCaseCondition.into_err(if_pos)); - } - - (None, None) - } - (Token::Underscore, pos) => return Err(PERR::DuplicatedSwitchCase.into_err(*pos)), - - _ if def_stmt.is_some() => return Err(PERR::WrongSwitchDefaultCase.into_err(def_pos)), - - _ => { - let case_expr = Some(parse_expr(input, state, lib, settings.level_up())?); - - let condition = if match_token(input, Token::If).0 { - Some(parse_expr(input, state, lib, settings.level_up())?) - } else { - None - }; - (case_expr, condition) - } - }; - - let (hash, range) = if let Some(expr) = expr { - let value = expr.get_literal_value().ok_or_else(|| { - PERR::ExprExpected("a literal".to_string()).into_err(expr.start_position()) - })?; - - let guard = value.read_lock::(); - - if let Some(range) = guard { - (None, Some((range.start, range.end, false))) - } else if let Some(range) = value.read_lock::() { - (None, Some((*range.start(), *range.end(), true))) - } else if value.is::() && !ranges.is_empty() { - return Err(PERR::WrongSwitchIntegerCase.into_err(expr.start_position())); - } else { - let hasher = &mut get_hasher(); - value.hash(hasher); - let hash = hasher.finish(); - - if cases.contains_key(&hash) { - return Err(PERR::DuplicatedSwitchCase.into_err(expr.start_position())); - } - (Some(hash), None) - } - } else { - (None, None) - }; - - match input.next().expect(NEVER_ENDS) { - (Token::DoubleArrow, ..) => (), - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (.., pos) => { - return Err(PERR::MissingToken( - Token::DoubleArrow.into(), - "in this switch case".to_string(), - ) - .into_err(pos)) - } - }; - - let stmt = parse_stmt(input, state, lib, settings.level_up())?; - - let need_comma = !stmt.is_self_terminated(); - - def_stmt = match (hash, range) { - (None, Some(range)) => { - let is_empty = if range.2 { - (range.0..=range.1).is_empty() - } else { - (range.0..range.1).is_empty() - }; - - if !is_empty { - match (range.1.checked_sub(range.0), range.2) { - // Unroll single range - (Some(1), false) | (Some(0), true) => { - let value = Dynamic::from_int(range.0); - let hasher = &mut get_hasher(); - value.hash(hasher); - let hash = hasher.finish(); - - cases.entry(hash).or_insert_with(|| { - let block: ConditionalStmtBlock = (condition, stmt).into(); - block.into() - }); - } - // Other range - _ => { - let block: ConditionalStmtBlock = (condition, stmt).into(); - ranges.push((range.0, range.1, range.2, block.into())) - } + // Any more indexing following? + match input.peek().expect(NEVER_ENDS) { + // If another indexing level, right-bind it + (Token::LeftBracket, ..) => { + let prev_pos = settings.pos; + settings.pos = eat_token(input, Token::LeftBracket); + // Recursively parse the indexing chain, right-binding each + let idx_expr = self.parse_index_chain( + input, + state, + lib, + idx_expr, + settings.level_up(), + )?; + // Indexing binds to right + Ok(Expr::Index( + BinaryExpr { lhs, rhs: idx_expr }.into(), + ASTFlags::NONE, + prev_pos, + )) } + // Otherwise terminate the indexing chain + _ => Ok(Expr::Index( + BinaryExpr { lhs, rhs: idx_expr }.into(), + ASTFlags::BREAK, + settings.pos, + )), } - None } - (Some(hash), None) => { - let block: ConditionalStmtBlock = (condition, stmt).into(); - cases.insert(hash, block.into()); - None - } - (None, None) => Some(Box::new(stmt.into())), - _ => unreachable!("both hash and range in switch statement case"), - }; - - match input.peek().expect(NEVER_ENDS) { - (Token::Comma, ..) => { - eat_token(input, Token::Comma); - } - (Token::RightBrace, ..) => (), - (Token::EOF, pos) => { - return Err( - PERR::MissingToken(Token::RightParen.into(), MISSING_RBRACE.into()) - .into_err(*pos), - ) - } - (Token::LexError(err), pos) => return Err(err.clone().into_err(*pos)), - (.., pos) if need_comma => { - return Err(PERR::MissingToken( - Token::Comma.into(), - "to separate the items in this switch block".into(), - ) - .into_err(*pos)) - } - _ => (), + (Token::LexError(err), pos) => Err(err.clone().into_err(*pos)), + (.., pos) => Err(PERR::MissingToken( + Token::RightBracket.into(), + "for a matching [ in this index expression".into(), + ) + .into_err(*pos)), } } - let cases = SwitchCases { - cases, - def_case: def_stmt.unwrap_or_else(|| StmtBlock::NONE.into()), - ranges, - }; + /// Parse an array literal. + #[cfg(not(feature = "no_index"))] + fn parse_array_literal( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; - Ok(Stmt::Switch((item, cases).into(), settings.pos)) -} + // [ ... + let mut settings = settings; + settings.pos = eat_token(input, Token::LeftBracket); -/// Parse a primary expression. -fn parse_primary( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; + let mut arr = StaticVec::new_const(); - let (token, token_pos) = input.peek().expect(NEVER_ENDS); - - let mut settings = settings; - settings.pos = *token_pos; - - let root_expr = match token { - Token::EOF => return Err(PERR::UnexpectedEOF.into_err(settings.pos)), - - Token::IntegerConstant(..) - | Token::CharConstant(..) - | Token::StringConstant(..) - | Token::True - | Token::False => match input.next().expect(NEVER_ENDS).0 { - Token::IntegerConstant(x) => Expr::IntegerConstant(x, settings.pos), - Token::CharConstant(c) => Expr::CharConstant(c, settings.pos), - Token::StringConstant(s) => { - Expr::StringConstant(state.get_interned_string("", s), settings.pos) - } - Token::True => Expr::BoolConstant(true, settings.pos), - Token::False => Expr::BoolConstant(false, settings.pos), - token => unreachable!("token is {:?}", token), - }, - #[cfg(not(feature = "no_float"))] - Token::FloatConstant(x) => { - let x = *x; - input.next().expect(NEVER_ENDS); - Expr::FloatConstant(x, settings.pos) - } - #[cfg(feature = "decimal")] - Token::DecimalConstant(x) => { - let x = (*x).into(); - input.next().expect(NEVER_ENDS); - Expr::DynamicConstant(Box::new(x), settings.pos) - } - - // { - block statement as expression - Token::LeftBrace if settings.allow_stmt_expr => { - match parse_block(input, state, lib, settings.level_up())? { - block @ Stmt::Block(..) => Expr::Stmt(Box::new(block.into())), - stmt => unreachable!("Stmt::Block expected but gets {:?}", stmt), - } - } - // ( - grouped expression - Token::LeftParen => parse_paren_expr(input, state, lib, settings.level_up())?, - - // If statement is allowed to act as expressions - Token::If if settings.allow_if_expr => Expr::Stmt(Box::new( - parse_if(input, state, lib, settings.level_up())?.into(), - )), - // Switch statement is allowed to act as expressions - Token::Switch if settings.allow_switch_expr => Expr::Stmt(Box::new( - parse_switch(input, state, lib, settings.level_up())?.into(), - )), - - // | ... - #[cfg(not(feature = "no_function"))] - Token::Pipe | Token::Or if settings.allow_anonymous_fn => { - let mut new_state = ParseState::new(state.engine, state.tokenizer_control.clone()); + loop { + const MISSING_RBRACKET: &str = "to end this array literal"; #[cfg(not(feature = "unchecked"))] - { - new_state.max_expr_depth = new_state.max_function_expr_depth; + if self.max_array_size() > 0 && arr.len() >= self.max_array_size() { + return Err(PERR::LiteralTooLarge( + "Size of array literal".to_string(), + self.max_array_size(), + ) + .into_err(input.peek().expect(NEVER_ENDS).1)); } - let new_settings = ParseSettings { - allow_if_expr: settings.default_options.allow_if_expr, - allow_switch_expr: settings.default_options.allow_switch_expr, - allow_stmt_expr: settings.default_options.allow_stmt_expr, - allow_anonymous_fn: settings.default_options.allow_anonymous_fn, - strict_var: if cfg!(feature = "no_closure") { - settings.strict_var - } else { - // A capturing closure can access variables not defined locally - false - }, - is_global: false, - is_function_scope: true, - #[cfg(not(feature = "no_closure"))] - is_closure: true, - is_breakable: false, - level: 0, - ..settings + match input.peek().expect(NEVER_ENDS) { + (Token::RightBracket, ..) => { + eat_token(input, Token::RightBracket); + break; + } + (Token::EOF, pos) => { + return Err(PERR::MissingToken( + Token::RightBracket.into(), + MISSING_RBRACKET.into(), + ) + .into_err(*pos)) + } + _ => { + let expr = self.parse_expr(input, state, lib, settings.level_up())?; + arr.push(expr); + } + } + + match input.peek().expect(NEVER_ENDS) { + (Token::Comma, ..) => { + eat_token(input, Token::Comma); + } + (Token::RightBracket, ..) => (), + (Token::EOF, pos) => { + return Err(PERR::MissingToken( + Token::RightBracket.into(), + MISSING_RBRACKET.into(), + ) + .into_err(*pos)) + } + (Token::LexError(err), pos) => return Err(err.clone().into_err(*pos)), + (.., pos) => { + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the items of this array literal".into(), + ) + .into_err(*pos)) + } }; - - let (expr, func) = parse_anon_fn(input, &mut new_state, lib, new_settings)?; - - #[cfg(not(feature = "no_closure"))] - new_state - .external_vars - .iter() - .try_for_each(|crate::ast::Ident { name, pos }| { - let index = state.access_var(name, *pos); - - if settings.strict_var && !settings.is_closure && index.is_none() { - // If the parent scope is not inside another capturing closure - // then we can conclude that the captured variable doesn't exist. - // Under Strict Variables mode, this is not allowed. - Err(PERR::VariableUndefined(name.to_string()).into_err(*pos)) - } else { - Ok::<_, ParseError>(()) - } - })?; - - let hash_script = calc_fn_hash(&func.name, func.params.len()); - lib.insert(hash_script, func.into()); - - expr } - // Interpolated string - Token::InterpolatedString(..) => { - let mut segments = StaticVec::::new(); + arr.shrink_to_fit(); + + Ok(Expr::Array(arr.into(), settings.pos)) + } + + /// Parse a map literal. + #[cfg(not(feature = "no_object"))] + fn parse_map_literal( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + // #{ ... + let mut settings = settings; + settings.pos = eat_token(input, Token::MapStart); + + let mut map = StaticVec::<(Ident, Expr)>::new(); + let mut template = BTreeMap::::new(); + + loop { + const MISSING_RBRACE: &str = "to end this object map literal"; + + match input.peek().expect(NEVER_ENDS) { + (Token::RightBrace, ..) => { + eat_token(input, Token::RightBrace); + break; + } + (Token::EOF, pos) => { + return Err( + PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) + .into_err(*pos), + ) + } + _ => (), + } + + let (name, pos) = match input.next().expect(NEVER_ENDS) { + (Token::Identifier(s), pos) | (Token::StringConstant(s), pos) => { + if map.iter().any(|(p, ..)| p.name == &*s) { + return Err(PERR::DuplicatedProperty(s.to_string()).into_err(pos)); + } + (s, pos) + } + (Token::InterpolatedString(..), pos) => { + return Err(PERR::PropertyExpected.into_err(pos)) + } + (Token::Reserved(s), pos) if is_valid_identifier(s.chars()) => { + return Err(PERR::Reserved(s.to_string()).into_err(pos)); + } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (Token::EOF, pos) => { + return Err(PERR::MissingToken( + Token::RightBrace.into(), + MISSING_RBRACE.into(), + ) + .into_err(pos)); + } + (.., pos) if map.is_empty() => { + return Err(PERR::MissingToken( + Token::RightBrace.into(), + MISSING_RBRACE.into(), + ) + .into_err(pos)); + } + (.., pos) => return Err(PERR::PropertyExpected.into_err(pos)), + }; match input.next().expect(NEVER_ENDS) { - (Token::InterpolatedString(s), ..) if s.is_empty() => (), - (Token::InterpolatedString(s), pos) => { - segments.push(Expr::StringConstant(s.into(), pos)) + (Token::Colon, ..) => (), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (.., pos) => { + return Err(PERR::MissingToken( + Token::Colon.into(), + format!( + "to follow the property '{}' in this object map literal", + name + ), + ) + .into_err(pos)) } - token => unreachable!("Token::InterpolatedString expected but gets {:?}", token), + }; + + #[cfg(not(feature = "unchecked"))] + if self.max_map_size() > 0 && map.len() >= self.max_map_size() { + return Err(PERR::LiteralTooLarge( + "Number of properties in object map literal".to_string(), + self.max_map_size(), + ) + .into_err(input.peek().expect(NEVER_ENDS).1)); } - loop { - let expr = match parse_block(input, state, lib, settings.level_up())? { + let expr = self.parse_expr(input, state, lib, settings.level_up())?; + let name = state.get_identifier("", name); + template.insert(name.clone(), crate::Dynamic::UNIT); + map.push((Ident { name, pos }, expr)); + + match input.peek().expect(NEVER_ENDS) { + (Token::Comma, ..) => { + eat_token(input, Token::Comma); + } + (Token::RightBrace, ..) => (), + (Token::Identifier(..), pos) => { + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the items of this object map literal".into(), + ) + .into_err(*pos)) + } + (Token::LexError(err), pos) => return Err(err.clone().into_err(*pos)), + (.., pos) => { + return Err( + PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) + .into_err(*pos), + ) + } + } + } + + map.shrink_to_fit(); + + Ok(Expr::Map((map, template).into(), settings.pos)) + } + + /// Parse a switch expression. + fn parse_switch( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + // switch ... + let mut settings = settings; + settings.pos = eat_token(input, Token::Switch); + + let item = self.parse_expr(input, state, lib, settings.level_up())?; + + match input.next().expect(NEVER_ENDS) { + (Token::LeftBrace, ..) => (), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (.., pos) => { + return Err(PERR::MissingToken( + Token::LeftBrace.into(), + "to start a switch block".into(), + ) + .into_err(pos)) + } + } + + let mut cases = BTreeMap::>::new(); + let mut ranges = StaticVec::<(INT, INT, bool, Box)>::new(); + let mut def_pos = Position::NONE; + let mut def_stmt = None; + + loop { + const MISSING_RBRACE: &str = "to end this switch block"; + + let (expr, condition) = match input.peek().expect(NEVER_ENDS) { + (Token::RightBrace, ..) => { + eat_token(input, Token::RightBrace); + break; + } + (Token::EOF, pos) => { + return Err( + PERR::MissingToken(Token::RightBrace.into(), MISSING_RBRACE.into()) + .into_err(*pos), + ) + } + (Token::Underscore, pos) if def_stmt.is_none() => { + def_pos = *pos; + eat_token(input, Token::Underscore); + + let (if_clause, if_pos) = match_token(input, Token::If); + + if if_clause { + return Err(PERR::WrongSwitchCaseCondition.into_err(if_pos)); + } + + (None, None) + } + (Token::Underscore, pos) => return Err(PERR::DuplicatedSwitchCase.into_err(*pos)), + + _ if def_stmt.is_some() => { + return Err(PERR::WrongSwitchDefaultCase.into_err(def_pos)) + } + + _ => { + let case_expr = + Some(self.parse_expr(input, state, lib, settings.level_up())?); + + let condition = if match_token(input, Token::If).0 { + Some(self.parse_expr(input, state, lib, settings.level_up())?) + } else { + None + }; + (case_expr, condition) + } + }; + + let (hash, range) = if let Some(expr) = expr { + let value = expr.get_literal_value().ok_or_else(|| { + PERR::ExprExpected("a literal".to_string()).into_err(expr.start_position()) + })?; + + let guard = value.read_lock::(); + + if let Some(range) = guard { + (None, Some((range.start, range.end, false))) + } else if let Some(range) = value.read_lock::() { + (None, Some((*range.start(), *range.end(), true))) + } else if value.is::() && !ranges.is_empty() { + return Err(PERR::WrongSwitchIntegerCase.into_err(expr.start_position())); + } else { + let hasher = &mut get_hasher(); + value.hash(hasher); + let hash = hasher.finish(); + + if !cases.is_empty() && cases.contains_key(&hash) { + return Err(PERR::DuplicatedSwitchCase.into_err(expr.start_position())); + } + (Some(hash), None) + } + } else { + (None, None) + }; + + match input.next().expect(NEVER_ENDS) { + (Token::DoubleArrow, ..) => (), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (.., pos) => { + return Err(PERR::MissingToken( + Token::DoubleArrow.into(), + "in this switch case".to_string(), + ) + .into_err(pos)) + } + }; + + let stmt = self.parse_stmt(input, state, lib, settings.level_up())?; + + let need_comma = !stmt.is_self_terminated(); + + def_stmt = match (hash, range) { + (None, Some(range)) => { + let is_empty = if range.2 { + (range.0..=range.1).is_empty() + } else { + (range.0..range.1).is_empty() + }; + + if !is_empty { + match (range.1.checked_sub(range.0), range.2) { + // Unroll single range + (Some(1), false) | (Some(0), true) => { + let value = Dynamic::from_int(range.0); + let hasher = &mut get_hasher(); + value.hash(hasher); + let hash = hasher.finish(); + + cases.entry(hash).or_insert_with(|| { + let block: ConditionalStmtBlock = (condition, stmt).into(); + block.into() + }); + } + // Other range + _ => { + let block: ConditionalStmtBlock = (condition, stmt).into(); + ranges.push((range.0, range.1, range.2, block.into())) + } + } + } + None + } + (Some(hash), None) => { + let block: ConditionalStmtBlock = (condition, stmt).into(); + cases.insert(hash, block.into()); + None + } + (None, None) => Some(Box::new(stmt.into())), + _ => unreachable!("both hash and range in switch statement case"), + }; + + match input.peek().expect(NEVER_ENDS) { + (Token::Comma, ..) => { + eat_token(input, Token::Comma); + } + (Token::RightBrace, ..) => (), + (Token::EOF, pos) => { + return Err( + PERR::MissingToken(Token::RightParen.into(), MISSING_RBRACE.into()) + .into_err(*pos), + ) + } + (Token::LexError(err), pos) => return Err(err.clone().into_err(*pos)), + (.., pos) if need_comma => { + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the items in this switch block".into(), + ) + .into_err(*pos)) + } + _ => (), + } + } + + let cases = SwitchCases { + cases, + def_case: def_stmt.unwrap_or_else(|| StmtBlock::NONE.into()), + ranges, + }; + + Ok(Stmt::Switch((item, cases).into(), settings.pos)) + } + + /// Parse a primary expression. + fn parse_primary( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + let (token, token_pos) = input.peek().expect(NEVER_ENDS); + + let mut settings = settings; + settings.pos = *token_pos; + + let root_expr = match token { + Token::EOF => return Err(PERR::UnexpectedEOF.into_err(settings.pos)), + + Token::IntegerConstant(..) + | Token::CharConstant(..) + | Token::StringConstant(..) + | Token::True + | Token::False => match input.next().expect(NEVER_ENDS).0 { + Token::IntegerConstant(x) => Expr::IntegerConstant(x, settings.pos), + Token::CharConstant(c) => Expr::CharConstant(c, settings.pos), + Token::StringConstant(s) => { + Expr::StringConstant(state.get_interned_string("", s), settings.pos) + } + Token::True => Expr::BoolConstant(true, settings.pos), + Token::False => Expr::BoolConstant(false, settings.pos), + token => unreachable!("token is {:?}", token), + }, + #[cfg(not(feature = "no_float"))] + Token::FloatConstant(x) => { + let x = *x; + input.next().expect(NEVER_ENDS); + Expr::FloatConstant(x, settings.pos) + } + #[cfg(feature = "decimal")] + Token::DecimalConstant(x) => { + let x = (*x).into(); + input.next().expect(NEVER_ENDS); + Expr::DynamicConstant(Box::new(x), settings.pos) + } + + // { - block statement as expression + Token::LeftBrace if settings.options.allow_stmt_expr => { + match self.parse_block(input, state, lib, settings.level_up())? { block @ Stmt::Block(..) => Expr::Stmt(Box::new(block.into())), stmt => unreachable!("Stmt::Block expected but gets {:?}", stmt), - }; - match expr { - Expr::StringConstant(s, ..) if s.is_empty() => (), - _ => segments.push(expr), + } + } + // ( - grouped expression + Token::LeftParen => self.parse_paren_expr(input, state, lib, settings.level_up())?, + + // If statement is allowed to act as expressions + Token::If if settings.options.allow_if_expr => Expr::Stmt(Box::new( + self.parse_if(input, state, lib, settings.level_up())? + .into(), + )), + // Switch statement is allowed to act as expressions + Token::Switch if settings.options.allow_switch_expr => Expr::Stmt(Box::new( + self.parse_switch(input, state, lib, settings.level_up())? + .into(), + )), + + // | ... + #[cfg(not(feature = "no_function"))] + Token::Pipe | Token::Or if settings.options.allow_anonymous_fn => { + let mut new_state = ParseState::new(self, state.tokenizer_control.clone()); + + #[cfg(not(feature = "unchecked"))] + { + new_state.max_expr_depth = self.max_function_expr_depth(); } - // Make sure to parse the following as text - let mut control = state.tokenizer_control.get(); - control.is_within_text = true; - state.tokenizer_control.set(control); + let new_settings = ParseSettings { + is_global: false, + is_function_scope: true, + #[cfg(not(feature = "no_closure"))] + is_closure_scope: true, + is_breakable: false, + level: 0, + options: LanguageOptions { + strict_var: if cfg!(feature = "no_closure") { + settings.options.strict_var + } else { + // A capturing closure can access variables not defined locally + false + }, + ..self.options + }, + ..settings + }; + + let (expr, func) = self.parse_anon_fn(input, &mut new_state, lib, new_settings)?; + + #[cfg(not(feature = "no_closure"))] + new_state.external_vars.iter().try_for_each( + |crate::ast::Ident { name, pos }| { + let index = state.access_var(name, *pos); + + if settings.options.strict_var + && !settings.is_closure_scope + && index.is_none() + { + // If the parent scope is not inside another capturing closure + // then we can conclude that the captured variable doesn't exist. + // Under Strict Variables mode, this is not allowed. + Err(PERR::VariableUndefined(name.to_string()).into_err(*pos)) + } else { + Ok::<_, ParseError>(()) + } + }, + )?; + + let hash_script = calc_fn_hash(&func.name, func.params.len()); + lib.insert(hash_script, func.into()); + + expr + } + + // Interpolated string + Token::InterpolatedString(..) => { + let mut segments = StaticVec::::new(); match input.next().expect(NEVER_ENDS) { - (Token::StringConstant(s), pos) => { - if !s.is_empty() { - segments.push(Expr::StringConstant(s.into(), pos)); - } - // End the interpolated string if it is terminated by a back-tick. - break; - } + (Token::InterpolatedString(s), ..) if s.is_empty() => (), (Token::InterpolatedString(s), pos) => { - if !s.is_empty() { - segments.push(Expr::StringConstant(s.into(), pos)); + segments.push(Expr::StringConstant(s.into(), pos)) + } + token => { + unreachable!("Token::InterpolatedString expected but gets {:?}", token) + } + } + + loop { + let expr = match self.parse_block(input, state, lib, settings.level_up())? { + block @ Stmt::Block(..) => Expr::Stmt(Box::new(block.into())), + stmt => unreachable!("Stmt::Block expected but gets {:?}", stmt), + }; + match expr { + Expr::StringConstant(s, ..) if s.is_empty() => (), + _ => segments.push(expr), + } + + // Make sure to parse the following as text + let mut control = state.tokenizer_control.get(); + control.is_within_text = true; + state.tokenizer_control.set(control); + + match input.next().expect(NEVER_ENDS) { + (Token::StringConstant(s), pos) => { + if !s.is_empty() { + segments.push(Expr::StringConstant(s.into(), pos)); + } + // End the interpolated string if it is terminated by a back-tick. + break; } - } - (Token::LexError(err @ LexError::UnterminatedString), pos) => { - return Err(err.into_err(pos)) - } - (token, ..) => unreachable!( - "string within an interpolated string literal expected but gets {:?}", - token - ), - } - } - - if segments.is_empty() { - Expr::StringConstant(state.get_interned_string("", ""), settings.pos) - } else { - segments.shrink_to_fit(); - Expr::InterpolatedString(segments.into(), settings.pos) - } - } - - // Array literal - #[cfg(not(feature = "no_index"))] - Token::LeftBracket => parse_array_literal(input, state, lib, settings.level_up())?, - - // Map literal - #[cfg(not(feature = "no_object"))] - Token::MapStart => parse_map_literal(input, state, lib, settings.level_up())?, - - // Custom syntax. - Token::Custom(key) | Token::Reserved(key) | Token::Identifier(key) - if state.engine.custom_syntax.contains_key(&**key) => - { - let (key, syntax) = state.engine.custom_syntax.get_key_value(&**key).unwrap(); - let (.., pos) = input.next().expect(NEVER_ENDS); - let settings2 = settings.level_up(); - parse_custom_syntax(input, state, lib, settings2, key, syntax, pos)? - } - - // Identifier - Token::Identifier(..) => { - #[cfg(not(feature = "no_module"))] - let none = None; - #[cfg(feature = "no_module")] - let none = (); - - let s = match input.next().expect(NEVER_ENDS) { - (Token::Identifier(s), ..) => s, - token => unreachable!("Token::Identifier expected but gets {:?}", token), - }; - - match input.peek().expect(NEVER_ENDS).0 { - // Function call - Token::LeftParen | Token::Bang => { - #[cfg(not(feature = "no_closure"))] - { - // Once the identifier consumed we must enable next variables capturing - state.allow_capture = true; - } - Expr::Variable( - None, - settings.pos, - (None, none, state.get_identifier("", s)).into(), - ) - } - // Namespace qualification - #[cfg(not(feature = "no_module"))] - Token::DoubleColon => { - #[cfg(not(feature = "no_closure"))] - { - // Once the identifier consumed we must enable next variables capturing - state.allow_capture = true; - } - Expr::Variable( - None, - settings.pos, - (None, none, state.get_identifier("", s)).into(), - ) - } - // Normal variable access - _ => { - let index = state.access_var(&s, settings.pos); - - if settings.strict_var && index.is_none() { - return Err(PERR::VariableUndefined(s.to_string()).into_err(settings.pos)); - } - - let short_index = index.and_then(|x| { - if x.get() <= u8::MAX as usize { - NonZeroU8::new(x.get() as u8) - } else { - None + (Token::InterpolatedString(s), pos) => { + if !s.is_empty() { + segments.push(Expr::StringConstant(s.into(), pos)); + } } - }); - Expr::Variable( - short_index, - settings.pos, - (index, none, state.get_identifier("", s)).into(), - ) + (Token::LexError(err), pos) + if matches!(*err, LexError::UnterminatedString) => + { + return Err(err.into_err(pos)) + } + (token, ..) => unreachable!( + "string within an interpolated string literal expected but gets {:?}", + token + ), + } } - } - } - // Reserved keyword or symbol - Token::Reserved(..) => { - #[cfg(not(feature = "no_module"))] - let none = None; - #[cfg(feature = "no_module")] - let none = (); - - let s = match input.next().expect(NEVER_ENDS) { - (Token::Reserved(s), ..) => s, - token => unreachable!("Token::Reserved expected but gets {:?}", token), - }; - - match input.peek().expect(NEVER_ENDS).0 { - // Function call is allowed to have reserved keyword - Token::LeftParen | Token::Bang if is_keyword_function(&s) => Expr::Variable( - None, - settings.pos, - (None, none, state.get_identifier("", s)).into(), - ), - // Access to `this` as a variable is OK within a function scope - #[cfg(not(feature = "no_function"))] - _ if &*s == KEYWORD_THIS && settings.is_function_scope => Expr::Variable( - None, - settings.pos, - (None, none, state.get_identifier("", s)).into(), - ), - // Cannot access to `this` as a variable not in a function scope - _ if &*s == KEYWORD_THIS => { - let msg = format!("'{}' can only be used in functions", s); - return Err(LexError::ImproperSymbol(s.to_string(), msg).into_err(settings.pos)); - } - _ => return Err(PERR::Reserved(s.to_string()).into_err(settings.pos)), - } - } - - Token::LexError(..) => match input.next().expect(NEVER_ENDS) { - (Token::LexError(err), ..) => return Err(err.into_err(settings.pos)), - token => unreachable!("Token::LexError expected but gets {:?}", token), - }, - - _ => { - return Err(LexError::UnexpectedInput(token.syntax().to_string()).into_err(settings.pos)) - } - }; - - parse_postfix(input, state, lib, root_expr, settings) -} - -/// Tail processing of all possible postfix operators of a primary expression. -fn parse_postfix( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - mut lhs: Expr, - settings: ParseSettings, -) -> ParseResult { - let mut settings = settings; - - // Tail processing all possible postfix operators - loop { - let (tail_token, ..) = input.peek().expect(NEVER_ENDS); - - if !lhs.is_valid_postfix(tail_token) { - break; - } - - let (tail_token, tail_pos) = input.next().expect(NEVER_ENDS); - settings.pos = tail_pos; - - lhs = match (lhs, tail_token) { - // Qualified function call with ! - #[cfg(not(feature = "no_module"))] - (Expr::Variable(.., x), Token::Bang) if x.1.is_some() => { - return if !match_token(input, Token::LeftParen).0 { - Err(LexError::UnexpectedInput(Token::Bang.syntax().to_string()) - .into_err(tail_pos)) + if segments.is_empty() { + Expr::StringConstant(state.get_interned_string("", ""), settings.pos) } else { - Err(LexError::ImproperSymbol( - "!".to_string(), - "'!' cannot be used to call module functions".to_string(), - ) - .into_err(tail_pos)) - }; + segments.shrink_to_fit(); + Expr::InterpolatedString(segments.into(), settings.pos) + } } - // Function call with ! - (Expr::Variable(.., pos, x), Token::Bang) => { - match match_token(input, Token::LeftParen) { - (false, pos) => { - return Err(PERR::MissingToken( - Token::LeftParen.syntax().into(), - "to start arguments list of function call".into(), + + // Array literal + #[cfg(not(feature = "no_index"))] + Token::LeftBracket => { + self.parse_array_literal(input, state, lib, settings.level_up())? + } + + // Map literal + #[cfg(not(feature = "no_object"))] + Token::MapStart => self.parse_map_literal(input, state, lib, settings.level_up())?, + + // Custom syntax. + Token::Custom(key) | Token::Reserved(key) | Token::Identifier(key) + if !self.custom_syntax.is_empty() && self.custom_syntax.contains_key(&**key) => + { + let (key, syntax) = self.custom_syntax.get_key_value(&**key).unwrap(); + let (.., pos) = input.next().expect(NEVER_ENDS); + let settings2 = settings.level_up(); + self.parse_custom_syntax(input, state, lib, settings2, key, syntax, pos)? + } + + // Identifier + Token::Identifier(..) => { + #[cfg(not(feature = "no_module"))] + let none = None; + #[cfg(feature = "no_module")] + let none = (); + + let s = match input.next().expect(NEVER_ENDS) { + (Token::Identifier(s), ..) => s, + token => unreachable!("Token::Identifier expected but gets {:?}", token), + }; + + match input.peek().expect(NEVER_ENDS).0 { + // Function call + Token::LeftParen | Token::Bang => { + #[cfg(not(feature = "no_closure"))] + { + // Once the identifier consumed we must enable next variables capturing + state.allow_capture = true; + } + Expr::Variable( + None, + settings.pos, + (None, none, state.get_identifier("", s)).into(), + ) + } + // Namespace qualification + #[cfg(not(feature = "no_module"))] + Token::DoubleColon => { + #[cfg(not(feature = "no_closure"))] + { + // Once the identifier consumed we must enable next variables capturing + state.allow_capture = true; + } + Expr::Variable( + None, + settings.pos, + (None, none, state.get_identifier("", s)).into(), + ) + } + // Normal variable access + _ => { + let index = state.access_var(&s, settings.pos); + + if settings.options.strict_var && index.is_none() { + return Err( + PERR::VariableUndefined(s.to_string()).into_err(settings.pos) + ); + } + + let short_index = index.and_then(|x| { + if x.get() <= u8::MAX as usize { + NonZeroU8::new(x.get() as u8) + } else { + None + } + }); + Expr::Variable( + short_index, + settings.pos, + (index, none, state.get_identifier("", s)).into(), + ) + } + } + } + + // Reserved keyword or symbol + Token::Reserved(..) => { + #[cfg(not(feature = "no_module"))] + let none = None; + #[cfg(feature = "no_module")] + let none = (); + + let s = match input.next().expect(NEVER_ENDS) { + (Token::Reserved(s), ..) => s, + token => unreachable!("Token::Reserved expected but gets {:?}", token), + }; + + match input.peek().expect(NEVER_ENDS).0 { + // Function call is allowed to have reserved keyword + Token::LeftParen | Token::Bang if is_keyword_function(&s) => Expr::Variable( + None, + settings.pos, + (None, none, state.get_identifier("", s)).into(), + ), + // Access to `this` as a variable is OK within a function scope + #[cfg(not(feature = "no_function"))] + _ if &*s == KEYWORD_THIS && settings.is_function_scope => Expr::Variable( + None, + settings.pos, + (None, none, state.get_identifier("", s)).into(), + ), + // Cannot access to `this` as a variable not in a function scope + _ if &*s == KEYWORD_THIS => { + let msg = format!("'{}' can only be used in functions", s); + return Err( + LexError::ImproperSymbol(s.to_string(), msg).into_err(settings.pos) + ); + } + _ => return Err(PERR::Reserved(s.to_string()).into_err(settings.pos)), + } + } + + Token::LexError(..) => match input.next().expect(NEVER_ENDS) { + (Token::LexError(err), ..) => return Err(err.into_err(settings.pos)), + token => unreachable!("Token::LexError expected but gets {:?}", token), + }, + + _ => { + return Err( + LexError::UnexpectedInput(token.syntax().to_string()).into_err(settings.pos) + ) + } + }; + + self.parse_postfix(input, state, lib, root_expr, settings) + } + + /// Tail processing of all possible postfix operators of a primary expression. + fn parse_postfix( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + mut lhs: Expr, + settings: ParseSettings, + ) -> ParseResult { + let mut settings = settings; + + // Tail processing all possible postfix operators + loop { + let (tail_token, ..) = input.peek().expect(NEVER_ENDS); + + if !lhs.is_valid_postfix(tail_token) { + break; + } + + let (tail_token, tail_pos) = input.next().expect(NEVER_ENDS); + settings.pos = tail_pos; + + lhs = match (lhs, tail_token) { + // Qualified function call with ! + #[cfg(not(feature = "no_module"))] + (Expr::Variable(.., x), Token::Bang) if x.1.is_some() => { + return if !match_token(input, Token::LeftParen).0 { + Err(LexError::UnexpectedInput(Token::Bang.syntax().to_string()) + .into_err(tail_pos)) + } else { + Err(LexError::ImproperSymbol( + "!".to_string(), + "'!' cannot be used to call module functions".to_string(), + ) + .into_err(tail_pos)) + }; + } + // Function call with ! + (Expr::Variable(.., pos, x), Token::Bang) => { + match match_token(input, Token::LeftParen) { + (false, pos) => { + return Err(PERR::MissingToken( + Token::LeftParen.syntax().into(), + "to start arguments list of function call".into(), + ) + .into_err(pos)) + } + _ => (), + } + + let (.., _ns, name) = *x; + settings.pos = pos; + #[cfg(not(feature = "no_module"))] + let _ns = _ns.map(|(ns, ..)| ns); + self.parse_fn_call( + input, + state, + lib, + name, + true, + #[cfg(not(feature = "no_module"))] + _ns, + settings.level_up(), + )? + } + // Function call + (Expr::Variable(.., pos, x), Token::LeftParen) => { + let (.., _ns, name) = *x; + #[cfg(not(feature = "no_module"))] + let _ns = _ns.map(|(ns, ..)| ns); + settings.pos = pos; + self.parse_fn_call( + input, + state, + lib, + name, + false, + #[cfg(not(feature = "no_module"))] + _ns, + settings.level_up(), + )? + } + // module access + #[cfg(not(feature = "no_module"))] + (Expr::Variable(.., pos, x), Token::DoubleColon) => { + let (id2, pos2) = parse_var_name(input)?; + let (.., mut namespace, name) = *x; + let var_name_def = Ident { name, pos }; + + if let Some((ref mut namespace, ..)) = namespace { + namespace.push(var_name_def); + } else { + let mut ns = crate::module::Namespace::new(); + ns.push(var_name_def); + namespace = Some((ns, 42)); + } + + Expr::Variable( + None, + pos2, + (None, namespace, state.get_identifier("", id2)).into(), + ) + } + // Indexing + #[cfg(not(feature = "no_index"))] + (expr, Token::LeftBracket) => { + self.parse_index_chain(input, state, lib, expr, settings.level_up())? + } + // Property access + #[cfg(not(feature = "no_object"))] + (expr, Token::Period) => { + // Expression after dot must start with an identifier + match input.peek().expect(NEVER_ENDS) { + (Token::Identifier(..), ..) => { + #[cfg(not(feature = "no_closure"))] + { + // Prevents capturing of the object properties as vars: xxx. + state.allow_capture = false; + } + } + (Token::Reserved(s), ..) if is_keyword_function(s) => (), + (.., pos) => return Err(PERR::PropertyExpected.into_err(*pos)), + } + + let rhs = self.parse_primary(input, state, lib, settings.level_up())?; + Self::make_dot_expr(state, expr, ASTFlags::NONE, rhs, tail_pos)? + } + // Unknown postfix operator + (expr, token) => unreachable!( + "unknown postfix operator '{}' for {:?}", + token.syntax(), + expr + ), + } + } + + // Cache the hash key for namespace-qualified variables + #[cfg(not(feature = "no_module"))] + let namespaced_variable = match lhs { + Expr::Variable(.., ref mut x) if x.1.is_some() => Some(x.as_mut()), + Expr::Index(ref mut x, ..) | Expr::Dot(ref mut x, ..) => match x.lhs { + Expr::Variable(.., ref mut x) if x.1.is_some() => Some(x.as_mut()), + _ => None, + }, + _ => None, + }; + + #[cfg(not(feature = "no_module"))] + if let Some((.., Some((namespace, hash)), name)) = namespaced_variable { + *hash = crate::calc_qualified_var_hash(namespace.iter().map(|v| v.name.as_str()), name); + + #[cfg(not(feature = "no_module"))] + { + let index = state.find_module(namespace.root()); + + #[cfg(not(feature = "no_function"))] + let relax = settings.is_function_scope; + #[cfg(feature = "no_function")] + let relax = false; + + if !relax && settings.options.strict_var && index.is_none() { + return Err(PERR::ModuleUndefined(namespace.root().to_string()) + .into_err(namespace.position())); + } + + namespace.set_index(index); + } + } + + // Make sure identifiers are valid + Ok(lhs) + } + + /// Parse a potential unary operator. + fn parse_unary( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + let (token, token_pos) = input.peek().expect(NEVER_ENDS); + + let mut settings = settings; + settings.pos = *token_pos; + + match token { + // -expr + Token::Minus | Token::UnaryMinus => { + let token = token.clone(); + let pos = eat_token(input, token); + + match self.parse_unary(input, state, lib, settings.level_up())? { + // Negative integer + Expr::IntegerConstant(num, ..) => num + .checked_neg() + .map(|i| Expr::IntegerConstant(i, pos)) + .or_else(|| { + #[cfg(not(feature = "no_float"))] + return Some(Expr::FloatConstant((-(num as crate::FLOAT)).into(), pos)); + #[cfg(feature = "no_float")] + return None; + }) + .ok_or_else(|| { + LexError::MalformedNumber(format!("-{}", num)).into_err(pos) + }), + + // Negative float + #[cfg(not(feature = "no_float"))] + Expr::FloatConstant(x, ..) => Ok(Expr::FloatConstant((-(*x)).into(), pos)), + + // Call negative function + expr => { + let mut args = StaticVec::new_const(); + args.push(expr); + args.shrink_to_fit(); + + Ok(FnCallExpr { + name: state.get_identifier("", "-"), + hashes: FnCallHashes::from_native(calc_fn_hash("-", 1)), + args, + pos, + ..Default::default() + } + .into_fn_call_expr(pos)) + } + } + } + // +expr + Token::Plus | Token::UnaryPlus => { + let token = token.clone(); + let pos = eat_token(input, token); + + match self.parse_unary(input, state, lib, settings.level_up())? { + expr @ Expr::IntegerConstant(..) => Ok(expr), + #[cfg(not(feature = "no_float"))] + expr @ Expr::FloatConstant(..) => Ok(expr), + + // Call plus function + expr => { + let mut args = StaticVec::new_const(); + args.push(expr); + args.shrink_to_fit(); + + Ok(FnCallExpr { + name: state.get_identifier("", "+"), + hashes: FnCallHashes::from_native(calc_fn_hash("+", 1)), + args, + pos, + ..Default::default() + } + .into_fn_call_expr(pos)) + } + } + } + // !expr + Token::Bang => { + let pos = eat_token(input, Token::Bang); + let mut args = StaticVec::new_const(); + args.push(self.parse_unary(input, state, lib, settings.level_up())?); + args.shrink_to_fit(); + + Ok(FnCallExpr { + name: state.get_identifier("", "!"), + hashes: FnCallHashes::from_native(calc_fn_hash("!", 1)), + args, + pos, + ..Default::default() + } + .into_fn_call_expr(pos)) + } + // + Token::EOF => Err(PERR::UnexpectedEOF.into_err(settings.pos)), + // All other tokens + _ => self.parse_primary(input, state, lib, settings.level_up()), + } + } + + /// Make an assignment statement. + fn make_assignment_stmt( + op: Option, + state: &mut ParseState, + lhs: Expr, + rhs: Expr, + op_pos: Position, + ) -> ParseResult { + #[must_use] + fn check_lvalue(expr: &Expr, parent_is_dot: bool) -> Option { + match expr { + Expr::Index(x, options, ..) | Expr::Dot(x, options, ..) if parent_is_dot => { + match x.lhs { + Expr::Property(..) if !options.contains(ASTFlags::BREAK) => { + check_lvalue(&x.rhs, matches!(expr, Expr::Dot(..))) + } + Expr::Property(..) => None, + // Anything other than a property after dotting (e.g. a method call) is not an l-value + ref e => Some(e.position()), + } + } + Expr::Index(x, options, ..) | Expr::Dot(x, options, ..) => match x.lhs { + Expr::Property(..) => unreachable!("unexpected Expr::Property in indexing"), + _ if !options.contains(ASTFlags::BREAK) => { + check_lvalue(&x.rhs, matches!(expr, Expr::Dot(..))) + } + _ => None, + }, + Expr::Property(..) if parent_is_dot => None, + Expr::Property(..) => unreachable!("unexpected Expr::Property in indexing"), + e if parent_is_dot => Some(e.position()), + _ => None, + } + } + + let op_info = op.map(OpAssignment::new_from_token); + + match lhs { + // const_expr = rhs + ref expr if expr.is_constant() => { + Err(PERR::AssignmentToConstant("".into()).into_err(lhs.start_position())) + } + // var (non-indexed) = rhs + Expr::Variable(None, _, ref x) if x.0.is_none() => Ok(Stmt::Assignment( + (op_info, (lhs, rhs).into()).into(), + op_pos, + )), + // var (indexed) = rhs + Expr::Variable(i, var_pos, ref x) => { + let (index, _, name) = x.as_ref(); + let index = i.map_or_else( + || index.expect("either long or short index is `None`").get(), + |n| n.get() as usize, + ); + match state + .stack + .get_mut_by_index(state.stack.len() - index) + .access_mode() + { + AccessMode::ReadWrite => Ok(Stmt::Assignment( + (op_info, (lhs, rhs).into()).into(), + op_pos, + )), + // Constant values cannot be assigned to + AccessMode::ReadOnly => { + Err(PERR::AssignmentToConstant(name.to_string()).into_err(var_pos)) + } + } + } + // xxx[???]... = rhs, xxx.prop... = rhs + Expr::Index(ref x, options, ..) | Expr::Dot(ref x, options, ..) => { + let valid_lvalue = if options.contains(ASTFlags::BREAK) { + None + } else { + check_lvalue(&x.rhs, matches!(lhs, Expr::Dot(..))) + }; + + match valid_lvalue { + None => { + match x.lhs { + // var[???] = rhs, var.??? = rhs + Expr::Variable(..) => Ok(Stmt::Assignment( + (op_info, (lhs, rhs).into()).into(), + op_pos, + )), + // expr[???] = rhs, expr.??? = rhs + ref expr => Err(PERR::AssignmentToInvalidLHS("".to_string()) + .into_err(expr.position())), + } + } + Some(err_pos) => { + Err(PERR::AssignmentToInvalidLHS("".to_string()).into_err(err_pos)) + } + } + } + // ??? && ??? = rhs, ??? || ??? = rhs + Expr::And(..) | Expr::Or(..) => Err(LexError::ImproperSymbol( + "=".to_string(), + "Possibly a typo of '=='?".to_string(), + ) + .into_err(op_pos)), + // expr = rhs + _ => Err(PERR::AssignmentToInvalidLHS("".to_string()).into_err(lhs.position())), + } + } + + /// Parse an operator-assignment expression (if any). + fn parse_op_assignment_stmt( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + lhs: Expr, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + let (op, pos) = match input.peek().expect(NEVER_ENDS) { + // var = ... + (Token::Equals, ..) => (None, eat_token(input, Token::Equals)), + // var op= ... + (token, ..) if token.is_op_assignment() => input + .next() + .map(|(op, pos)| (Some(op), pos)) + .expect(NEVER_ENDS), + // Not op-assignment + _ => return Ok(Stmt::Expr(lhs.into())), + }; + + let mut settings = settings; + settings.pos = pos; + + let rhs = self.parse_expr(input, state, lib, settings.level_up())?; + Self::make_assignment_stmt(op, state, lhs, rhs, pos) + } + + /// Make a dot expression. + #[cfg(not(feature = "no_object"))] + fn make_dot_expr( + state: &mut ParseState, + lhs: Expr, + parent_options: ASTFlags, + rhs: Expr, + op_pos: Position, + ) -> ParseResult { + match (lhs, rhs) { + // lhs[idx_expr].rhs + (Expr::Index(mut x, options, pos), rhs) => { + x.rhs = Self::make_dot_expr(state, x.rhs, options | parent_options, rhs, op_pos)?; + Ok(Expr::Index(x, ASTFlags::NONE, pos)) + } + // lhs.module::id - syntax error + #[cfg(not(feature = "no_module"))] + (.., Expr::Variable(.., x)) if x.1.is_some() => { + Err(PERR::PropertyExpected.into_err(x.1.expect("`Some`").0.position())) + } + // lhs.id + (lhs, var_expr @ Expr::Variable(..)) => { + let rhs = var_expr.into_property(state); + Ok(Expr::Dot( + BinaryExpr { lhs, rhs }.into(), + ASTFlags::NONE, + op_pos, + )) + } + // lhs.prop + (lhs, prop @ Expr::Property(..)) => Ok(Expr::Dot( + BinaryExpr { lhs, rhs: prop }.into(), + ASTFlags::NONE, + op_pos, + )), + // lhs.nnn::func(...) - syntax error + #[cfg(not(feature = "no_module"))] + (.., Expr::FnCall(func, ..)) if func.is_qualified() => { + Err(PERR::PropertyExpected.into_err(func.namespace.expect("`Some`").position())) + } + // lhs.Fn() or lhs.eval() + (.., Expr::FnCall(func, func_pos)) + if func.args.is_empty() + && [crate::engine::KEYWORD_FN_PTR, crate::engine::KEYWORD_EVAL] + .contains(&func.name.as_ref()) => + { + let err_msg = format!( + "'{}' should not be called in method style. Try {}(...);", + func.name, func.name + ); + Err(LexError::ImproperSymbol(func.name.to_string(), err_msg).into_err(func_pos)) + } + // lhs.func!(...) + (.., Expr::FnCall(func, func_pos)) if func.capture_parent_scope => { + Err(PERR::MalformedCapture( + "method-call style does not support running within the caller's scope".into(), + ) + .into_err(func_pos)) + } + // lhs.func(...) + (lhs, Expr::FnCall(mut func, func_pos)) => { + // Recalculate hash + func.hashes = FnCallHashes::from_all( + #[cfg(not(feature = "no_function"))] + calc_fn_hash(&func.name, func.args.len()), + calc_fn_hash(&func.name, func.args.len() + 1), + ); + + let rhs = Expr::MethodCall(func, func_pos); + Ok(Expr::Dot( + BinaryExpr { lhs, rhs }.into(), + ASTFlags::NONE, + op_pos, + )) + } + // lhs.dot_lhs.dot_rhs or lhs.dot_lhs[idx_rhs] + (lhs, rhs @ Expr::Dot(..)) | (lhs, rhs @ Expr::Index(..)) => { + let (x, term, pos, is_dot) = match rhs { + Expr::Dot(x, term, pos) => (x, term, pos, true), + Expr::Index(x, term, pos) => (x, term, pos, false), + expr => unreachable!("Expr::Dot or Expr::Index expected but gets {:?}", expr), + }; + + match x.lhs { + // lhs.module::id.dot_rhs or lhs.module::id[idx_rhs] - syntax error + #[cfg(not(feature = "no_module"))] + Expr::Variable(.., x) if x.1.is_some() => { + Err(PERR::PropertyExpected.into_err(x.1.expect("`Some`").0.position())) + } + // lhs.module::func().dot_rhs or lhs.module::func()[idx_rhs] - syntax error + #[cfg(not(feature = "no_module"))] + Expr::FnCall(func, ..) if func.is_qualified() => { + Err(PERR::PropertyExpected + .into_err(func.namespace.expect("`Some`").position())) + } + // lhs.id.dot_rhs or lhs.id[idx_rhs] + Expr::Variable(..) | Expr::Property(..) => { + let new_lhs = BinaryExpr { + lhs: x.lhs.into_property(state), + rhs: x.rhs, + } + .into(); + + let rhs = if is_dot { + Expr::Dot(new_lhs, term, pos) + } else { + Expr::Index(new_lhs, term, pos) + }; + Ok(Expr::Dot( + BinaryExpr { lhs, rhs }.into(), + ASTFlags::NONE, + op_pos, + )) + } + // lhs.func().dot_rhs or lhs.func()[idx_rhs] + Expr::FnCall(mut func, func_pos) => { + // Recalculate hash + func.hashes = FnCallHashes::from_all( + #[cfg(not(feature = "no_function"))] + calc_fn_hash(&func.name, func.args.len()), + calc_fn_hash(&func.name, func.args.len() + 1), + ); + + let new_lhs = BinaryExpr { + lhs: Expr::MethodCall(func, func_pos), + rhs: x.rhs, + } + .into(); + + let rhs = if is_dot { + Expr::Dot(new_lhs, term, pos) + } else { + Expr::Index(new_lhs, term, pos) + }; + Ok(Expr::Dot( + BinaryExpr { lhs, rhs }.into(), + ASTFlags::NONE, + op_pos, + )) + } + expr => unreachable!("invalid dot expression: {:?}", expr), + } + } + // lhs.rhs + (.., rhs) => Err(PERR::PropertyExpected.into_err(rhs.start_position())), + } + } + + /// Parse a binary expression (if any). + fn parse_binary_op( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + parent_precedence: Option, + lhs: Expr, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + let mut settings = settings; + settings.pos = lhs.position(); + + let mut root = lhs; + + loop { + let (current_op, current_pos) = input.peek().expect(NEVER_ENDS); + let precedence = match current_op { + Token::Custom(c) => self + .custom_keywords + .get(c) + .cloned() + .ok_or_else(|| PERR::Reserved(c.to_string()).into_err(*current_pos))?, + Token::Reserved(c) if !is_valid_identifier(c.chars()) => { + return Err(PERR::UnknownOperator(c.to_string()).into_err(*current_pos)) + } + _ => current_op.precedence(), + }; + let bind_right = current_op.is_bind_right(); + + // Bind left to the parent lhs expression if precedence is higher + // If same precedence, then check if the operator binds right + if precedence < parent_precedence || (precedence == parent_precedence && !bind_right) { + return Ok(root); + } + + let (op_token, pos) = input.next().expect(NEVER_ENDS); + + let rhs = self.parse_unary(input, state, lib, settings)?; + + let (next_op, next_pos) = input.peek().expect(NEVER_ENDS); + let next_precedence = match next_op { + Token::Custom(c) => self + .custom_keywords + .get(c) + .cloned() + .ok_or_else(|| PERR::Reserved(c.to_string()).into_err(*next_pos))?, + Token::Reserved(c) if !is_valid_identifier(c.chars()) => { + return Err(PERR::UnknownOperator(c.to_string()).into_err(*next_pos)) + } + _ => next_op.precedence(), + }; + + // Bind to right if the next operator has higher precedence + // If same precedence, then check if the operator binds right + let rhs = + if (precedence == next_precedence && bind_right) || precedence < next_precedence { + self.parse_binary_op(input, state, lib, precedence, rhs, settings)? + } else { + // Otherwise bind to left (even if next operator has the same precedence) + rhs + }; + + settings = settings.level_up(); + settings.pos = pos; + + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + let op = op_token.syntax(); + let hash = calc_fn_hash(&op, 2); + + let op_base = FnCallExpr { + name: state.get_identifier("", op), + hashes: FnCallHashes::from_native(hash), + pos, + ..Default::default() + }; + + let mut args = StaticVec::new_const(); + args.push(root); + args.push(rhs); + args.shrink_to_fit(); + + root = match op_token { + // '!=' defaults to true when passed invalid operands + Token::NotEqualsTo => FnCallExpr { args, ..op_base }.into_fn_call_expr(pos), + + // Comparison operators default to false when passed invalid operands + Token::EqualsTo + | Token::LessThan + | Token::LessThanEqualsTo + | Token::GreaterThan + | Token::GreaterThanEqualsTo => { + let pos = args[0].start_position(); + FnCallExpr { args, ..op_base }.into_fn_call_expr(pos) + } + + Token::Or => { + let rhs = args.pop().unwrap(); + let current_lhs = args.pop().unwrap(); + Expr::Or( + BinaryExpr { + lhs: current_lhs.ensure_bool_expr()?, + rhs: rhs.ensure_bool_expr()?, + } + .into(), + pos, + ) + } + Token::And => { + let rhs = args.pop().unwrap(); + let current_lhs = args.pop().unwrap(); + Expr::And( + BinaryExpr { + lhs: current_lhs.ensure_bool_expr()?, + rhs: rhs.ensure_bool_expr()?, + } + .into(), + pos, + ) + } + Token::In => { + // Swap the arguments + let current_lhs = args.remove(0); + let pos = current_lhs.start_position(); + args.push(current_lhs); + args.shrink_to_fit(); + + // Convert into a call to `contains` + FnCallExpr { + hashes: calc_fn_hash(OP_CONTAINS, 2).into(), + args, + name: state.get_identifier("", OP_CONTAINS), + ..op_base + } + .into_fn_call_expr(pos) + } + + Token::Custom(s) + if self + .custom_keywords + .get(s.as_str()) + .map_or(false, Option::is_some) => + { + let hash = calc_fn_hash(&s, 2); + let pos = args[0].start_position(); + + FnCallExpr { + hashes: if is_valid_function_name(&s) { + hash.into() + } else { + FnCallHashes::from_native(hash) + }, + args, + ..op_base + } + .into_fn_call_expr(pos) + } + + _ => { + let pos = args[0].start_position(); + FnCallExpr { args, ..op_base }.into_fn_call_expr(pos) + } + }; + } + } + + /// Parse a custom syntax. + fn parse_custom_syntax( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + key: impl Into, + syntax: &CustomSyntax, + pos: Position, + ) -> ParseResult { + let mut settings = settings; + let mut inputs = StaticVec::::new(); + let mut segments = StaticVec::new_const(); + let mut tokens = StaticVec::new_const(); + + // Adjust the variables stack + if syntax.scope_may_be_changed { + // Add a barrier variable to the stack so earlier variables will not be matched. + // Variable searches stop at the first barrier. + let marker = state.get_identifier("", SCOPE_SEARCH_BARRIER_MARKER); + state.stack.push(marker, ()); + } + + let parse_func = syntax.parse.as_ref(); + let mut required_token: ImmutableString = key.into(); + + tokens.push(required_token.clone().into()); + segments.push(required_token.clone()); + + loop { + let (fwd_token, fwd_pos) = input.peek().expect(NEVER_ENDS); + settings.pos = *fwd_pos; + let settings = settings.level_up(); + + required_token = match parse_func(&segments, &*fwd_token.syntax()) { + Ok(Some(seg)) + if seg.starts_with(CUSTOM_SYNTAX_MARKER_SYNTAX_VARIANT) + && seg.len() > CUSTOM_SYNTAX_MARKER_SYNTAX_VARIANT.len() => + { + inputs.push(Expr::StringConstant( + state.get_interned_string("", seg), + pos, + )); + break; + } + Ok(Some(seg)) => seg, + Ok(None) => break, + Err(err) => return Err(err.0.into_err(settings.pos)), + }; + + match required_token.as_str() { + CUSTOM_SYNTAX_MARKER_IDENT => { + let (name, pos) = parse_var_name(input)?; + let name = state.get_identifier("", name); + segments.push(name.clone().into()); + tokens.push(state.get_identifier("", CUSTOM_SYNTAX_MARKER_IDENT)); + inputs.push(Expr::Variable( + None, + pos, + ( + None, + #[cfg(not(feature = "no_module"))] + None, + #[cfg(feature = "no_module")] + (), + name, + ) + .into(), + )); + } + CUSTOM_SYNTAX_MARKER_SYMBOL => { + let (symbol, pos) = parse_symbol(input)?; + let symbol = state.get_interned_string("", symbol); + segments.push(symbol.clone()); + tokens.push(state.get_identifier("", CUSTOM_SYNTAX_MARKER_SYMBOL)); + inputs.push(Expr::StringConstant(symbol, pos)); + } + CUSTOM_SYNTAX_MARKER_EXPR => { + inputs.push(self.parse_expr(input, state, lib, settings)?); + let keyword = state.get_identifier("", CUSTOM_SYNTAX_MARKER_EXPR); + segments.push(keyword.clone().into()); + tokens.push(keyword); + } + CUSTOM_SYNTAX_MARKER_BLOCK => { + match self.parse_block(input, state, lib, settings)? { + block @ Stmt::Block(..) => { + inputs.push(Expr::Stmt(Box::new(block.into()))); + let keyword = state.get_identifier("", CUSTOM_SYNTAX_MARKER_BLOCK); + segments.push(keyword.clone().into()); + tokens.push(keyword); + } + stmt => unreachable!("Stmt::Block expected but gets {:?}", stmt), + } + } + CUSTOM_SYNTAX_MARKER_BOOL => match input.next().expect(NEVER_ENDS) { + (b @ Token::True, pos) | (b @ Token::False, pos) => { + inputs.push(Expr::BoolConstant(b == Token::True, pos)); + segments.push(state.get_interned_string("", b.literal_syntax())); + tokens.push(state.get_identifier("", CUSTOM_SYNTAX_MARKER_BOOL)); + } + (.., pos) => { + return Err( + PERR::MissingSymbol("Expecting 'true' or 'false'".to_string()) + .into_err(pos), + ) + } + }, + CUSTOM_SYNTAX_MARKER_INT => match input.next().expect(NEVER_ENDS) { + (Token::IntegerConstant(i), pos) => { + inputs.push(Expr::IntegerConstant(i, pos)); + segments.push(i.to_string().into()); + tokens.push(state.get_identifier("", CUSTOM_SYNTAX_MARKER_INT)); + } + (.., pos) => { + return Err( + PERR::MissingSymbol("Expecting an integer number".to_string()) + .into_err(pos), + ) + } + }, + #[cfg(not(feature = "no_float"))] + CUSTOM_SYNTAX_MARKER_FLOAT => match input.next().expect(NEVER_ENDS) { + (Token::FloatConstant(f), pos) => { + inputs.push(Expr::FloatConstant(f, pos)); + segments.push(f.to_string().into()); + tokens.push(state.get_identifier("", CUSTOM_SYNTAX_MARKER_FLOAT)); + } + (.., pos) => { + return Err(PERR::MissingSymbol( + "Expecting a floating-point number".to_string(), ) .into_err(pos)) } - _ => (), - } - - let (.., _ns, name) = *x; - settings.pos = pos; - #[cfg(not(feature = "no_module"))] - let _ns = _ns.map(|(ns, ..)| ns); - parse_fn_call( - input, - state, - lib, - name, - true, - #[cfg(not(feature = "no_module"))] - _ns, - settings.level_up(), - )? - } - // Function call - (Expr::Variable(.., pos, x), Token::LeftParen) => { - let (.., _ns, name) = *x; - #[cfg(not(feature = "no_module"))] - let _ns = _ns.map(|(ns, ..)| ns); - settings.pos = pos; - parse_fn_call( - input, - state, - lib, - name, - false, - #[cfg(not(feature = "no_module"))] - _ns, - settings.level_up(), - )? - } - // module access - #[cfg(not(feature = "no_module"))] - (Expr::Variable(.., pos, x), Token::DoubleColon) => { - let (id2, pos2) = parse_var_name(input)?; - let (.., mut namespace, name) = *x; - let var_name_def = Ident { name, pos }; - - if let Some((ref mut namespace, ..)) = namespace { - namespace.push(var_name_def); - } else { - let mut ns = crate::module::Namespace::new(); - ns.push(var_name_def); - namespace = Some((ns, 42)); - } - - Expr::Variable( - None, - pos2, - (None, namespace, state.get_identifier("", id2)).into(), - ) - } - // Indexing - #[cfg(not(feature = "no_index"))] - (expr, Token::LeftBracket) => { - parse_index_chain(input, state, lib, expr, settings.level_up())? - } - // Property access - #[cfg(not(feature = "no_object"))] - (expr, Token::Period) => { - // Expression after dot must start with an identifier - match input.peek().expect(NEVER_ENDS) { - (Token::Identifier(..), ..) => { - #[cfg(not(feature = "no_closure"))] - { - // Prevents capturing of the object properties as vars: xxx. - state.allow_capture = false; - } + }, + CUSTOM_SYNTAX_MARKER_STRING => match input.next().expect(NEVER_ENDS) { + (Token::StringConstant(s), pos) => { + let s = state.get_interned_string("", s); + inputs.push(Expr::StringConstant(s.clone(), pos)); + segments.push(s); + tokens.push(state.get_identifier("", CUSTOM_SYNTAX_MARKER_STRING)); } - (Token::Reserved(s), ..) if is_keyword_function(s) => (), - (.., pos) => return Err(PERR::PropertyExpected.into_err(*pos)), - } - - let rhs = parse_primary(input, state, lib, settings.level_up())?; - make_dot_expr(state, expr, false, rhs, tail_pos)? + (.., pos) => { + return Err( + PERR::MissingSymbol("Expecting a string".to_string()).into_err(pos) + ) + } + }, + s => match input.next().expect(NEVER_ENDS) { + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (t, ..) if &*t.syntax() == s => { + segments.push(required_token.clone()); + tokens.push(required_token.clone().into()); + } + (.., pos) => { + return Err(PERR::MissingToken( + s.to_string(), + format!("for '{}' expression", segments[0]), + ) + .into_err(pos)) + } + }, } - // Unknown postfix operator - (expr, token) => unreachable!( - "unknown postfix operator '{}' for {:?}", - token.syntax(), - expr - ), } + + inputs.shrink_to_fit(); + tokens.shrink_to_fit(); + + const KEYWORD_SEMICOLON: &str = Token::SemiColon.literal_syntax(); + const KEYWORD_CLOSE_BRACE: &str = Token::RightBrace.literal_syntax(); + + let self_terminated = match required_token.as_str() { + // It is self-terminating if the last symbol is a block + CUSTOM_SYNTAX_MARKER_BLOCK => true, + // If the last symbol is `;` or `}`, it is self-terminating + KEYWORD_SEMICOLON | KEYWORD_CLOSE_BRACE => true, + _ => false, + }; + + Ok(Expr::Custom( + CustomExpr { + inputs, + tokens, + scope_may_be_changed: syntax.scope_may_be_changed, + self_terminated, + } + .into(), + pos, + )) } - // Cache the hash key for namespace-qualified variables - #[cfg(not(feature = "no_module"))] - let namespaced_variable = match lhs { - Expr::Variable(.., ref mut x) if x.1.is_some() => Some(x.as_mut()), - Expr::Index(ref mut x, ..) | Expr::Dot(ref mut x, ..) => match x.lhs { - Expr::Variable(.., ref mut x) if x.1.is_some() => Some(x.as_mut()), - _ => None, - }, - _ => None, - }; + /// Parse an expression. + fn parse_expr( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; - #[cfg(not(feature = "no_module"))] - if let Some((.., Some((namespace, hash)), name)) = namespaced_variable { - *hash = crate::calc_qualified_var_hash(namespace.iter().map(|v| v.name.as_str()), name); + let mut settings = settings; + settings.pos = input.peek().expect(NEVER_ENDS).1; - #[cfg(not(feature = "no_module"))] - { - let index = state.find_module(&namespace[0].name); + // Parse expression normally. + let precedence = Precedence::new(1); + let lhs = self.parse_unary(input, state, lib, settings.level_up())?; + self.parse_binary_op(input, state, lib, precedence, lhs, settings.level_up()) + } - #[cfg(not(feature = "no_function"))] - let relax = settings.is_function_scope; - #[cfg(feature = "no_function")] - let relax = false; + /// Parse an if statement. + fn parse_if( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; - if !relax && settings.strict_var && index.is_none() { + // if ... + let mut settings = settings; + settings.pos = eat_token(input, Token::If); + + // if guard { if_body } + ensure_not_statement_expr(input, "a boolean")?; + let guard = self + .parse_expr(input, state, lib, settings.level_up())? + .ensure_bool_expr()?; + ensure_not_assignment(input)?; + let if_body = self.parse_block(input, state, lib, settings.level_up())?; + + // if guard { if_body } else ... + let else_body = if match_token(input, Token::Else).0 { + if let (Token::If, ..) = input.peek().expect(NEVER_ENDS) { + // if guard { if_body } else if ... + self.parse_if(input, state, lib, settings.level_up())? + } else { + // if guard { if_body } else { else-body } + self.parse_block(input, state, lib, settings.level_up())? + } + } else { + Stmt::Noop(Position::NONE) + }; + + Ok(Stmt::If( + (guard, if_body.into(), else_body.into()).into(), + settings.pos, + )) + } + + /// Parse a while loop. + fn parse_while_loop( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + let mut settings = settings; + + // while|loops ... + let (guard, token_pos) = match input.next().expect(NEVER_ENDS) { + (Token::While, pos) => { + ensure_not_statement_expr(input, "a boolean")?; + let expr = self + .parse_expr(input, state, lib, settings.level_up())? + .ensure_bool_expr()?; + ensure_not_assignment(input)?; + (expr, pos) + } + (Token::Loop, pos) => (Expr::Unit(Position::NONE), pos), + token => unreachable!("Token::While or Token::Loop expected but gets {:?}", token), + }; + settings.pos = token_pos; + settings.is_breakable = true; + + let body = self.parse_block(input, state, lib, settings.level_up())?; + + Ok(Stmt::While((guard, body.into()).into(), settings.pos)) + } + + /// Parse a do loop. + fn parse_do( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + // do ... + let mut settings = settings; + settings.pos = eat_token(input, Token::Do); + + // do { body } [while|until] guard + settings.is_breakable = true; + let body = self.parse_block(input, state, lib, settings.level_up())?; + + let negated = match input.next().expect(NEVER_ENDS) { + (Token::While, ..) => ASTFlags::NONE, + (Token::Until, ..) => ASTFlags::NEGATED, + (.., pos) => { return Err( - PERR::ModuleUndefined(namespace[0].name.to_string()).into_err(namespace[0].pos) + PERR::MissingToken(Token::While.into(), "for the do statement".into()) + .into_err(pos), + ) + } + }; + + settings.is_breakable = false; + + ensure_not_statement_expr(input, "a boolean")?; + let guard = self + .parse_expr(input, state, lib, settings.level_up())? + .ensure_bool_expr()?; + ensure_not_assignment(input)?; + + Ok(Stmt::Do((guard, body.into()).into(), negated, settings.pos)) + } + + /// Parse a for loop. + fn parse_for( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + // for ... + let mut settings = settings; + settings.pos = eat_token(input, Token::For); + + // for name ... + let (name, name_pos, counter_name, counter_pos) = if match_token(input, Token::LeftParen).0 + { + // ( name, counter ) + let (name, name_pos) = parse_var_name(input)?; + let (has_comma, pos) = match_token(input, Token::Comma); + if !has_comma { + return Err(PERR::MissingToken( + Token::Comma.into(), + "after the iteration variable name".into(), + ) + .into_err(pos)); + } + let (counter_name, counter_pos) = parse_var_name(input)?; + + if counter_name == name { + return Err( + PERR::DuplicatedVariable(counter_name.to_string()).into_err(counter_pos) ); } - namespace.set_index(index); + let (has_close_paren, pos) = match_token(input, Token::RightParen); + if !has_close_paren { + return Err(PERR::MissingToken( + Token::RightParen.into(), + "to close the iteration variable".into(), + ) + .into_err(pos)); + } + (name, name_pos, Some(counter_name), Some(counter_pos)) + } else { + // name + let (name, name_pos) = parse_var_name(input)?; + (name, name_pos, None, None) + }; + + // for name in ... + match input.next().expect(NEVER_ENDS) { + (Token::In, ..) => (), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (.., pos) => { + return Err(PERR::MissingToken( + Token::In.into(), + "after the iteration variable".into(), + ) + .into_err(pos)) + } } + + // for name in expr { body } + ensure_not_statement_expr(input, "a boolean")?; + let expr = self + .parse_expr(input, state, lib, settings.level_up())? + .ensure_iterable()?; + + let prev_stack_len = state.stack.len(); + + let counter_var = counter_name.map(|name| { + let name = state.get_identifier("", name); + let pos = counter_pos.expect("`Some`"); + state.stack.push(name.clone(), ()); + Ident { name, pos } + }); + + let loop_var = state.get_identifier("", name); + state.stack.push(loop_var.clone(), ()); + let loop_var = Ident { + name: loop_var, + pos: name_pos, + }; + + settings.is_breakable = true; + let body = self.parse_block(input, state, lib, settings.level_up())?; + + state.stack.rewind(prev_stack_len); + + Ok(Stmt::For( + Box::new((loop_var, counter_var, expr, body.into())), + settings.pos, + )) } - // Make sure identifiers are valid - Ok(lhs) -} + /// Parse a variable definition statement. + fn parse_let( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + access: AccessMode, + is_export: bool, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; -/// Parse a potential unary operator. -fn parse_unary( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; + // let/const... (specified in `var_type`) + let mut settings = settings; + settings.pos = input.next().expect(NEVER_ENDS).1; - let (token, token_pos) = input.peek().expect(NEVER_ENDS); + // let name ... + let (name, pos) = parse_var_name(input)?; - let mut settings = settings; - settings.pos = *token_pos; + if !self.allow_shadowing() && state.stack.iter().any(|(v, ..)| v == &name) { + return Err(PERR::VariableExists(name.to_string()).into_err(pos)); + } - match token { - // -expr - Token::Minus | Token::UnaryMinus => { - let token = token.clone(); - let pos = eat_token(input, token); + if let Some(ref filter) = self.def_var_filter { + let will_shadow = state.stack.iter().any(|(v, ..)| v == &name); + let level = settings.level; + let is_const = access == AccessMode::ReadOnly; + let info = VarDefInfo { + name: &name, + is_const, + nesting_level: level, + will_shadow, + }; + let context = EvalContext { + engine: self, + scope: &mut state.stack, + global: &mut GlobalRuntimeState::new(self), + state: &mut EvalState::new(), + lib: &[], + this_ptr: &mut None, + level, + }; - match parse_unary(input, state, lib, settings.level_up())? { - // Negative integer - Expr::IntegerConstant(num, ..) => num - .checked_neg() - .map(|i| Expr::IntegerConstant(i, pos)) - .or_else(|| { - #[cfg(not(feature = "no_float"))] - return Some(Expr::FloatConstant((-(num as crate::FLOAT)).into(), pos)); - #[cfg(feature = "no_float")] - return None; - }) - .ok_or_else(|| LexError::MalformedNumber(format!("-{}", num)).into_err(pos)), - - // Negative float - #[cfg(not(feature = "no_float"))] - Expr::FloatConstant(x, ..) => Ok(Expr::FloatConstant((-(*x)).into(), pos)), - - // Call negative function - expr => { - let mut args = StaticVec::new_const(); - args.push(expr); - args.shrink_to_fit(); - - Ok(FnCallExpr { - name: state.get_identifier("", "-"), - hashes: FnCallHashes::from_native(calc_fn_hash("-", 1)), - args, - pos, - ..Default::default() - } - .into_fn_call_expr(pos)) - } + match filter(false, info, &context) { + Ok(true) => (), + Ok(false) => return Err(PERR::ForbiddenVariable(name.to_string()).into_err(pos)), + Err(err) => match *err { + EvalAltResult::ErrorParsing(perr, pos) => return Err(perr.into_err(pos)), + _ => return Err(PERR::ForbiddenVariable(name.to_string()).into_err(pos)), + }, } } - // +expr - Token::Plus | Token::UnaryPlus => { - let token = token.clone(); - let pos = eat_token(input, token); - match parse_unary(input, state, lib, settings.level_up())? { - expr @ Expr::IntegerConstant(..) => Ok(expr), - #[cfg(not(feature = "no_float"))] - expr @ Expr::FloatConstant(..) => Ok(expr), + let name = state.get_identifier("", name); - // Call plus function - expr => { - let mut args = StaticVec::new_const(); - args.push(expr); - args.shrink_to_fit(); + // let name = ... + let expr = if match_token(input, Token::Equals).0 { + // let name = expr + self.parse_expr(input, state, lib, settings.level_up())? + } else { + Expr::Unit(Position::NONE) + }; - Ok(FnCallExpr { - name: state.get_identifier("", "+"), - hashes: FnCallHashes::from_native(calc_fn_hash("+", 1)), - args, - pos, - ..Default::default() - } - .into_fn_call_expr(pos)) - } - } - } - // !expr - Token::Bang => { - let pos = eat_token(input, Token::Bang); - let mut args = StaticVec::new_const(); - args.push(parse_unary(input, state, lib, settings.level_up())?); - args.shrink_to_fit(); + let export = if is_export { + ASTFlags::EXPORTED + } else { + ASTFlags::NONE + }; - Ok(FnCallExpr { - name: state.get_identifier("", "!"), - hashes: FnCallHashes::from_native(calc_fn_hash("!", 1)), - args, - pos, - ..Default::default() - } - .into_fn_call_expr(pos)) - } - // - Token::EOF => Err(PERR::UnexpectedEOF.into_err(settings.pos)), - // All other tokens - _ => parse_primary(input, state, lib, settings.level_up()), - } -} - -/// Make an assignment statement. -fn make_assignment_stmt( - op: Option, - state: &mut ParseState, - lhs: Expr, - rhs: Expr, - op_pos: Position, -) -> ParseResult { - #[must_use] - fn check_lvalue(expr: &Expr, parent_is_dot: bool) -> Option { - match expr { - Expr::Index(x, term, ..) | Expr::Dot(x, term, ..) if parent_is_dot => match x.lhs { - Expr::Property(..) if !term => check_lvalue(&x.rhs, matches!(expr, Expr::Dot(..))), - Expr::Property(..) => None, - // Anything other than a property after dotting (e.g. a method call) is not an l-value - ref e => Some(e.position()), - }, - Expr::Index(x, term, ..) | Expr::Dot(x, term, ..) => match x.lhs { - Expr::Property(..) => unreachable!("unexpected Expr::Property in indexing"), - _ if !term => check_lvalue(&x.rhs, matches!(expr, Expr::Dot(..))), - _ => None, - }, - Expr::Property(..) if parent_is_dot => None, - Expr::Property(..) => unreachable!("unexpected Expr::Property in indexing"), - e if parent_is_dot => Some(e.position()), - _ => None, - } - } - - let op_info = op.map(OpAssignment::new_from_token); - - match lhs { - // const_expr = rhs - ref expr if expr.is_constant() => { - Err(PERR::AssignmentToConstant("".into()).into_err(lhs.start_position())) - } - // var (non-indexed) = rhs - Expr::Variable(None, _, ref x) if x.0.is_none() => Ok(Stmt::Assignment( - (op_info, (lhs, rhs).into()).into(), - op_pos, - )), - // var (indexed) = rhs - Expr::Variable(i, var_pos, ref x) => { - let (index, _, name) = x.as_ref(); - let index = i.map_or_else( - || index.expect("either long or short index is `None`").get(), - |n| n.get() as usize, - ); - match state - .stack - .get_mut_by_index(state.stack.len() - index) - .access_mode() - { - AccessMode::ReadWrite => Ok(Stmt::Assignment( - (op_info, (lhs, rhs).into()).into(), - op_pos, - )), - // Constant values cannot be assigned to - AccessMode::ReadOnly => { - Err(PERR::AssignmentToConstant(name.to_string()).into_err(var_pos)) - } - } - } - // xxx[???]... = rhs, xxx.prop... = rhs - Expr::Index(ref x, term, ..) | Expr::Dot(ref x, term, ..) => { - let valid_lvalue = if term { + let (existing, hit_barrier) = state.find_var(&name); + let existing = if !hit_barrier && existing > 0 { + let offset = state.stack.len() - existing; + if offset < state.block_stack_len { + // Defined in parent block None } else { - check_lvalue(&x.rhs, matches!(lhs, Expr::Dot(..))) - }; + Some(offset) + } + } else { + None + }; - match valid_lvalue { - None => { - match x.lhs { - // var[???] = rhs, var.??? = rhs - Expr::Variable(..) => Ok(Stmt::Assignment( - (op_info, (lhs, rhs).into()).into(), - op_pos, - )), - // expr[???] = rhs, expr.??? = rhs - ref expr => { - Err(PERR::AssignmentToInvalidLHS("".to_string()) - .into_err(expr.position())) + let idx = if let Some(n) = existing { + state.stack.get_mut_by_index(n).set_access_mode(access); + Some(NonZeroUsize::new(state.stack.len() - n).unwrap()) + } else { + state.stack.push_entry(name.as_str(), access, Dynamic::UNIT); + None + }; + + let var_def = (Ident { name, pos }, expr, idx).into(); + + Ok(match access { + // let name = expr + AccessMode::ReadWrite => Stmt::Var(var_def, export, settings.pos), + // const name = { expr:constant } + AccessMode::ReadOnly => Stmt::Var(var_def, ASTFlags::CONSTANT | export, settings.pos), + }) + } + + /// Parse an import statement. + #[cfg(not(feature = "no_module"))] + fn parse_import( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + // import ... + let mut settings = settings; + settings.pos = eat_token(input, Token::Import); + + // import expr ... + let expr = self.parse_expr(input, state, lib, settings.level_up())?; + + // import expr as ... + if !match_token(input, Token::As).0 { + return Ok(Stmt::Import((expr, None).into(), settings.pos)); + } + + // import expr as name ... + let (name, pos) = parse_var_name(input)?; + let name = state.get_identifier("", name); + state.imports.push(name.clone()); + + Ok(Stmt::Import( + (expr, Some(Ident { name, pos })).into(), + settings.pos, + )) + } + + /// Parse an export statement. + #[cfg(not(feature = "no_module"))] + fn parse_export( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + let mut settings = settings; + settings.pos = eat_token(input, Token::Export); + + match input.peek().expect(NEVER_ENDS) { + (Token::Let, pos) => { + let pos = *pos; + let mut stmt = + self.parse_let(input, state, lib, AccessMode::ReadWrite, true, settings)?; + stmt.set_position(pos); + return Ok(stmt); + } + (Token::Const, pos) => { + let pos = *pos; + let mut stmt = + self.parse_let(input, state, lib, AccessMode::ReadOnly, true, settings)?; + stmt.set_position(pos); + return Ok(stmt); + } + _ => (), + } + + let (id, id_pos) = parse_var_name(input)?; + + let (alias, alias_pos) = if match_token(input, Token::As).0 { + let (name, pos) = parse_var_name(input)?; + (Some(name), pos) + } else { + (None, Position::NONE) + }; + + let export = ( + Ident { + name: state.get_identifier("", id), + pos: id_pos, + }, + Ident { + name: state.get_identifier("", alias.as_ref().map_or("", <_>::as_ref)), + pos: alias_pos, + }, + ); + + Ok(Stmt::Export(export.into(), settings.pos)) + } + + /// Parse a statement block. + fn parse_block( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + // Must start with { + let mut settings = settings; + settings.pos = match input.next().expect(NEVER_ENDS) { + (Token::LeftBrace, pos) => pos, + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (.., pos) => { + return Err(PERR::MissingToken( + Token::LeftBrace.into(), + "to start a statement block".into(), + ) + .into_err(pos)) + } + }; + + let mut statements = StaticVec::new(); + + let prev_entry_stack_len = state.block_stack_len; + state.block_stack_len = state.stack.len(); + + #[cfg(not(feature = "no_module"))] + let orig_imports_len = state.imports.len(); + + let end_pos = loop { + // Terminated? + match input.peek().expect(NEVER_ENDS) { + (Token::RightBrace, ..) => break eat_token(input, Token::RightBrace), + (Token::EOF, pos) => { + return Err(PERR::MissingToken( + Token::RightBrace.into(), + "to terminate this block".into(), + ) + .into_err(*pos)); + } + _ => (), + } + + // Parse statements inside the block + settings.is_global = false; + + let stmt = self.parse_stmt(input, state, lib, settings.level_up())?; + + if stmt.is_noop() { + continue; + } + + // See if it needs a terminating semicolon + let need_semicolon = !stmt.is_self_terminated(); + + statements.push(stmt); + + match input.peek().expect(NEVER_ENDS) { + // { ... stmt } + (Token::RightBrace, ..) => break eat_token(input, Token::RightBrace), + // { ... stmt; + (Token::SemiColon, ..) if need_semicolon => { + eat_token(input, Token::SemiColon); + } + // { ... { stmt } ; + (Token::SemiColon, ..) if !need_semicolon => { + eat_token(input, Token::SemiColon); + } + // { ... { stmt } ??? + _ if !need_semicolon => (), + // { ... stmt + (Token::LexError(err), err_pos) => return Err(err.clone().into_err(*err_pos)), + // { ... stmt ??? + (.., pos) => { + // Semicolons are not optional between statements + return Err(PERR::MissingToken( + Token::SemiColon.into(), + "to terminate this statement".into(), + ) + .into_err(*pos)); + } + } + }; + + state.stack.rewind(state.block_stack_len); + state.block_stack_len = prev_entry_stack_len; + + #[cfg(not(feature = "no_module"))] + state.imports.truncate(orig_imports_len); + + Ok((statements, settings.pos, end_pos).into()) + } + + /// Parse an expression as a statement. + fn parse_expr_stmt( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + let mut settings = settings; + settings.pos = input.peek().expect(NEVER_ENDS).1; + + let expr = self.parse_expr(input, state, lib, settings.level_up())?; + let stmt = self.parse_op_assignment_stmt(input, state, lib, expr, settings.level_up())?; + Ok(stmt) + } + + /// Parse a single statement. + fn parse_stmt( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + use AccessMode::{ReadOnly, ReadWrite}; + + let mut settings = settings; + + #[cfg(not(feature = "no_function"))] + #[cfg(feature = "metadata")] + let comments = { + let mut comments = StaticVec::::new(); + let mut comments_pos = Position::NONE; + + // Handle doc-comments. + while let (Token::Comment(ref comment), pos) = input.peek().expect(NEVER_ENDS) { + if comments_pos.is_none() { + comments_pos = *pos; + } + + if !crate::tokenizer::is_doc_comment(comment) { + unreachable!("doc-comment expected but gets {:?}", comment); + } + + if !settings.is_global { + return Err(PERR::WrongDocComment.into_err(comments_pos)); + } + + match input.next().expect(NEVER_ENDS).0 { + Token::Comment(comment) => { + comments.push(comment); + + match input.peek().expect(NEVER_ENDS) { + (Token::Fn, ..) | (Token::Private, ..) => break, + (Token::Comment(..), ..) => (), + _ => return Err(PERR::WrongDocComment.into_err(comments_pos)), } } - } - Some(err_pos) => { - Err(PERR::AssignmentToInvalidLHS("".to_string()).into_err(err_pos)) + token => unreachable!("Token::Comment expected but gets {:?}", token), } } - } - // ??? && ??? = rhs, ??? || ??? = rhs - Expr::And(..) | Expr::Or(..) => Err(LexError::ImproperSymbol( - "=".to_string(), - "Possibly a typo of '=='?".to_string(), - ) - .into_err(op_pos)), - // expr = rhs - _ => Err(PERR::AssignmentToInvalidLHS("".to_string()).into_err(lhs.position())), - } -} -/// Parse an operator-assignment expression (if any). -fn parse_op_assignment_stmt( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - lhs: Expr, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - let (op, pos) = match input.peek().expect(NEVER_ENDS) { - // var = ... - (Token::Equals, ..) => (None, eat_token(input, Token::Equals)), - // var op= ... - (token, ..) if token.is_op_assignment() => input - .next() - .map(|(op, pos)| (Some(op), pos)) - .expect(NEVER_ENDS), - // Not op-assignment - _ => return Ok(Stmt::Expr(lhs.into())), - }; - - let mut settings = settings; - settings.pos = pos; - - let rhs = parse_expr(input, state, lib, settings.level_up())?; - make_assignment_stmt(op, state, lhs, rhs, pos) -} - -/// Make a dot expression. -#[cfg(not(feature = "no_object"))] -fn make_dot_expr( - state: &mut ParseState, - lhs: Expr, - terminate_chaining: bool, - rhs: Expr, - op_pos: Position, -) -> ParseResult { - match (lhs, rhs) { - // lhs[idx_expr].rhs - (Expr::Index(mut x, term, pos), rhs) => { - x.rhs = make_dot_expr(state, x.rhs, term || terminate_chaining, rhs, op_pos)?; - Ok(Expr::Index(x, false, pos)) - } - // lhs.module::id - syntax error - #[cfg(not(feature = "no_module"))] - (.., Expr::Variable(.., x)) if x.1.is_some() => { - Err(PERR::PropertyExpected.into_err(x.1.expect("`Some`").0.position())) - } - // lhs.id - (lhs, var_expr @ Expr::Variable(..)) => { - let rhs = var_expr.into_property(state); - Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), false, op_pos)) - } - // lhs.prop - (lhs, prop @ Expr::Property(..)) => Ok(Expr::Dot( - BinaryExpr { lhs, rhs: prop }.into(), - false, - op_pos, - )), - // lhs.nnn::func(...) - syntax error - #[cfg(not(feature = "no_module"))] - (.., Expr::FnCall(func, ..)) if func.is_qualified() => { - Err(PERR::PropertyExpected.into_err(func.namespace.expect("`Some`").position())) - } - // lhs.Fn() or lhs.eval() - (.., Expr::FnCall(func, func_pos)) - if func.args.is_empty() - && [crate::engine::KEYWORD_FN_PTR, crate::engine::KEYWORD_EVAL] - .contains(&func.name.as_ref()) => - { - let err_msg = format!( - "'{}' should not be called in method style. Try {}(...);", - func.name, func.name - ); - Err(LexError::ImproperSymbol(func.name.to_string(), err_msg).into_err(func_pos)) - } - // lhs.func!(...) - (.., Expr::FnCall(func, func_pos)) if func.capture_parent_scope => { - Err(PERR::MalformedCapture( - "method-call style does not support running within the caller's scope".into(), - ) - .into_err(func_pos)) - } - // lhs.func(...) - (lhs, Expr::FnCall(mut func, func_pos)) => { - // Recalculate hash - func.hashes = FnCallHashes::from_all( - #[cfg(not(feature = "no_function"))] - calc_fn_hash(&func.name, func.args.len()), - calc_fn_hash(&func.name, func.args.len() + 1), - ); - - let rhs = Expr::FnCall(func, func_pos); - Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), false, op_pos)) - } - // lhs.dot_lhs.dot_rhs or lhs.dot_lhs[idx_rhs] - (lhs, rhs @ Expr::Dot(..)) | (lhs, rhs @ Expr::Index(..)) => { - let (x, term, pos, is_dot) = match rhs { - Expr::Dot(x, term, pos) => (x, term, pos, true), - Expr::Index(x, term, pos) => (x, term, pos, false), - expr => unreachable!("Expr::Dot or Expr::Index expected but gets {:?}", expr), - }; - - match x.lhs { - // lhs.module::id.dot_rhs or lhs.module::id[idx_rhs] - syntax error - #[cfg(not(feature = "no_module"))] - Expr::Variable(.., x) if x.1.is_some() => { - Err(PERR::PropertyExpected.into_err(x.1.expect("`Some`").0.position())) - } - // lhs.module::func().dot_rhs or lhs.module::func()[idx_rhs] - syntax error - #[cfg(not(feature = "no_module"))] - Expr::FnCall(func, ..) if func.is_qualified() => { - Err(PERR::PropertyExpected.into_err(func.namespace.expect("`Some`").position())) - } - // lhs.id.dot_rhs or lhs.id[idx_rhs] - Expr::Variable(..) | Expr::Property(..) => { - let new_lhs = BinaryExpr { - lhs: x.lhs.into_property(state), - rhs: x.rhs, - } - .into(); - - let rhs = if is_dot { - Expr::Dot(new_lhs, term, pos) - } else { - Expr::Index(new_lhs, term, pos) - }; - Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), false, op_pos)) - } - // lhs.func().dot_rhs or lhs.func()[idx_rhs] - Expr::FnCall(mut func, func_pos) => { - // Recalculate hash - func.hashes = FnCallHashes::from_all( - #[cfg(not(feature = "no_function"))] - calc_fn_hash(&func.name, func.args.len()), - calc_fn_hash(&func.name, func.args.len() + 1), - ); - - let new_lhs = BinaryExpr { - lhs: Expr::FnCall(func, func_pos), - rhs: x.rhs, - } - .into(); - - let rhs = if is_dot { - Expr::Dot(new_lhs, term, pos) - } else { - Expr::Index(new_lhs, term, pos) - }; - Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), false, op_pos)) - } - expr => unreachable!("invalid dot expression: {:?}", expr), - } - } - // lhs.rhs - (.., rhs) => Err(PERR::PropertyExpected.into_err(rhs.start_position())), - } -} - -/// Parse a binary expression (if any). -fn parse_binary_op( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - parent_precedence: Option, - lhs: Expr, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - let mut settings = settings; - settings.pos = lhs.position(); - - let mut root = lhs; - - loop { - let (current_op, current_pos) = input.peek().expect(NEVER_ENDS); - let precedence = match current_op { - Token::Custom(c) => state - .engine - .custom_keywords - .get(c.as_ref()) - .cloned() - .ok_or_else(|| PERR::Reserved(c.to_string()).into_err(*current_pos))?, - Token::Reserved(c) if !is_valid_identifier(c.chars()) => { - return Err(PERR::UnknownOperator(c.to_string()).into_err(*current_pos)) - } - _ => current_op.precedence(), - }; - let bind_right = current_op.is_bind_right(); - - // Bind left to the parent lhs expression if precedence is higher - // If same precedence, then check if the operator binds right - if precedence < parent_precedence || (precedence == parent_precedence && !bind_right) { - return Ok(root); - } - - let (op_token, pos) = input.next().expect(NEVER_ENDS); - - let rhs = parse_unary(input, state, lib, settings)?; - - let (next_op, next_pos) = input.peek().expect(NEVER_ENDS); - let next_precedence = match next_op { - Token::Custom(c) => state - .engine - .custom_keywords - .get(c.as_ref()) - .cloned() - .ok_or_else(|| PERR::Reserved(c.to_string()).into_err(*next_pos))?, - Token::Reserved(c) if !is_valid_identifier(c.chars()) => { - return Err(PERR::UnknownOperator(c.to_string()).into_err(*next_pos)) - } - _ => next_op.precedence(), + comments }; - // Bind to right if the next operator has higher precedence - // If same precedence, then check if the operator binds right - let rhs = if (precedence == next_precedence && bind_right) || precedence < next_precedence { - parse_binary_op(input, state, lib, precedence, rhs, settings)? - } else { - // Otherwise bind to left (even if next operator has the same precedence) - rhs + let (token, token_pos) = match input.peek().expect(NEVER_ENDS) { + (Token::EOF, pos) => return Ok(Stmt::Noop(*pos)), + (x, pos) => (x, *pos), }; - - settings = settings.level_up(); - settings.pos = pos; + settings.pos = token_pos; #[cfg(not(feature = "unchecked"))] settings.ensure_level_within_max_limit(state.max_expr_depth)?; - let op = op_token.syntax(); - let hash = calc_fn_hash(&op, 2); - - let op_base = FnCallExpr { - name: state.get_identifier("", op), - hashes: FnCallHashes::from_native(hash), - pos, - ..Default::default() - }; - - let mut args = StaticVec::new_const(); - args.push(root); - args.push(rhs); - args.shrink_to_fit(); - - root = match op_token { - // '!=' defaults to true when passed invalid operands - Token::NotEqualsTo => FnCallExpr { args, ..op_base }.into_fn_call_expr(pos), - - // Comparison operators default to false when passed invalid operands - Token::EqualsTo - | Token::LessThan - | Token::LessThanEqualsTo - | Token::GreaterThan - | Token::GreaterThanEqualsTo => { - let pos = args[0].start_position(); - FnCallExpr { args, ..op_base }.into_fn_call_expr(pos) - } - - Token::Or => { - let rhs = args.pop().unwrap(); - let current_lhs = args.pop().unwrap(); - Expr::Or( - BinaryExpr { - lhs: current_lhs.ensure_bool_expr()?, - rhs: rhs.ensure_bool_expr()?, - } - .into(), - pos, - ) - } - Token::And => { - let rhs = args.pop().unwrap(); - let current_lhs = args.pop().unwrap(); - Expr::And( - BinaryExpr { - lhs: current_lhs.ensure_bool_expr()?, - rhs: rhs.ensure_bool_expr()?, - } - .into(), - pos, - ) - } - Token::In => { - // Swap the arguments - let current_lhs = args.remove(0); - let pos = current_lhs.start_position(); - args.push(current_lhs); - args.shrink_to_fit(); - - // Convert into a call to `contains` - FnCallExpr { - hashes: calc_fn_hash(OP_CONTAINS, 2).into(), - args, - name: state.get_identifier("", OP_CONTAINS), - ..op_base - } - .into_fn_call_expr(pos) - } - - Token::Custom(s) - if state - .engine - .custom_keywords - .get(s.as_ref()) - .map_or(false, Option::is_some) => - { - let hash = calc_fn_hash(&s, 2); - let pos = args[0].start_position(); - - FnCallExpr { - hashes: if is_valid_function_name(&s) { - hash.into() - } else { - FnCallHashes::from_native(hash) - }, - args, - ..op_base - } - .into_fn_call_expr(pos) - } - - _ => { - let pos = args[0].start_position(); - FnCallExpr { args, ..op_base }.into_fn_call_expr(pos) - } - }; - } -} - -/// Parse a custom syntax. -fn parse_custom_syntax( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, - key: impl Into, - syntax: &CustomSyntax, - pos: Position, -) -> ParseResult { - let mut settings = settings; - let mut inputs = StaticVec::::new(); - let mut segments = StaticVec::new_const(); - let mut tokens = StaticVec::new_const(); - - // Adjust the variables stack - if syntax.scope_may_be_changed { - // Add a barrier variable to the stack so earlier variables will not be matched. - // Variable searches stop at the first barrier. - let marker = state.get_identifier("", SCOPE_SEARCH_BARRIER_MARKER); - state.stack.push(marker, ()); - } - - let parse_func = syntax.parse.as_ref(); - let mut required_token: ImmutableString = key.into(); - - tokens.push(required_token.clone().into()); - segments.push(required_token.clone()); - - loop { - let (fwd_token, fwd_pos) = input.peek().expect(NEVER_ENDS); - settings.pos = *fwd_pos; - let settings = settings.level_up(); - - required_token = match parse_func(&segments, &*fwd_token.syntax()) { - Ok(Some(seg)) - if seg.starts_with(CUSTOM_SYNTAX_MARKER_SYNTAX_VARIANT) - && seg.len() > CUSTOM_SYNTAX_MARKER_SYNTAX_VARIANT.len() => - { - inputs.push(Expr::StringConstant( - state.get_interned_string("", seg), - pos, - )); - break; - } - Ok(Some(seg)) => seg, - Ok(None) => break, - Err(err) => return Err(err.0.into_err(settings.pos)), - }; - - match required_token.as_str() { - CUSTOM_SYNTAX_MARKER_IDENT => { - let (name, pos) = parse_var_name(input)?; - let name = state.get_identifier("", name); - segments.push(name.clone().into()); - tokens.push(state.get_identifier("", CUSTOM_SYNTAX_MARKER_IDENT)); - inputs.push(Expr::Variable( - None, - pos, - ( - None, - #[cfg(not(feature = "no_module"))] - None, - #[cfg(feature = "no_module")] - (), - name, - ) - .into(), - )); - } - CUSTOM_SYNTAX_MARKER_SYMBOL => { - let (symbol, pos) = parse_symbol(input)?; - let symbol = state.get_interned_string("", symbol); - segments.push(symbol.clone()); - tokens.push(state.get_identifier("", CUSTOM_SYNTAX_MARKER_SYMBOL)); - inputs.push(Expr::StringConstant(symbol, pos)); - } - CUSTOM_SYNTAX_MARKER_EXPR => { - inputs.push(parse_expr(input, state, lib, settings)?); - let keyword = state.get_identifier("", CUSTOM_SYNTAX_MARKER_EXPR); - segments.push(keyword.clone().into()); - tokens.push(keyword); - } - CUSTOM_SYNTAX_MARKER_BLOCK => match parse_block(input, state, lib, settings)? { - block @ Stmt::Block(..) => { - inputs.push(Expr::Stmt(Box::new(block.into()))); - let keyword = state.get_identifier("", CUSTOM_SYNTAX_MARKER_BLOCK); - segments.push(keyword.clone().into()); - tokens.push(keyword); - } - stmt => unreachable!("Stmt::Block expected but gets {:?}", stmt), - }, - CUSTOM_SYNTAX_MARKER_BOOL => match input.next().expect(NEVER_ENDS) { - (b @ Token::True, pos) | (b @ Token::False, pos) => { - inputs.push(Expr::BoolConstant(b == Token::True, pos)); - segments.push(state.get_interned_string("", b.literal_syntax())); - tokens.push(state.get_identifier("", CUSTOM_SYNTAX_MARKER_BOOL)); - } - (.., pos) => { - return Err( - PERR::MissingSymbol("Expecting 'true' or 'false'".to_string()) - .into_err(pos), - ) - } - }, - CUSTOM_SYNTAX_MARKER_INT => match input.next().expect(NEVER_ENDS) { - (Token::IntegerConstant(i), pos) => { - inputs.push(Expr::IntegerConstant(i, pos)); - segments.push(i.to_string().into()); - tokens.push(state.get_identifier("", CUSTOM_SYNTAX_MARKER_INT)); - } - (.., pos) => { - return Err( - PERR::MissingSymbol("Expecting an integer number".to_string()) - .into_err(pos), - ) - } - }, - #[cfg(not(feature = "no_float"))] - CUSTOM_SYNTAX_MARKER_FLOAT => match input.next().expect(NEVER_ENDS) { - (Token::FloatConstant(f), pos) => { - inputs.push(Expr::FloatConstant(f, pos)); - segments.push(f.to_string().into()); - tokens.push(state.get_identifier("", CUSTOM_SYNTAX_MARKER_FLOAT)); - } - (.., pos) => { - return Err(PERR::MissingSymbol( - "Expecting a floating-point number".to_string(), - ) - .into_err(pos)) - } - }, - CUSTOM_SYNTAX_MARKER_STRING => match input.next().expect(NEVER_ENDS) { - (Token::StringConstant(s), pos) => { - let s = state.get_interned_string("", s); - inputs.push(Expr::StringConstant(s.clone(), pos)); - segments.push(s); - tokens.push(state.get_identifier("", CUSTOM_SYNTAX_MARKER_STRING)); - } - (.., pos) => { - return Err(PERR::MissingSymbol("Expecting a string".to_string()).into_err(pos)) - } - }, - s => match input.next().expect(NEVER_ENDS) { - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (t, ..) if &*t.syntax() == s => { - segments.push(required_token.clone()); - tokens.push(required_token.clone().into()); - } - (.., pos) => { - return Err(PERR::MissingToken( - s.to_string(), - format!("for '{}' expression", segments[0]), - ) - .into_err(pos)) - } - }, - } - } - - inputs.shrink_to_fit(); - tokens.shrink_to_fit(); - - const KEYWORD_SEMICOLON: &str = Token::SemiColon.literal_syntax(); - const KEYWORD_CLOSE_BRACE: &str = Token::RightBrace.literal_syntax(); - - let self_terminated = match required_token.as_str() { - // It is self-terminating if the last symbol is a block - CUSTOM_SYNTAX_MARKER_BLOCK => true, - // If the last symbol is `;` or `}`, it is self-terminating - KEYWORD_SEMICOLON | KEYWORD_CLOSE_BRACE => true, - _ => false, - }; - - Ok(Expr::Custom( - CustomExpr { - inputs, - tokens, - scope_may_be_changed: syntax.scope_may_be_changed, - self_terminated, - } - .into(), - pos, - )) -} - -/// Parse an expression. -fn parse_expr( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - let mut settings = settings; - settings.pos = input.peek().expect(NEVER_ENDS).1; - - // Parse expression normally. - let precedence = Precedence::new(1); - let lhs = parse_unary(input, state, lib, settings.level_up())?; - parse_binary_op(input, state, lib, precedence, lhs, settings.level_up()) -} - -/// Parse an if statement. -fn parse_if( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - // if ... - let mut settings = settings; - settings.pos = eat_token(input, Token::If); - - // if guard { if_body } - ensure_not_statement_expr(input, "a boolean")?; - let guard = parse_expr(input, state, lib, settings.level_up())?.ensure_bool_expr()?; - ensure_not_assignment(input)?; - let if_body = parse_block(input, state, lib, settings.level_up())?; - - // if guard { if_body } else ... - let else_body = if match_token(input, Token::Else).0 { - if let (Token::If, ..) = input.peek().expect(NEVER_ENDS) { - // if guard { if_body } else if ... - parse_if(input, state, lib, settings.level_up())? - } else { - // if guard { if_body } else { else-body } - parse_block(input, state, lib, settings.level_up())? - } - } else { - Stmt::Noop(Position::NONE) - }; - - Ok(Stmt::If( - (guard, if_body.into(), else_body.into()).into(), - settings.pos, - )) -} - -/// Parse a while loop. -fn parse_while_loop( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - let mut settings = settings; - - // while|loops ... - let (guard, token_pos) = match input.next().expect(NEVER_ENDS) { - (Token::While, pos) => { - ensure_not_statement_expr(input, "a boolean")?; - let expr = parse_expr(input, state, lib, settings.level_up())?.ensure_bool_expr()?; - ensure_not_assignment(input)?; - (expr, pos) - } - (Token::Loop, pos) => (Expr::Unit(Position::NONE), pos), - token => unreachable!("Token::While or Token::Loop expected but gets {:?}", token), - }; - settings.pos = token_pos; - settings.is_breakable = true; - - let body = parse_block(input, state, lib, settings.level_up())?; - - Ok(Stmt::While((guard, body.into()).into(), settings.pos)) -} - -/// Parse a do loop. -fn parse_do( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - // do ... - let mut settings = settings; - settings.pos = eat_token(input, Token::Do); - - // do { body } [while|until] guard - settings.is_breakable = true; - let body = parse_block(input, state, lib, settings.level_up())?; - - let negated = match input.next().expect(NEVER_ENDS) { - (Token::While, ..) => AST_OPTION_NONE, - (Token::Until, ..) => AST_OPTION_NEGATED, - (.., pos) => { - return Err( - PERR::MissingToken(Token::While.into(), "for the do statement".into()) - .into_err(pos), - ) - } - }; - - settings.is_breakable = false; - - ensure_not_statement_expr(input, "a boolean")?; - let guard = parse_expr(input, state, lib, settings.level_up())?.ensure_bool_expr()?; - ensure_not_assignment(input)?; - - Ok(Stmt::Do((guard, body.into()).into(), negated, settings.pos)) -} - -/// Parse a for loop. -fn parse_for( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - // for ... - let mut settings = settings; - settings.pos = eat_token(input, Token::For); - - // for name ... - let (name, name_pos, counter_name, counter_pos) = if match_token(input, Token::LeftParen).0 { - // ( name, counter ) - let (name, name_pos) = parse_var_name(input)?; - let (has_comma, pos) = match_token(input, Token::Comma); - if !has_comma { - return Err(PERR::MissingToken( - Token::Comma.into(), - "after the iteration variable name".into(), - ) - .into_err(pos)); - } - let (counter_name, counter_pos) = parse_var_name(input)?; - - if counter_name == name { - return Err(PERR::DuplicatedVariable(counter_name.to_string()).into_err(counter_pos)); - } - - let (has_close_paren, pos) = match_token(input, Token::RightParen); - if !has_close_paren { - return Err(PERR::MissingToken( - Token::RightParen.into(), - "to close the iteration variable".into(), - ) - .into_err(pos)); - } - (name, name_pos, Some(counter_name), Some(counter_pos)) - } else { - // name - let (name, name_pos) = parse_var_name(input)?; - (name, name_pos, None, None) - }; - - // for name in ... - match input.next().expect(NEVER_ENDS) { - (Token::In, ..) => (), - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (.., pos) => { - return Err( - PERR::MissingToken(Token::In.into(), "after the iteration variable".into()) - .into_err(pos), - ) - } - } - - // for name in expr { body } - ensure_not_statement_expr(input, "a boolean")?; - let expr = parse_expr(input, state, lib, settings.level_up())?.ensure_iterable()?; - - let prev_stack_len = state.stack.len(); - - let counter_var = counter_name.map(|name| { - let name = state.get_identifier("", name); - let pos = counter_pos.expect("`Some`"); - state.stack.push(name.clone(), ()); - Ident { name, pos } - }); - - let loop_var = state.get_identifier("", name); - state.stack.push(loop_var.clone(), ()); - let loop_var = Ident { - name: loop_var, - pos: name_pos, - }; - - settings.is_breakable = true; - let body = parse_block(input, state, lib, settings.level_up())?; - - state.stack.rewind(prev_stack_len); - - Ok(Stmt::For( - Box::new((loop_var, counter_var, expr, body.into())), - settings.pos, - )) -} - -/// Parse a variable definition statement. -fn parse_let( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - access: AccessMode, - is_export: bool, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - // let/const... (specified in `var_type`) - let mut settings = settings; - settings.pos = input.next().expect(NEVER_ENDS).1; - - // let name ... - let (name, pos) = parse_var_name(input)?; - - if !settings.default_options.allow_shadowing - && state.stack.iter().any(|(v, ..)| v == name.as_ref()) - { - return Err(PERR::VariableExists(name.to_string()).into_err(pos)); - } - - if let Some(ref filter) = state.engine.def_var_filter { - let will_shadow = state.stack.iter().any(|(v, ..)| v == name.as_ref()); - let level = settings.level; - let is_const = access == AccessMode::ReadOnly; - let info = VarDefInfo { - name: &name, - is_const, - nesting_level: level, - will_shadow, - }; - let context = EvalContext { - engine: state.engine, - scope: &mut state.stack, - global: &mut GlobalRuntimeState::new(state.engine), - state: &mut EvalState::new(), - lib: &[], - this_ptr: &mut None, - level, - }; - - match filter(false, info, &context) { - Ok(true) => (), - Ok(false) => return Err(PERR::ForbiddenVariable(name.to_string()).into_err(pos)), - Err(err) => match *err { - EvalAltResult::ErrorParsing(perr, pos) => return Err(perr.into_err(pos)), - _ => return Err(PERR::ForbiddenVariable(name.to_string()).into_err(pos)), - }, - } - } - - let name = state.get_identifier("", name); - - // let name = ... - let expr = if match_token(input, Token::Equals).0 { - // let name = expr - parse_expr(input, state, lib, settings.level_up())? - } else { - Expr::Unit(Position::NONE) - }; - - let export = if is_export { - AST_OPTION_EXPORTED - } else { - AST_OPTION_NONE - }; - - let existing = state.stack.get_index(&name).and_then(|(n, ..)| { - if n < state.block_stack_len { - // Defined in parent block - None - } else { - Some(n) - } - }); - - let idx = if let Some(n) = existing { - state.stack.get_mut_by_index(n).set_access_mode(access); - Some(NonZeroUsize::new(state.stack.len() - n).unwrap()) - } else { - state.stack.push_entry(name.as_str(), access, Dynamic::UNIT); - None - }; - - let var_def = (Ident { name, pos }, expr, idx).into(); - - Ok(match access { - // let name = expr - AccessMode::ReadWrite => Stmt::Var(var_def, export, settings.pos), - // const name = { expr:constant } - AccessMode::ReadOnly => Stmt::Var(var_def, AST_OPTION_CONSTANT + export, settings.pos), - }) -} - -/// Parse an import statement. -#[cfg(not(feature = "no_module"))] -fn parse_import( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - // import ... - let mut settings = settings; - settings.pos = eat_token(input, Token::Import); - - // import expr ... - let expr = parse_expr(input, state, lib, settings.level_up())?; - - // import expr as ... - if !match_token(input, Token::As).0 { - return Ok(Stmt::Import((expr, None).into(), settings.pos)); - } - - // import expr as name ... - let (name, pos) = parse_var_name(input)?; - let name = state.get_identifier("", name); - state.imports.push(name.clone()); - - Ok(Stmt::Import( - (expr, Some(Ident { name, pos })).into(), - settings.pos, - )) -} - -/// Parse an export statement. -#[cfg(not(feature = "no_module"))] -fn parse_export( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - let mut settings = settings; - settings.pos = eat_token(input, Token::Export); - - match input.peek().expect(NEVER_ENDS) { - (Token::Let, pos) => { - let pos = *pos; - let mut stmt = parse_let(input, state, lib, AccessMode::ReadWrite, true, settings)?; - stmt.set_position(pos); - return Ok(stmt); - } - (Token::Const, pos) => { - let pos = *pos; - let mut stmt = parse_let(input, state, lib, AccessMode::ReadOnly, true, settings)?; - stmt.set_position(pos); - return Ok(stmt); - } - _ => (), - } - - let (id, id_pos) = parse_var_name(input)?; - - let (alias, alias_pos) = if match_token(input, Token::As).0 { - let (name, pos) = parse_var_name(input)?; - (Some(name), pos) - } else { - (None, Position::NONE) - }; - - let export = ( - Ident { - name: state.get_identifier("", id), - pos: id_pos, - }, - Ident { - name: state.get_identifier("", alias.as_ref().map_or("", <_>::as_ref)), - pos: alias_pos, - }, - ); - - Ok(Stmt::Export(export.into(), settings.pos)) -} - -/// Parse a statement block. -fn parse_block( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - // Must start with { - let mut settings = settings; - settings.pos = match input.next().expect(NEVER_ENDS) { - (Token::LeftBrace, pos) => pos, - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (.., pos) => { - return Err(PERR::MissingToken( - Token::LeftBrace.into(), - "to start a statement block".into(), - ) - .into_err(pos)) - } - }; - - let mut statements = Vec::with_capacity(8); - - let prev_entry_stack_len = state.block_stack_len; - state.block_stack_len = state.stack.len(); - - #[cfg(not(feature = "no_module"))] - let orig_imports_len = state.imports.len(); - - let end_pos = loop { - // Terminated? - match input.peek().expect(NEVER_ENDS) { - (Token::RightBrace, ..) => break eat_token(input, Token::RightBrace), - (Token::EOF, pos) => { - return Err(PERR::MissingToken( - Token::RightBrace.into(), - "to terminate this block".into(), - ) - .into_err(*pos)); - } - _ => (), - } - - // Parse statements inside the block - settings.is_global = false; - - let stmt = parse_stmt(input, state, lib, settings.level_up())?; - - if stmt.is_noop() { - continue; - } - - // See if it needs a terminating semicolon - let need_semicolon = !stmt.is_self_terminated(); - - statements.push(stmt); - - match input.peek().expect(NEVER_ENDS) { - // { ... stmt } - (Token::RightBrace, ..) => break eat_token(input, Token::RightBrace), - // { ... stmt; - (Token::SemiColon, ..) if need_semicolon => { + match token { + // ; - empty statement + Token::SemiColon => { eat_token(input, Token::SemiColon); - } - // { ... { stmt } ; - (Token::SemiColon, ..) if !need_semicolon => { - eat_token(input, Token::SemiColon); - } - // { ... { stmt } ??? - _ if !need_semicolon => (), - // { ... stmt - (Token::LexError(err), err_pos) => return Err(err.clone().into_err(*err_pos)), - // { ... stmt ??? - (.., pos) => { - // Semicolons are not optional between statements - return Err(PERR::MissingToken( - Token::SemiColon.into(), - "to terminate this statement".into(), - ) - .into_err(*pos)); - } - } - }; - - state.stack.rewind(state.block_stack_len); - state.block_stack_len = prev_entry_stack_len; - - #[cfg(not(feature = "no_module"))] - state.imports.truncate(orig_imports_len); - - Ok((statements, settings.pos, end_pos).into()) -} - -/// Parse an expression as a statement. -fn parse_expr_stmt( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - let mut settings = settings; - settings.pos = input.peek().expect(NEVER_ENDS).1; - - let expr = parse_expr(input, state, lib, settings.level_up())?; - let stmt = parse_op_assignment_stmt(input, state, lib, expr, settings.level_up())?; - Ok(stmt) -} - -/// Parse a single statement. -fn parse_stmt( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - use AccessMode::{ReadOnly, ReadWrite}; - - let mut settings = settings; - - #[cfg(not(feature = "no_function"))] - #[cfg(feature = "metadata")] - let comments = { - let mut comments = Vec::>::new(); - let mut comments_pos = Position::NONE; - - // Handle doc-comments. - while let (Token::Comment(ref comment), pos) = input.peek().expect(NEVER_ENDS) { - if comments_pos.is_none() { - comments_pos = *pos; + Ok(Stmt::Noop(token_pos)) } - if !crate::tokenizer::is_doc_comment(comment) { - unreachable!("doc-comment expected but gets {:?}", comment); - } + // { - statements block + Token::LeftBrace => Ok(self.parse_block(input, state, lib, settings.level_up())?), - if !settings.is_global { - return Err(PERR::WrongDocComment.into_err(comments_pos)); - } + // fn ... + #[cfg(not(feature = "no_function"))] + Token::Fn if !settings.is_global => Err(PERR::WrongFnDefinition.into_err(token_pos)), - match input.next().expect(NEVER_ENDS).0 { - Token::Comment(comment) => { - comments.push(comment); + #[cfg(not(feature = "no_function"))] + Token::Fn | Token::Private => { + let access = if matches!(token, Token::Private) { + eat_token(input, Token::Private); + crate::FnAccess::Private + } else { + crate::FnAccess::Public + }; - match input.peek().expect(NEVER_ENDS) { - (Token::Fn, ..) | (Token::Private, ..) => break, - (Token::Comment(..), ..) => (), - _ => return Err(PERR::WrongDocComment.into_err(comments_pos)), - } - } - token => unreachable!("Token::Comment expected but gets {:?}", token), - } - } + match input.next().expect(NEVER_ENDS) { + (Token::Fn, pos) => { + let mut new_state = ParseState::new(self, state.tokenizer_control.clone()); - comments - }; + #[cfg(not(feature = "unchecked"))] + { + new_state.max_expr_depth = self.max_function_expr_depth(); + } - let (token, token_pos) = match input.peek().expect(NEVER_ENDS) { - (Token::EOF, pos) => return Ok(Stmt::Noop(*pos)), - (x, pos) => (x, *pos), - }; - settings.pos = token_pos; + let new_settings = ParseSettings { + is_global: false, + is_function_scope: true, + #[cfg(not(feature = "no_closure"))] + is_closure_scope: false, + is_breakable: false, + level: 0, + options: LanguageOptions { + strict_var: settings.options.strict_var, + ..self.options + }, + pos, + ..settings + }; - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; + let func = self.parse_fn( + input, + &mut new_state, + lib, + access, + new_settings, + #[cfg(not(feature = "no_function"))] + #[cfg(feature = "metadata")] + comments, + )?; - match token { - // ; - empty statement - Token::SemiColon => { - eat_token(input, Token::SemiColon); - Ok(Stmt::Noop(token_pos)) - } + let hash = calc_fn_hash(&func.name, func.params.len()); - // { - statements block - Token::LeftBrace => Ok(parse_block(input, state, lib, settings.level_up())?), + if !lib.is_empty() && lib.contains_key(&hash) { + return Err(PERR::FnDuplicatedDefinition( + func.name.to_string(), + func.params.len(), + ) + .into_err(pos)); + } - // fn ... - #[cfg(not(feature = "no_function"))] - Token::Fn if !settings.is_global => Err(PERR::WrongFnDefinition.into_err(token_pos)), + lib.insert(hash, func.into()); - #[cfg(not(feature = "no_function"))] - Token::Fn | Token::Private => { - let access = if matches!(token, Token::Private) { - eat_token(input, Token::Private); - crate::FnAccess::Private - } else { - crate::FnAccess::Public - }; - - match input.next().expect(NEVER_ENDS) { - (Token::Fn, pos) => { - let mut new_state = - ParseState::new(state.engine, state.tokenizer_control.clone()); - - #[cfg(not(feature = "unchecked"))] - { - new_state.max_expr_depth = new_state.max_function_expr_depth; + Ok(Stmt::Noop(pos)) } - let new_settings = ParseSettings { - allow_if_expr: settings.default_options.allow_if_expr, - allow_switch_expr: settings.default_options.allow_switch_expr, - allow_stmt_expr: settings.default_options.allow_stmt_expr, - allow_anonymous_fn: settings.default_options.allow_anonymous_fn, - strict_var: settings.strict_var, - is_global: false, - is_function_scope: true, - #[cfg(not(feature = "no_closure"))] - is_closure: false, - is_breakable: false, - level: 0, - pos, - ..settings - }; + (.., pos) => Err(PERR::MissingToken( + Token::Fn.into(), + format!("following '{}'", Token::Private.syntax()), + ) + .into_err(pos)), + } + } - let func = parse_fn( - input, - &mut new_state, - lib, - access, - new_settings, - #[cfg(not(feature = "no_function"))] - #[cfg(feature = "metadata")] - comments, - )?; + Token::If => self.parse_if(input, state, lib, settings.level_up()), + Token::Switch => self.parse_switch(input, state, lib, settings.level_up()), + Token::While | Token::Loop if self.allow_looping() => { + self.parse_while_loop(input, state, lib, settings.level_up()) + } + Token::Do if self.allow_looping() => { + self.parse_do(input, state, lib, settings.level_up()) + } + Token::For if self.allow_looping() => { + self.parse_for(input, state, lib, settings.level_up()) + } - let hash = calc_fn_hash(&func.name, func.params.len()); + Token::Continue if self.allow_looping() && settings.is_breakable => { + let pos = eat_token(input, Token::Continue); + Ok(Stmt::BreakLoop(ASTFlags::NONE, pos)) + } + Token::Break if self.allow_looping() && settings.is_breakable => { + let pos = eat_token(input, Token::Break); + Ok(Stmt::BreakLoop(ASTFlags::BREAK, pos)) + } + Token::Continue | Token::Break if self.allow_looping() => { + Err(PERR::LoopBreak.into_err(token_pos)) + } - if lib.contains_key(&hash) { - return Err(PERR::FnDuplicatedDefinition( - func.name.to_string(), - func.params.len(), - ) - .into_err(pos)); + Token::Return | Token::Throw => { + let (return_type, token_pos) = input + .next() + .map(|(token, pos)| { + let flags = match token { + Token::Return => ASTFlags::NONE, + Token::Throw => ASTFlags::BREAK, + token => unreachable!( + "Token::Return or Token::Throw expected but gets {:?}", + token + ), + }; + (flags, pos) + }) + .expect(NEVER_ENDS); + + match input.peek().expect(NEVER_ENDS) { + // `return`/`throw` at + (Token::EOF, ..) => Ok(Stmt::Return(None, return_type, token_pos)), + // `return`/`throw` at end of block + (Token::RightBrace, ..) if !settings.is_global => { + Ok(Stmt::Return(None, return_type, token_pos)) + } + // `return;` or `throw;` + (Token::SemiColon, ..) => Ok(Stmt::Return(None, return_type, token_pos)), + // `return` or `throw` with expression + _ => { + let expr = self.parse_expr(input, state, lib, settings.level_up())?; + Ok(Stmt::Return(Some(expr.into()), return_type, token_pos)) } - - lib.insert(hash, func.into()); - - Ok(Stmt::Noop(pos)) - } - - (.., pos) => Err(PERR::MissingToken( - Token::Fn.into(), - format!("following '{}'", Token::Private.syntax()), - ) - .into_err(pos)), - } - } - - Token::If => parse_if(input, state, lib, settings.level_up()), - Token::Switch => parse_switch(input, state, lib, settings.level_up()), - Token::While | Token::Loop if settings.default_options.allow_looping => { - parse_while_loop(input, state, lib, settings.level_up()) - } - Token::Do if settings.default_options.allow_looping => { - parse_do(input, state, lib, settings.level_up()) - } - Token::For if settings.default_options.allow_looping => { - parse_for(input, state, lib, settings.level_up()) - } - - Token::Continue if settings.default_options.allow_looping && settings.is_breakable => { - let pos = eat_token(input, Token::Continue); - Ok(Stmt::BreakLoop(AST_OPTION_NONE, pos)) - } - Token::Break if settings.default_options.allow_looping && settings.is_breakable => { - let pos = eat_token(input, Token::Break); - Ok(Stmt::BreakLoop(AST_OPTION_BREAK, pos)) - } - Token::Continue | Token::Break if settings.default_options.allow_looping => { - Err(PERR::LoopBreak.into_err(token_pos)) - } - - Token::Return | Token::Throw => { - let (return_type, token_pos) = input - .next() - .map(|(token, pos)| { - let flags = match token { - Token::Return => AST_OPTION_NONE, - Token::Throw => AST_OPTION_BREAK, - token => unreachable!( - "Token::Return or Token::Throw expected but gets {:?}", - token - ), - }; - (flags, pos) - }) - .expect(NEVER_ENDS); - - match input.peek().expect(NEVER_ENDS) { - // `return`/`throw` at - (Token::EOF, ..) => Ok(Stmt::Return(None, return_type, token_pos)), - // `return`/`throw` at end of block - (Token::RightBrace, ..) if !settings.is_global => { - Ok(Stmt::Return(None, return_type, token_pos)) - } - // `return;` or `throw;` - (Token::SemiColon, ..) => Ok(Stmt::Return(None, return_type, token_pos)), - // `return` or `throw` with expression - _ => { - let expr = parse_expr(input, state, lib, settings.level_up())?; - Ok(Stmt::Return(Some(expr.into()), return_type, token_pos)) } } + + Token::Try => self.parse_try_catch(input, state, lib, settings.level_up()), + + Token::Let => self.parse_let(input, state, lib, ReadWrite, false, settings.level_up()), + Token::Const => self.parse_let(input, state, lib, ReadOnly, false, settings.level_up()), + + #[cfg(not(feature = "no_module"))] + Token::Import => self.parse_import(input, state, lib, settings.level_up()), + + #[cfg(not(feature = "no_module"))] + Token::Export if !settings.is_global => Err(PERR::WrongExport.into_err(token_pos)), + + #[cfg(not(feature = "no_module"))] + Token::Export => self.parse_export(input, state, lib, settings.level_up()), + + _ => self.parse_expr_stmt(input, state, lib, settings.level_up()), } - - Token::Try => parse_try_catch(input, state, lib, settings.level_up()), - - Token::Let => parse_let(input, state, lib, ReadWrite, false, settings.level_up()), - Token::Const => parse_let(input, state, lib, ReadOnly, false, settings.level_up()), - - #[cfg(not(feature = "no_module"))] - Token::Import => parse_import(input, state, lib, settings.level_up()), - - #[cfg(not(feature = "no_module"))] - Token::Export if !settings.is_global => Err(PERR::WrongExport.into_err(token_pos)), - - #[cfg(not(feature = "no_module"))] - Token::Export => parse_export(input, state, lib, settings.level_up()), - - _ => parse_expr_stmt(input, state, lib, settings.level_up()), - } -} - -/// Parse a try/catch statement. -fn parse_try_catch( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - // try ... - let mut settings = settings; - settings.pos = eat_token(input, Token::Try); - - // try { try_block } - let try_block = parse_block(input, state, lib, settings.level_up())?; - - // try { try_block } catch - let (matched, catch_pos) = match_token(input, Token::Catch); - - if !matched { - return Err( - PERR::MissingToken(Token::Catch.into(), "for the 'try' statement".into()) - .into_err(catch_pos), - ); } - // try { try_block } catch ( - let catch_var = if match_token(input, Token::LeftParen).0 { - let (name, pos) = parse_var_name(input)?; - let (matched, err_pos) = match_token(input, Token::RightParen); + /// Parse a try/catch statement. + fn parse_try_catch( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; + + // try ... + let mut settings = settings; + settings.pos = eat_token(input, Token::Try); + + // try { try_block } + let try_block = self.parse_block(input, state, lib, settings.level_up())?; + + // try { try_block } catch + let (matched, catch_pos) = match_token(input, Token::Catch); if !matched { - return Err(PERR::MissingToken( - Token::RightParen.into(), - "to enclose the catch variable".into(), - ) - .into_err(err_pos)); + return Err( + PERR::MissingToken(Token::Catch.into(), "for the 'try' statement".into()) + .into_err(catch_pos), + ); } - let name = state.get_identifier("", name); - state.stack.push(name.clone(), ()); - Some(Ident { name, pos }) - } else { - None - }; + // try { try_block } catch ( + let catch_var = if match_token(input, Token::LeftParen).0 { + let (name, pos) = parse_var_name(input)?; + let (matched, err_pos) = match_token(input, Token::RightParen); - // try { try_block } catch ( var ) { catch_block } - let catch_block = parse_block(input, state, lib, settings.level_up())?; + if !matched { + return Err(PERR::MissingToken( + Token::RightParen.into(), + "to enclose the catch variable".into(), + ) + .into_err(err_pos)); + } - if catch_var.is_some() { - // Remove the error variable from the stack - state.stack.rewind(state.stack.len() - 1); + let name = state.get_identifier("", name); + state.stack.push(name.clone(), ()); + Some(Ident { name, pos }) + } else { + None + }; + + // try { try_block } catch ( var ) { catch_block } + let catch_block = self.parse_block(input, state, lib, settings.level_up())?; + + if catch_var.is_some() { + // Remove the error variable from the stack + state.stack.rewind(state.stack.len() - 1); + } + + Ok(Stmt::TryCatch( + TryCatchBlock { + try_block: try_block.into(), + catch_var, + catch_block: catch_block.into(), + } + .into(), + settings.pos, + )) } - Ok(Stmt::TryCatch( - TryCatchBlock { - try_block: try_block.into(), - catch_var, - catch_block: catch_block.into(), - } - .into(), - settings.pos, - )) -} - -/// Parse a function definition. -#[cfg(not(feature = "no_function"))] -fn parse_fn( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - access: crate::FnAccess, - settings: ParseSettings, + /// Parse a function definition. #[cfg(not(feature = "no_function"))] - #[cfg(feature = "metadata")] - comments: Vec>, -) -> ParseResult { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - let mut settings = settings; - - let (token, pos) = input.next().expect(NEVER_ENDS); - - let name = match token.into_function_name_for_override() { - Ok(r) => r, - Err(Token::Reserved(s)) => return Err(PERR::Reserved(s.to_string()).into_err(pos)), - Err(_) => return Err(PERR::FnMissingName.into_err(pos)), - }; - - match input.peek().expect(NEVER_ENDS) { - (Token::LeftParen, ..) => eat_token(input, Token::LeftParen), - (.., pos) => return Err(PERR::FnMissingParams(name.to_string()).into_err(*pos)), - }; - - let mut params = StaticVec::new_const(); - - if !match_token(input, Token::RightParen).0 { - let sep_err = format!("to separate the parameters of function '{}'", name); - - loop { - match input.next().expect(NEVER_ENDS) { - (Token::RightParen, ..) => break, - (Token::Identifier(s), pos) => { - if params.iter().any(|(p, _)| p == &*s) { - return Err( - PERR::FnDuplicatedParam(name.to_string(), s.to_string()).into_err(pos) - ); - } - let s = state.get_identifier("", s); - state.stack.push(s.clone(), ()); - params.push((s, pos)) - } - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (.., pos) => { - return Err(PERR::MissingToken( - Token::RightParen.into(), - format!("to close the parameters list of function '{}'", name), - ) - .into_err(pos)) - } - } - - match input.next().expect(NEVER_ENDS) { - (Token::RightParen, ..) => break, - (Token::Comma, ..) => (), - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (.., pos) => { - return Err(PERR::MissingToken(Token::Comma.into(), sep_err).into_err(pos)) - } - } - } - } - - // Parse function body - let body = match input.peek().expect(NEVER_ENDS) { - (Token::LeftBrace, ..) => { - settings.is_breakable = false; - parse_block(input, state, lib, settings.level_up())? - } - (.., pos) => return Err(PERR::FnMissingBody(name.to_string()).into_err(*pos)), - } - .into(); - - let mut params: StaticVec<_> = params.into_iter().map(|(p, ..)| p).collect(); - params.shrink_to_fit(); - - Ok(ScriptFnDef { - name: state.get_identifier("", name), - access, - params, - body, - #[cfg(not(feature = "no_module"))] - environ: None, + fn parse_fn( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + access: crate::FnAccess, + settings: ParseSettings, #[cfg(not(feature = "no_function"))] #[cfg(feature = "metadata")] - comments: if comments.is_empty() { - None - } else { - Some(comments.into()) - }, - }) -} + comments: StaticVec, + ) -> ParseResult { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; -/// Creates a curried expression from a list of external variables -#[cfg(not(feature = "no_function"))] -#[cfg(not(feature = "no_closure"))] -fn make_curry_from_externals( - state: &mut ParseState, - fn_expr: Expr, - externals: StaticVec, - pos: Position, -) -> Expr { - // If there are no captured variables, no need to curry - if externals.is_empty() { - return fn_expr; - } + let mut settings = settings; - let num_externals = externals.len(); - let mut args = StaticVec::with_capacity(externals.len() + 1); + let (token, pos) = input.next().expect(NEVER_ENDS); - args.push(fn_expr); + let name = match token.into_function_name_for_override() { + Ok(r) => r, + Err(Token::Reserved(s)) => return Err(PERR::Reserved(s.to_string()).into_err(pos)), + Err(_) => return Err(PERR::FnMissingName.into_err(pos)), + }; - args.extend( - externals - .iter() - .cloned() - .map(|crate::ast::Ident { name, pos }| { - Expr::Variable( - None, - pos, - ( - None, - #[cfg(not(feature = "no_module"))] - None, - #[cfg(feature = "no_module")] - (), - name, - ) - .into(), - ) - }), - ); + match input.peek().expect(NEVER_ENDS) { + (Token::LeftParen, ..) => eat_token(input, Token::LeftParen), + (.., pos) => return Err(PERR::FnMissingParams(name.to_string()).into_err(*pos)), + }; - let expr = FnCallExpr { - name: state.get_identifier("", crate::engine::KEYWORD_FN_PTR_CURRY), - hashes: FnCallHashes::from_native(calc_fn_hash( - crate::engine::KEYWORD_FN_PTR_CURRY, - num_externals + 1, - )), - args, - pos, - ..Default::default() - } - .into_fn_call_expr(pos); + let mut params = StaticVec::new_const(); - // Convert the entire expression into a statement block, then insert the relevant - // [`Share`][Stmt::Share] statements. - let mut statements = StaticVec::with_capacity(externals.len() + 1); - statements.extend( - externals - .into_iter() - .map(|crate::ast::Ident { name, pos }| Stmt::Share(name.into(), pos)), - ); - statements.push(Stmt::Expr(expr.into())); - Expr::Stmt(crate::ast::StmtBlock::new(statements, pos, Position::NONE).into()) -} + if !match_token(input, Token::RightParen).0 { + let sep_err = format!("to separate the parameters of function '{}'", name); -/// Parse an anonymous function definition. -#[cfg(not(feature = "no_function"))] -fn parse_anon_fn( - input: &mut TokenStream, - state: &mut ParseState, - lib: &mut FnLib, - settings: ParseSettings, -) -> ParseResult<(Expr, ScriptFnDef)> { - #[cfg(not(feature = "unchecked"))] - settings.ensure_level_within_max_limit(state.max_expr_depth)?; - - let mut settings = settings; - let mut params_list = StaticVec::new_const(); - - if input.next().expect(NEVER_ENDS).0 != Token::Or && !match_token(input, Token::Pipe).0 { - loop { - match input.next().expect(NEVER_ENDS) { - (Token::Pipe, ..) => break, - (Token::Identifier(s), pos) => { - if params_list.iter().any(|p| p == &*s) { - return Err( - PERR::FnDuplicatedParam("".to_string(), s.to_string()).into_err(pos) - ); + loop { + match input.next().expect(NEVER_ENDS) { + (Token::RightParen, ..) => break, + (Token::Identifier(s), pos) => { + if params.iter().any(|(p, _)| p == &*s) { + return Err(PERR::FnDuplicatedParam(name.to_string(), s.to_string()) + .into_err(pos)); + } + let s = state.get_identifier("", s); + state.stack.push(s.clone(), ()); + params.push((s, pos)) + } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (.., pos) => { + return Err(PERR::MissingToken( + Token::RightParen.into(), + format!("to close the parameters list of function '{}'", name), + ) + .into_err(pos)) } - let s = state.get_identifier("", s); - state.stack.push(s.clone(), ()); - params_list.push(s) } - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (.., pos) => { - return Err(PERR::MissingToken( - Token::Pipe.into(), - "to close the parameters list of anonymous function".into(), - ) - .into_err(pos)) - } - } - match input.next().expect(NEVER_ENDS) { - (Token::Pipe, ..) => break, - (Token::Comma, ..) => (), - (Token::LexError(err), pos) => return Err(err.into_err(pos)), - (.., pos) => { - return Err(PERR::MissingToken( - Token::Comma.into(), - "to separate the parameters of anonymous function".into(), - ) - .into_err(pos)) + match input.next().expect(NEVER_ENDS) { + (Token::RightParen, ..) => break, + (Token::Comma, ..) => (), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (.., pos) => { + return Err(PERR::MissingToken(Token::Comma.into(), sep_err).into_err(pos)) + } } } } + + // Parse function body + let body = match input.peek().expect(NEVER_ENDS) { + (Token::LeftBrace, ..) => { + settings.is_breakable = false; + self.parse_block(input, state, lib, settings.level_up())? + } + (.., pos) => return Err(PERR::FnMissingBody(name.to_string()).into_err(*pos)), + } + .into(); + + let mut params: StaticVec<_> = params.into_iter().map(|(p, ..)| p).collect(); + params.shrink_to_fit(); + + Ok(ScriptFnDef { + name: state.get_identifier("", name), + access, + params, + body, + #[cfg(not(feature = "no_module"))] + environ: None, + #[cfg(not(feature = "no_function"))] + #[cfg(feature = "metadata")] + comments: if comments.is_empty() { + None + } else { + Some( + comments + .into_iter() + .map(|s| s.to_string().into_boxed_str()) + .collect::>() + .into_boxed_slice(), + ) + }, + }) } - // Parse function body - settings.is_breakable = false; - let body = parse_stmt(input, state, lib, settings.level_up())?; - - // External variables may need to be processed in a consistent order, - // so extract them into a list. + /// Creates a curried expression from a list of external variables + #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_closure"))] - let (mut params, externals) = { - let externals: StaticVec<_> = state.external_vars.iter().cloned().collect(); + fn make_curry_from_externals( + state: &mut ParseState, + fn_expr: Expr, + externals: StaticVec, + pos: Position, + ) -> Expr { + // If there are no captured variables, no need to curry + if externals.is_empty() { + return fn_expr; + } - let mut params = StaticVec::with_capacity(params_list.len() + externals.len()); - params.extend( + let num_externals = externals.len(); + let mut args = StaticVec::with_capacity(externals.len() + 1); + + args.push(fn_expr); + + args.extend( externals .iter() - .map(|crate::ast::Ident { name, .. }| name.clone()), + .cloned() + .map(|crate::ast::Ident { name, pos }| { + Expr::Variable( + None, + pos, + ( + None, + #[cfg(not(feature = "no_module"))] + None, + #[cfg(feature = "no_module")] + (), + name, + ) + .into(), + ) + }), ); - (params, externals) - }; - #[cfg(feature = "no_closure")] - let mut params = StaticVec::with_capacity(params_list.len()); + let expr = FnCallExpr { + name: state.get_identifier("", crate::engine::KEYWORD_FN_PTR_CURRY), + hashes: FnCallHashes::from_native(calc_fn_hash( + crate::engine::KEYWORD_FN_PTR_CURRY, + num_externals + 1, + )), + args, + pos, + ..Default::default() + } + .into_fn_call_expr(pos); - params.append(&mut params_list); + // Convert the entire expression into a statement block, then insert the relevant + // [`Share`][Stmt::Share] statements. + let mut statements = StaticVec::with_capacity(externals.len() + 1); + statements.extend( + externals + .into_iter() + .map(|crate::ast::Ident { name, pos }| Stmt::Share(name.into(), pos)), + ); + statements.push(Stmt::Expr(expr.into())); + Expr::Stmt(crate::ast::StmtBlock::new(statements, pos, Position::NONE).into()) + } - // Create unique function name by hashing the script body plus the parameters. - let hasher = &mut get_hasher(); - params.iter().for_each(|p| p.hash(hasher)); - body.hash(hasher); - let hash = hasher.finish(); - let fn_name = state.get_identifier("", make_anonymous_fn(hash)); + /// Parse an anonymous function definition. + #[cfg(not(feature = "no_function"))] + fn parse_anon_fn( + &self, + input: &mut TokenStream, + state: &mut ParseState, + lib: &mut FnLib, + settings: ParseSettings, + ) -> ParseResult<(Expr, ScriptFnDef)> { + #[cfg(not(feature = "unchecked"))] + settings.ensure_level_within_max_limit(state.max_expr_depth)?; - // Define the function - let script = ScriptFnDef { - name: fn_name.clone(), - access: crate::FnAccess::Public, - params, - body: body.into(), - #[cfg(not(feature = "no_module"))] - environ: None, - #[cfg(not(feature = "no_function"))] - #[cfg(feature = "metadata")] - comments: None, - }; + let mut settings = settings; + let mut params_list = StaticVec::new_const(); - let fn_ptr = crate::FnPtr::new_unchecked(fn_name, StaticVec::new_const()); - let expr = Expr::DynamicConstant(Box::new(fn_ptr.into()), settings.pos); + if input.next().expect(NEVER_ENDS).0 != Token::Or && !match_token(input, Token::Pipe).0 { + loop { + match input.next().expect(NEVER_ENDS) { + (Token::Pipe, ..) => break, + (Token::Identifier(s), pos) => { + if params_list.iter().any(|p| p == &*s) { + return Err(PERR::FnDuplicatedParam("".to_string(), s.to_string()) + .into_err(pos)); + } + let s = state.get_identifier("", s); + state.stack.push(s.clone(), ()); + params_list.push(s) + } + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (.., pos) => { + return Err(PERR::MissingToken( + Token::Pipe.into(), + "to close the parameters list of anonymous function".into(), + ) + .into_err(pos)) + } + } - #[cfg(not(feature = "no_closure"))] - let expr = make_curry_from_externals(state, expr, externals, settings.pos); + match input.next().expect(NEVER_ENDS) { + (Token::Pipe, ..) => break, + (Token::Comma, ..) => (), + (Token::LexError(err), pos) => return Err(err.into_err(pos)), + (.., pos) => { + return Err(PERR::MissingToken( + Token::Comma.into(), + "to separate the parameters of anonymous function".into(), + ) + .into_err(pos)) + } + } + } + } - Ok((expr, script)) -} + // Parse function body + settings.is_breakable = false; + let body = self.parse_stmt(input, state, lib, settings.level_up())?; + + // External variables may need to be processed in a consistent order, + // so extract them into a list. + #[cfg(not(feature = "no_closure"))] + let (mut params, externals) = { + let externals: StaticVec<_> = state.external_vars.iter().cloned().collect(); + + let mut params = StaticVec::with_capacity(params_list.len() + externals.len()); + params.extend( + externals + .iter() + .map(|crate::ast::Ident { name, .. }| name.clone()), + ); + + (params, externals) + }; + #[cfg(feature = "no_closure")] + let mut params = StaticVec::with_capacity(params_list.len()); + + params.append(&mut params_list); + + // Create unique function name by hashing the script body plus the parameters. + let hasher = &mut get_hasher(); + params.iter().for_each(|p| p.hash(hasher)); + body.hash(hasher); + let hash = hasher.finish(); + let fn_name = state.get_identifier("", make_anonymous_fn(hash)); + + // Define the function + let script = ScriptFnDef { + name: fn_name.clone(), + access: crate::FnAccess::Public, + params, + body: body.into(), + #[cfg(not(feature = "no_module"))] + environ: None, + #[cfg(not(feature = "no_function"))] + #[cfg(feature = "metadata")] + comments: None, + }; + + let fn_ptr = crate::FnPtr::new_unchecked(fn_name, StaticVec::new_const()); + let expr = Expr::DynamicConstant(Box::new(fn_ptr.into()), settings.pos); + + #[cfg(not(feature = "no_closure"))] + let expr = Self::make_curry_from_externals(state, expr, externals, settings.pos); + + Ok((expr, script)) + } -impl Engine { /// Parse a global level expression. pub(crate) fn parse_global_expr( &self, @@ -3454,24 +3543,25 @@ impl Engine { let mut functions = BTreeMap::new(); let settings = ParseSettings { - default_options: self.options, - allow_if_expr: false, - allow_switch_expr: false, - allow_stmt_expr: false, - #[cfg(not(feature = "no_function"))] - allow_anonymous_fn: false, - strict_var: self.options.strict_var, is_global: true, #[cfg(not(feature = "no_function"))] is_function_scope: false, #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_closure"))] - is_closure: false, + is_closure_scope: false, is_breakable: false, level: 0, + options: LanguageOptions { + allow_if_expr: false, + allow_switch_expr: false, + allow_stmt_expr: false, + #[cfg(not(feature = "no_function"))] + allow_anonymous_fn: false, + ..self.options + }, pos: Position::NONE, }; - let expr = parse_expr(input, state, &mut functions, settings)?; + let expr = self.parse_expr(input, state, &mut functions, settings)?; assert!(functions.is_empty()); @@ -3515,25 +3605,19 @@ impl Engine { while !input.peek().expect(NEVER_ENDS).0.is_eof() { let settings = ParseSettings { - default_options: self.options, - allow_if_expr: self.options.allow_if_expr, - allow_switch_expr: self.options.allow_switch_expr, - allow_stmt_expr: self.options.allow_stmt_expr, - #[cfg(not(feature = "no_function"))] - allow_anonymous_fn: self.options.allow_anonymous_fn, - strict_var: self.options.strict_var, is_global: true, #[cfg(not(feature = "no_function"))] is_function_scope: false, #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_closure"))] - is_closure: false, + is_closure_scope: false, is_breakable: false, + options: self.options, level: 0, pos: Position::NONE, }; - let stmt = parse_stmt(input, state, &mut functions, settings)?; + let stmt = self.parse_stmt(input, state, &mut functions, settings)?; if stmt.is_noop() { continue; diff --git a/src/tests.rs b/src/tests.rs index f9e099e2..d1ffd525 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -19,6 +19,7 @@ fn check_struct_sizes() { size_of::(), if cfg!(feature = "no_position") { 0 } else { 4 } ); + assert_eq!(size_of::(), 32); assert_eq!(size_of::(), if PACKED { 12 } else { 16 }); assert_eq!(size_of::>(), if PACKED { 12 } else { 16 }); assert_eq!(size_of::(), if PACKED { 12 } else { 16 }); @@ -26,7 +27,7 @@ fn check_struct_sizes() { #[cfg(target_pointer_width = "64")] { - assert_eq!(size_of::(), 400); + assert_eq!(size_of::(), 536); assert_eq!(size_of::(), 80); assert_eq!(size_of::(), 56); assert_eq!( diff --git a/src/tokenizer.rs b/src/tokenizer.rs index bf8326ac..5e8cd23a 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -5,7 +5,7 @@ use crate::engine::{ KEYWORD_FN_PTR_CURRY, KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, KEYWORD_THIS, KEYWORD_TYPE_OF, }; use crate::func::native::OnParseTokenCallback; -use crate::{Engine, LexError, StaticVec, INT, UNSIGNED_INT}; +use crate::{Engine, LexError, SmartString, StaticVec, INT, UNSIGNED_INT}; #[cfg(feature = "no_std")] use std::prelude::v1::*; use std::{ @@ -363,13 +363,13 @@ pub enum Token { #[cfg(feature = "decimal")] DecimalConstant(rust_decimal::Decimal), /// An identifier. - Identifier(Box), + Identifier(SmartString), /// A character constant. CharConstant(char), /// A string constant. - StringConstant(Box), + StringConstant(SmartString), /// An interpolated string. - InterpolatedString(Box), + InterpolatedString(SmartString), /// `{` LeftBrace, /// `}` @@ -534,13 +534,13 @@ pub enum Token { #[cfg(not(feature = "no_module"))] As, /// A lexer error. - LexError(LexError), + LexError(Box), /// A comment block. - Comment(Box), + Comment(SmartString), /// A reserved symbol. - Reserved(Box), + Reserved(SmartString), /// A custom keyword. - Custom(Box), + Custom(SmartString), /// End of the input stream. EOF, } @@ -1022,9 +1022,9 @@ impl Token { /// Convert a token into a function name, if possible. #[cfg(not(feature = "no_function"))] #[inline] - pub(crate) fn into_function_name_for_override(self) -> Result, Self> { + pub(crate) fn into_function_name_for_override(self) -> Result { match self { - Self::Custom(s) | Self::Identifier(s) if is_valid_function_name(&*s) => Ok(s), + Self::Custom(s) | Self::Identifier(s) if is_valid_function_name(&s) => Ok(s), _ => Err(self), } } @@ -1112,9 +1112,9 @@ pub fn parse_string_literal( verbatim: bool, allow_line_continuation: bool, allow_interpolation: bool, -) -> Result<(Box, bool, Position), (LexError, Position)> { - let mut result = String::with_capacity(12); - let mut escape = String::with_capacity(12); +) -> Result<(SmartString, bool, Position), (LexError, Position)> { + let mut result = SmartString::new(); + let mut escape = SmartString::new(); let start = *pos; let mut first_char = Position::NONE; @@ -1146,7 +1146,6 @@ pub fn parse_string_literal( break; } None => { - result += &escape; pos.advance(); state.is_within_text_terminated_by = None; return Err((LERR::UnterminatedString, start)); @@ -1242,7 +1241,7 @@ pub fn parse_string_literal( result.push( char::from_u32(out_val) - .ok_or_else(|| (LERR::MalformedEscapeSequence(seq), *pos))?, + .ok_or_else(|| (LERR::MalformedEscapeSequence(seq.to_string()), *pos))?, ); } @@ -1283,7 +1282,7 @@ pub fn parse_string_literal( _ if !escape.is_empty() => { escape.push(next_char); - return Err((LERR::MalformedEscapeSequence(escape), *pos)); + return Err((LERR::MalformedEscapeSequence(escape.to_string()), *pos)); } // Whitespace to skip @@ -1309,7 +1308,7 @@ pub fn parse_string_literal( } } - Ok((result.into(), interpolated, first_char)) + Ok((result, interpolated, first_char)) } /// Consume the next character. @@ -1445,7 +1444,7 @@ fn get_next_token_inner( // Within text? if let Some(ch) = state.is_within_text_terminated_by.take() { return parse_string_literal(stream, state, pos, ch, true, false, true).map_or_else( - |(err, err_pos)| Some((Token::LexError(err), err_pos)), + |(err, err_pos)| Some((Token::LexError(err.into()), err_pos)), |(result, interpolated, start_pos)| { if interpolated { Some((Token::InterpolatedString(result), start_pos)) @@ -1583,7 +1582,9 @@ fn get_next_token_inner( .map(|v| v as INT) .map(Token::IntegerConstant) .unwrap_or_else(|_| { - Token::LexError(LERR::MalformedNumber(result.into_iter().collect())) + Token::LexError( + LERR::MalformedNumber(result.into_iter().collect()).into(), + ) }) } else { let out: String = @@ -1609,7 +1610,9 @@ fn get_next_token_inner( }); num.unwrap_or_else(|_| { - Token::LexError(LERR::MalformedNumber(result.into_iter().collect())) + Token::LexError( + LERR::MalformedNumber(result.into_iter().collect()).into(), + ) }) }, num_pos, @@ -1630,7 +1633,7 @@ fn get_next_token_inner( ('"', ..) => { return parse_string_literal(stream, state, pos, c, false, true, false) .map_or_else( - |(err, err_pos)| Some((Token::LexError(err), err_pos)), + |(err, err_pos)| Some((Token::LexError(err.into()), err_pos)), |(result, ..)| Some((Token::StringConstant(result), start_pos)), ); } @@ -1656,7 +1659,7 @@ fn get_next_token_inner( } return parse_string_literal(stream, state, pos, c, true, false, true).map_or_else( - |(err, err_pos)| Some((Token::LexError(err), err_pos)), + |(err, err_pos)| Some((Token::LexError(err.into()), err_pos)), |(result, interpolated, ..)| { if interpolated { Some((Token::InterpolatedString(result), start_pos)) @@ -1670,21 +1673,21 @@ fn get_next_token_inner( // ' - character literal ('\'', '\'') => { return Some(( - Token::LexError(LERR::MalformedChar("".to_string())), + Token::LexError(LERR::MalformedChar("".to_string()).into()), start_pos, )) } ('\'', ..) => { return Some( parse_string_literal(stream, state, pos, c, false, false, false).map_or_else( - |(err, err_pos)| (Token::LexError(err), err_pos), + |(err, err_pos)| (Token::LexError(err.into()), err_pos), |(result, ..)| { let mut chars = result.chars(); let first = chars.next().unwrap(); if chars.next().is_some() { ( - Token::LexError(LERR::MalformedChar(result.to_string())), + Token::LexError(LERR::MalformedChar(result.to_string()).into()), start_pos, ) } else { @@ -2021,7 +2024,7 @@ fn get_next_token_inner( (ch, ..) => { return Some(( - Token::LexError(LERR::UnexpectedInput(ch.to_string())), + Token::LexError(LERR::UnexpectedInput(ch.to_string()).into()), start_pos, )) } @@ -2063,7 +2066,7 @@ fn get_identifier( if !is_valid_identifier { return Some(( - Token::LexError(LERR::MalformedIdentifier(identifier)), + Token::LexError(LERR::MalformedIdentifier(identifier).into()), start_pos, )); } @@ -2244,65 +2247,65 @@ impl<'a> Iterator for TokenIterator<'a> { // script it is a syntax error. Some((Token::StringConstant(..), pos)) if self.state.is_within_text_terminated_by.is_some() => { self.state.is_within_text_terminated_by = None; - return Some((Token::LexError(LERR::UnterminatedString), pos)); + return Some((Token::LexError(LERR::UnterminatedString.into()), pos)); } // Reserved keyword/symbol Some((Token::Reserved(s), pos)) => (match - (&*s, self.engine.custom_keywords.contains_key(&*s)) + (&*s, !self.engine.custom_keywords.is_empty() && self.engine.custom_keywords.contains_key(&*s)) { ("===", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(), "'===' is not a valid operator. This is not JavaScript! Should it be '=='?".to_string(), - )), + ).into()), ("!==", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(), "'!==' is not a valid operator. This is not JavaScript! Should it be '!='?".to_string(), - )), + ).into()), ("->", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(), - "'->' is not a valid symbol. This is not C or C++!".to_string())), + "'->' is not a valid symbol. This is not C or C++!".to_string()).into()), ("<-", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(), "'<-' is not a valid symbol. This is not Go! Should it be '<='?".to_string(), - )), + ).into()), (":=", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(), "':=' is not a valid assignment operator. This is not Go or Pascal! Should it be simply '='?".to_string(), - )), + ).into()), (":;", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(), "':;' is not a valid symbol. Should it be '::'?".to_string(), - )), + ).into()), ("::<", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(), "'::<>' is not a valid symbol. This is not Rust! Should it be '::'?".to_string(), - )), + ).into()), ("(*", false) | ("*)", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(), "'(* .. *)' is not a valid comment format. This is not Pascal! Should it be '/* .. */'?".to_string(), - )), + ).into()), ("# {", false) => Token::LexError(LERR::ImproperSymbol(s.to_string(), "'#' is not a valid symbol. Should it be '#{'?".to_string(), - )), + ).into()), // Reserved keyword/operator that is custom. (.., true) => Token::Custom(s), // Reserved keyword that is not custom and disabled. - (token, false) if self.engine.disabled_symbols.contains(token) => { + (token, false) if !self.engine.disabled_symbols.is_empty() && self.engine.disabled_symbols.contains(token) => { let msg = format!("reserved {} '{}' is disabled", if is_valid_identifier(token.chars()) { "keyword"} else {"symbol"}, token); - Token::LexError(LERR::ImproperSymbol(s.to_string(), msg)) + Token::LexError(LERR::ImproperSymbol(s.to_string(), msg).into()) }, // Reserved keyword/operator that is not custom. (.., false) => Token::Reserved(s), }, pos), // Custom keyword - Some((Token::Identifier(s), pos)) if self.engine.custom_keywords.contains_key(&*s) => { + Some((Token::Identifier(s), pos)) if !self.engine.custom_keywords.is_empty() && self.engine.custom_keywords.contains_key(&*s) => { (Token::Custom(s), pos) } // Custom keyword/symbol - must be disabled - Some((token, pos)) if self.engine.custom_keywords.contains_key(&*token.syntax()) => { - if self.engine.disabled_symbols.contains(&*token.syntax()) { + Some((token, pos)) if !self.engine.custom_keywords.is_empty() && self.engine.custom_keywords.contains_key(token.literal_syntax()) => { + if !self.engine.disabled_symbols.is_empty() && self.engine.disabled_symbols.contains(token.literal_syntax()) { // Disabled standard keyword/symbol - (Token::Custom(token.syntax().into()), pos) + (Token::Custom(token.literal_syntax().into()), pos) } else { // Active standard keyword - should never be a custom keyword! unreachable!("{:?} is an active keyword", token) } } // Disabled symbol - Some((token, pos)) if self.engine.disabled_symbols.contains(&*token.syntax()) => { - (Token::Reserved(token.syntax().into()), pos) + Some((token, pos)) if !self.engine.disabled_symbols.is_empty() && self.engine.disabled_symbols.contains(token.literal_syntax()) => { + (Token::Reserved(token.literal_syntax().into()), pos) } // Normal symbol Some(r) => r, diff --git a/src/types/dynamic.rs b/src/types/dynamic.rs index abf7f346..30c05a52 100644 --- a/src/types/dynamic.rs +++ b/src/types/dynamic.rs @@ -162,8 +162,6 @@ pub enum Union { /// An integer value. Int(INT, Tag, AccessMode), /// A floating-point value. - /// - /// Not available under `no_float`. #[cfg(not(feature = "no_float"))] Float(crate::ast::FloatWrapper, Tag, AccessMode), /// _(decimal)_ A fixed-precision decimal value. @@ -171,25 +169,17 @@ pub enum Union { #[cfg(feature = "decimal")] Decimal(Box, Tag, AccessMode), /// An array value. - /// - /// Not available under `no_index`. #[cfg(not(feature = "no_index"))] Array(Box, Tag, AccessMode), /// An blob (byte array). - /// - /// Not available under `no_index`. #[cfg(not(feature = "no_index"))] Blob(Box, Tag, AccessMode), /// An object map value. - /// - /// Not available under `no_object`. #[cfg(not(feature = "no_object"))] Map(Box, Tag, AccessMode), /// A function pointer. FnPtr(Box, Tag, AccessMode), /// A timestamp value. - /// - /// Not available under `no-std`. #[cfg(not(feature = "no_std"))] TimeStamp(Box, Tag, AccessMode), @@ -198,8 +188,6 @@ pub enum Union { Variant(Box>, Tag, AccessMode), /// A _shared_ value of any type. - /// - /// Not available under `no_closure`. #[cfg(not(feature = "no_closure"))] Shared(crate::Shared>, Tag, AccessMode), } @@ -218,14 +206,9 @@ enum DynamicReadLockInner<'d, T: Clone> { /// A simple reference to a non-shared value. Reference(&'d T), - /// A read guard to a shared [`RefCell`][std::cell::RefCell]. + /// A read guard to a shared value. #[cfg(not(feature = "no_closure"))] - #[cfg(not(feature = "sync"))] - Guard(std::cell::Ref<'d, Dynamic>), - /// A read guard to a shared [`RwLock`][std::sync::RwLock]. - #[cfg(not(feature = "no_closure"))] - #[cfg(feature = "sync")] - Guard(std::sync::RwLockReadGuard<'d, Dynamic>), + Guard(crate::func::native::LockGuard<'d, Dynamic>), } impl<'d, T: Any + Clone> Deref for DynamicReadLock<'d, T> { @@ -256,10 +239,8 @@ enum DynamicWriteLockInner<'d, T: Clone> { Reference(&'d mut T), /// A write guard to a shared value. - /// - /// Not available under `no_closure`. #[cfg(not(feature = "no_closure"))] - Guard(crate::func::native::LockGuard<'d, Dynamic>), + Guard(crate::func::native::LockGuardMut<'d, Dynamic>), } impl<'d, T: Any + Clone> Deref for DynamicWriteLock<'d, T> { diff --git a/src/types/error.rs b/src/types/error.rs index 9eaf7907..10dfe851 100644 --- a/src/types/error.rs +++ b/src/types/error.rs @@ -145,9 +145,9 @@ impl fmt::Display for EvalAltResult { } Self::ErrorInModule(s, err, ..) if s.is_empty() => { - write!(f, "Error in module: {}", err)? + write!(f, "Error in module > {}", err)? } - Self::ErrorInModule(s, err, ..) => write!(f, "Error in module {}: {}", s, err)?, + Self::ErrorInModule(s, err, ..) => write!(f, "Error in module '{}' > {}", s, err)?, Self::ErrorVariableExists(s, ..) => write!(f, "Variable is already defined: {}", s)?, Self::ErrorForbiddenVariable(s, ..) => write!(f, "Forbidden variable name: {}", s)?, @@ -180,15 +180,15 @@ impl fmt::Display for EvalAltResult { Self::ErrorRuntime(d, ..) => write!(f, "Runtime error: {}", d)?, Self::ErrorAssignmentToConstant(s, ..) => write!(f, "Cannot modify constant: {}", s)?, - Self::ErrorMismatchOutputType(s, r, ..) => match (r.as_str(), s.as_str()) { - ("", s) => write!(f, "Output type is incorrect, expecting {}", s), - (r, "") => write!(f, "Output type is incorrect: {}", r), - (r, s) => write!(f, "Output type is incorrect: {} (expecting {})", r, s), + Self::ErrorMismatchOutputType(e, a, ..) => match (a.as_str(), e.as_str()) { + ("", e) => write!(f, "Output type is incorrect, expecting {}", e), + (a, "") => write!(f, "Output type is incorrect: {}", a), + (a, e) => write!(f, "Output type is incorrect: {} (expecting {})", a, e), }?, - Self::ErrorMismatchDataType(s, r, ..) => match (r.as_str(), s.as_str()) { - ("", s) => write!(f, "Data type is incorrect, expecting {}", s), - (r, "") => write!(f, "Data type is incorrect: {}", r), - (r, s) => write!(f, "Data type is incorrect: {} (expecting {})", r, s), + Self::ErrorMismatchDataType(e, a, ..) => match (a.as_str(), e.as_str()) { + ("", e) => write!(f, "Data type is incorrect, expecting {}", e), + (a, "") => write!(f, "Data type is incorrect: {}", a), + (a, e) => write!(f, "Data type is incorrect: {} (expecting {})", a, e), }?, Self::ErrorArithmetic(s, ..) => match s.as_str() { "" => f.write_str("Arithmetic error"), diff --git a/src/types/immutable_string.rs b/src/types/immutable_string.rs index 8c8dc3eb..1df41b4d 100644 --- a/src/types/immutable_string.rs +++ b/src/types/immutable_string.rs @@ -459,8 +459,50 @@ impl Sub for &ImmutableString { impl SubAssign for ImmutableString { #[inline] fn sub_assign(&mut self, rhs: String) { - let rhs: SmartString = self.replace(&rhs, "").into(); - self.0 = rhs.into(); + if !rhs.is_empty() { + let rhs: SmartString = self.replace(&rhs, "").into(); + self.0 = rhs.into(); + } + } +} + +impl Sub<&str> for ImmutableString { + type Output = Self; + + #[inline] + fn sub(self, rhs: &str) -> Self::Output { + if rhs.is_empty() { + self + } else if self.is_empty() { + rhs.into() + } else { + self.replace(rhs, "").into() + } + } +} + +impl Sub<&str> for &ImmutableString { + type Output = ImmutableString; + + #[inline] + fn sub(self, rhs: &str) -> Self::Output { + if rhs.is_empty() { + self.clone() + } else if self.is_empty() { + rhs.into() + } else { + self.replace(rhs, "").into() + } + } +} + +impl SubAssign<&str> for ImmutableString { + #[inline] + fn sub_assign(&mut self, rhs: &str) { + if !rhs.is_empty() { + let rhs: SmartString = self.replace(rhs, "").into(); + self.0 = rhs.into(); + } } } diff --git a/src/types/interner.rs b/src/types/interner.rs index 05e19fc5..e82152be 100644 --- a/src/types/interner.rs +++ b/src/types/interner.rs @@ -59,7 +59,7 @@ impl StringsInterner { _ => unreachable!("unsupported prefix {}", prefix.as_ref()), }; - if dict.contains_key(text.as_ref()) { + if !dict.is_empty() && dict.contains_key(text.as_ref()) { dict.get(text.as_ref()).unwrap().clone() } else { let value: ImmutableString = mapper(text.as_ref()).into(); diff --git a/src/types/parse_error.rs b/src/types/parse_error.rs index fb922e15..0a86d8e9 100644 --- a/src/types/parse_error.rs +++ b/src/types/parse_error.rs @@ -267,7 +267,12 @@ impl From for ParseErrorType { /// Error when parsing a script. #[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub struct ParseError(pub Box, pub Position); +pub struct ParseError( + /// Parse error type. + pub Box, + /// [Position] of the parse error. + pub Position, +); impl Error for ParseError {} diff --git a/src/types/scope.rs b/src/types/scope.rs index 310877c7..199940e9 100644 --- a/src/types/scope.rs +++ b/src/types/scope.rs @@ -1,7 +1,7 @@ //! Module that defines the [`Scope`] type representing a function call-stack scope. use super::dynamic::{AccessMode, Variant}; -use crate::{Dynamic, Identifier, StaticVec}; +use crate::{Dynamic, Identifier}; use smallvec::SmallVec; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -53,20 +53,20 @@ const SCOPE_ENTRIES_INLINED: usize = 8; // // # Implementation Notes // -// [`Scope`] is implemented as two arrays of exactly the same length. Variables data (name, type, -// etc.) is manually split into two equal-length arrays. That's because variable names take up the -// most space, with [`Identifier`] being three words long, but in the vast majority of cases the -// name is NOT used to look up a variable. Variable lookup is usually via direct indexing, -// by-passing the name altogether. +// [`Scope`] is implemented as three arrays of exactly the same length. That's because variable +// names take up the most space, with [`Identifier`] being three words long, but in the vast +// majority of cases the name is NOT used to look up a variable. Variable lookup is usually via +// direct indexing, by-passing the name altogether. // -// Since [`Dynamic`] is reasonably small, packing it tightly improves cache locality when variables -// are accessed. +// [`Dynamic`] is reasonably small so packing it tightly improves cache performance. #[derive(Debug, Clone, Hash, Default)] pub struct Scope<'a> { /// Current value of the entry. values: SmallVec<[Dynamic; SCOPE_ENTRIES_INLINED]>, - /// (Name, aliases) of the entry. - names: SmallVec<[(Identifier, Option>>); SCOPE_ENTRIES_INLINED]>, + /// Name of the entry. + names: SmallVec<[Identifier; SCOPE_ENTRIES_INLINED]>, + /// Aliases of the entry. + aliases: SmallVec<[Vec; SCOPE_ENTRIES_INLINED]>, /// Phantom to keep the lifetime parameter in order not to break existing code. phantom: PhantomData<&'a ()>, } @@ -77,15 +77,12 @@ impl IntoIterator for Scope<'_> { #[inline] fn into_iter(self) -> Self::IntoIter { - Box::new(self.values.into_iter().zip(self.names.into_iter()).map( - |(value, (name, alias))| { - ( - name.into(), - value, - alias.map(|a| a.to_vec()).unwrap_or_default(), - ) - }, - )) + Box::new( + self.values + .into_iter() + .zip(self.names.into_iter().zip(self.aliases.into_iter())) + .map(|(value, (name, alias))| (name.into(), value, alias)), + ) } } @@ -108,6 +105,7 @@ impl Scope<'_> { Self { values: SmallVec::new_const(), names: SmallVec::new_const(), + aliases: SmallVec::new_const(), phantom: PhantomData, } } @@ -134,6 +132,7 @@ impl Scope<'_> { pub fn clear(&mut self) -> &mut Self { self.names.clear(); self.values.clear(); + self.aliases.clear(); self } /// Get the number of entries inside the [`Scope`]. @@ -258,7 +257,8 @@ impl Scope<'_> { access: AccessMode, mut value: Dynamic, ) -> &mut Self { - self.names.push((name.into(), None)); + self.names.push(name.into()); + self.aliases.push(Vec::new()); value.set_access_mode(access); self.values.push(value); self @@ -293,6 +293,7 @@ impl Scope<'_> { pub fn rewind(&mut self, size: usize) -> &mut Self { self.names.truncate(size); self.values.truncate(size); + self.aliases.truncate(size); self } /// Does the [`Scope`] contain the entry? @@ -311,7 +312,7 @@ impl Scope<'_> { #[inline] #[must_use] pub fn contains(&self, name: &str) -> bool { - self.names.iter().any(|(key, ..)| name == key) + self.names.iter().any(|key| name == key) } /// Find an entry in the [`Scope`], starting from the last. #[inline] @@ -323,7 +324,7 @@ impl Scope<'_> { .iter() .rev() // Always search a Scope in reverse order .enumerate() - .find_map(|(i, (key, ..))| { + .find_map(|(i, key)| { if name == key { let index = len - 1 - i; Some((index, self.values[index].access_mode())) @@ -353,7 +354,7 @@ impl Scope<'_> { .iter() .rev() .enumerate() - .find(|(.., (key, ..))| name == key) + .find(|(.., key)| &name == key) .and_then(|(index, ..)| self.values[len - 1 - index].flatten_clone().try_cast()) } /// Check if the named entry in the [`Scope`] is constant. @@ -514,15 +515,9 @@ impl Scope<'_> { #[cfg(not(feature = "no_module"))] #[inline] pub(crate) fn add_entry_alias(&mut self, index: usize, alias: Identifier) -> &mut Self { - let (.., aliases) = self.names.get_mut(index).unwrap(); - match aliases { - None => { - let mut list = StaticVec::new_const(); - list.push(alias); - *aliases = Some(list.into()); - } - Some(aliases) if !aliases.iter().any(|a| a == &alias) => aliases.push(alias), - Some(_) => (), + let aliases = self.aliases.get_mut(index).unwrap(); + if aliases.is_empty() || !aliases.contains(&alias) { + aliases.push(alias); } self } @@ -533,20 +528,23 @@ impl Scope<'_> { pub fn clone_visible(&self) -> Self { let len = self.len(); - self.names.iter().rev().enumerate().fold( - Self::new(), - |mut entries, (index, (name, alias))| { - if !entries.names.iter().any(|(key, ..)| key == name) { + self.names + .iter() + .rev() + .enumerate() + .fold(Self::new(), |mut entries, (index, name)| { + if entries.names.is_empty() || !entries.names.contains(name) { let orig_value = &self.values[len - 1 - index]; + let alias = &self.aliases[len - 1 - index]; let mut value = orig_value.clone(); value.set_access_mode(orig_value.access_mode()); - entries.names.push((name.clone(), alias.clone())); + entries.names.push(name.clone()); entries.values.push(value); + entries.aliases.push(alias.clone()); } entries - }, - ) + }) } /// Get an iterator to entries in the [`Scope`]. #[inline] @@ -554,10 +552,8 @@ impl Scope<'_> { pub(crate) fn into_iter(self) -> impl Iterator)> { self.names .into_iter() - .zip(self.values.into_iter()) - .map(|((name, alias), value)| { - (name, value, alias.map(|a| a.to_vec()).unwrap_or_default()) - }) + .zip(self.values.into_iter().zip(self.aliases.into_iter())) + .map(|(name, (value, alias))| (name, value, alias)) } /// Get an iterator to entries in the [`Scope`]. /// Shared values are flatten-cloned. @@ -596,7 +592,7 @@ impl Scope<'_> { self.names .iter() .zip(self.values.iter()) - .map(|((name, ..), value)| (name.as_ref(), value.is_read_only(), value)) + .map(|(name, value)| (name.as_ref(), value.is_read_only(), value)) } /// Get a reverse iterator to entries in the [`Scope`]. /// Shared values are not expanded. @@ -606,7 +602,7 @@ impl Scope<'_> { .iter() .rev() .zip(self.values.iter().rev()) - .map(|((name, ..), value)| (name.as_ref(), value.is_read_only(), value)) + .map(|(name, value)| (name.as_ref(), value.is_read_only(), value)) } /// Remove a range of entries within the [`Scope`]. /// @@ -618,6 +614,7 @@ impl Scope<'_> { pub(crate) fn remove_range(&mut self, start: usize, len: usize) { self.values.drain(start..start + len).for_each(|_| {}); self.names.drain(start..start + len).for_each(|_| {}); + self.aliases.drain(start..start + len).for_each(|_| {}); } } diff --git a/tests/method_call.rs b/tests/method_call.rs index a65b511a..a9f2c4b7 100644 --- a/tests/method_call.rs +++ b/tests/method_call.rs @@ -2,23 +2,23 @@ use rhai::{Engine, EvalAltResult, INT}; +#[derive(Debug, Clone, Eq, PartialEq)] +struct TestStruct { + x: INT, +} + +impl TestStruct { + fn update(&mut self, n: INT) { + self.x += n; + } + + fn new() -> Self { + Self { x: 1 } + } +} + #[test] fn test_method_call() -> Result<(), Box> { - #[derive(Debug, Clone, Eq, PartialEq)] - struct TestStruct { - x: INT, - } - - impl TestStruct { - fn update(&mut self, n: INT) { - self.x += n; - } - - fn new() -> Self { - Self { x: 1 } - } - } - let mut engine = Engine::new(); engine @@ -47,3 +47,31 @@ fn test_method_call_style() -> Result<(), Box> { Ok(()) } + +#[cfg(not(feature = "no_optimize"))] +#[test] +fn test_method_call_with_full_optimization() -> Result<(), Box> { + let mut engine = Engine::new(); + + engine.set_optimization_level(rhai::OptimizationLevel::Full); + + engine + .register_fn("new_ts", || TestStruct::new()) + .register_fn("ymd", |_: INT, _: INT, _: INT| 42 as INT) + .register_fn("range", |_: &mut TestStruct, _: INT, _: INT| { + TestStruct::new() + }); + + assert_eq!( + engine.eval::( + " + let xs = new_ts(); + let ys = xs.range(ymd(2022, 2, 1), ymd(2022, 2, 2)); + ys + " + )?, + TestStruct::new() + ); + + Ok(()) +} diff --git a/tests/optimizer.rs b/tests/optimizer.rs index 886b4fff..351d91e5 100644 --- a/tests/optimizer.rs +++ b/tests/optimizer.rs @@ -84,7 +84,7 @@ fn test_optimizer_parse() -> Result<(), Box> { assert_eq!( format!("{:?}", ast), - r#"AST { body: [Var(("DECISION" @ 1:7, false @ 1:18, None), (Constant), 1:1), Expr(123 @ 1:51)] }"# + r#"AST { body: [Var(("DECISION" @ 1:7, false @ 1:18, None), CONSTANT, 1:1), Expr(123 @ 1:51)] }"# ); let ast = engine.compile("if 1 == 2 { 42 }")?;