diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01b165a7..c889e006 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,24 +18,25 @@ jobs: os: [ubuntu-latest] flags: - "" + - "--features debugging" - "--features metadata,serde,internals" - - "--features unchecked,serde,metadata,internals" - - "--features sync,serde,metadata,internals" - - "--features no_position,serde,metadata,internals" - - "--features no_optimize,serde,metadata,internals" - - "--features no_float,serde,metadata,internals" - - "--features f32_float,serde,metadata,internals" - - "--features decimal,serde,metadata,internals" + - "--features unchecked,serde,metadata,internals,debugging" + - "--features sync,serde,metadata,internals,debugging" + - "--features no_position,serde,metadata,internals,debugging" + - "--features no_optimize,serde,metadata,internals,debugging" + - "--features no_float,serde,metadata,internals,debugging" + - "--features f32_float,serde,metadata,internals,debugging" + - "--features decimal,serde,metadata,internals,debugging" - "--features no_float,decimal" - - "--tests --features only_i32,serde,metadata,internals" - - "--features only_i64,serde,metadata,internals" - - "--features no_index,serde,metadata,internals" - - "--features no_object,serde,metadata,internals" - - "--features no_function,serde,metadata,internals" - - "--features no_module,serde,metadata,internals" - - "--features no_closure,serde,metadata,internals" - - "--features unicode-xid-ident,serde,metadata,internals" - - "--features sync,no_function,no_float,no_position,no_optimize,no_module,no_closure,metadata,serde,unchecked" + - "--tests --features only_i32,serde,metadata,internals,debugging" + - "--features only_i64,serde,metadata,internals,debugging" + - "--features no_index,serde,metadata,internals,debugging" + - "--features no_object,serde,metadata,internals,debugging" + - "--features no_function,serde,metadata,internals,debugging" + - "--features no_module,serde,metadata,internals,debugging" + - "--features no_closure,serde,metadata,internals,debugging" + - "--features unicode-xid-ident,serde,metadata,internals,debugging" + - "--features sync,no_function,no_float,no_position,no_optimize,no_module,no_closure,metadata,serde,unchecked,debugging" - "--features no_function,no_float,no_position,no_index,no_object,no_optimize,no_module,no_closure,unchecked" toolchain: [stable] experimental: [false] diff --git a/.gitignore b/.gitignore index 9b226ad0..2d64e220 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ Cargo.lock benches/results before* after* +.rhai-repl-history.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 6987d52e..58c9de46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,36 @@ Rhai Release Notes ================== -Version 1.4.2 +Version 1.5.0 ============= +This version adds a debugging interface, which can be used to integrate a debugger. + Bug fixes --------- +* In `Scope::clone_visible`, constants are now properly cloned as constants. * Variables introduced inside `try` blocks are now properly cleaned up upon an exception. * Off-by-one error in character positions after a comment line is now fixed. +Script-breaking changes +----------------------- + +* For consistency, the `export` statement no longer exports multiple variables. + +New features +------------ + +* A debugging interface is added. +* A new bin tool, `rhai-dbg` (aka _The Rhai Debugger_), is added to showcase the debugging interface. +* A new package, `DebuggingPackage`, is added which contains the `stack_trace` function to get the current call stack anywhere in a script. + Enhancements ------------ * `rhai-repl` tool has a few more commands, such as `strict` to turn on/off _Strict Variables Mode_ and `optimize` to turn on/off script optimization. * Default features for dependencies (such as `ahash/std` and `num-traits/std`) are no longer required. +* The `no_module` feature now eliminates large sections of code via feature gates. Version 1.4.1 diff --git a/Cargo.toml b/Cargo.toml index 81d16837..2d687d30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,15 @@ num-traits = { version = "0.2", default-features = false } smartstring = { version = "0.2.8", 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 } +libm = { version = "0.2", default-features = false, optional = true } +core-error = { version = "0.0", default-features = false, features = ["alloc"], optional = true } +serde = { version = "1.0", default-features = false, features = ["derive", "alloc"], optional = true } +serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true } +unicode-xid = { version = "0.2", default-features = false, optional = true } +rust_decimal = { version = "1.16", default-features = false, features = ["maths"], optional = true } +rustyline = { version = "9", optional = true } + [dev-dependencies] serde_bytes = "0.11" @@ -44,6 +53,7 @@ no_module = [] # no modules internals = [] # expose internal data structures unicode-xid-ident = ["unicode-xid"] # allow Unicode Standard Annex #31 for identifiers. metadata = ["serde", "serde_json", "rhai_codegen/metadata", "smartstring/serde"] # enable exporting functions metadata +debugging = ["internals"] # enable debugging no_std = ["no-std-compat", "num-traits/libm", "core-error", "libm", "ahash/compile-time-rng"] @@ -51,54 +61,25 @@ no_std = ["no-std-compat", "num-traits/libm", "core-error", "libm", "ahash/compi wasm-bindgen = ["instant/wasm-bindgen"] stdweb = ["instant/stdweb"] +[[bin]] +name = "rhai-repl" +required-features = ["rustyline"] + +[[bin]] +name = "rhai-run" + +[[bin]] +name = "rhai-dbg" +required-features = ["debugging"] + [profile.release] lto = "fat" codegen-units = 1 #opt-level = "z" # optimize for size #panic = 'abort' # remove stack backtrace for no-std -[dependencies.no-std-compat] -version = "0.4" -default-features = false -features = ["alloc"] -optional = true - -[dependencies.libm] -version = "0.2" -default-features = false -optional = true - -[dependencies.core-error] -version = "0.0" -default-features = false -features = ["alloc"] -optional = true - -[dependencies.serde] -version = "1.0" -default-features = false -features = ["derive", "alloc"] -optional = true - -[dependencies.serde_json] -version = "1.0" -default-features = false -features = ["alloc"] -optional = true - -[dependencies.unicode-xid] -version = "0.2" -default-features = false -optional = true - -[dependencies.rust_decimal] -version = "1.16" -default-features = false -features = ["maths"] -optional = true - [target.'cfg(target_family = "wasm")'.dependencies] instant = { version = "0.1.10" } # WASM implementation of std::time::Instant [package.metadata.docs.rs] -features = ["metadata", "serde", "internals", "decimal"] # compiling for no-std +features = ["metadata", "serde", "internals", "decimal", "debugging"] diff --git a/README.md b/README.md index 85dac181..257072c6 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Standard features * 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). -* Easy custom API development via [plugins](https://rhai.rs/book/plugins/index.html) system powered by procedural macros. +* Easy custom API development via [plugins](https://rhai.rs/book/plugins) system powered by procedural macros. * [Function overloading](https://rhai.rs/book/language/overload.html) and [operator overloading](https://rhai.rs/book/rust/operators.html). * Dynamic dispatch via [function pointers](https://rhai.rs/book/language/fn-ptr.html) with additional support for [currying](https://rhai.rs/book/language/fn-curry.html). * [Closures](https://rhai.rs/book/language/fn-closure.html) (anonymous functions) that can capture shared values. @@ -51,6 +51,7 @@ Standard features * Organize code base with dynamically-loadable [modules](https://rhai.rs/book/language/modules.html), optionally [overriding the resolution process](https://rhai.rs/book/rust/modules/resolvers.html). * Serialization/deserialization support via [serde](https://crates.io/crates/serde) (requires the `serde` feature). * Support for [minimal builds](https://rhai.rs/book/start/builds/minimal.html) by excluding unneeded language [features](https://rhai.rs/book/start/features.html). +* A [debugging](https://rhai.rs/book/engine/debugging) interface. Protected against attacks diff --git a/examples/event_handler_js/main.rs b/examples/event_handler_js/main.rs index 770127ce..99118feb 100644 --- a/examples/event_handler_js/main.rs +++ b/examples/event_handler_js/main.rs @@ -17,24 +17,21 @@ struct Handler { } fn print_scope(scope: &Scope) { - scope - .iter_raw() - .enumerate() - .for_each(|(i, (name, constant, value))| { - #[cfg(not(feature = "no_closure"))] - let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; - #[cfg(feature = "no_closure")] - let value_is_shared = ""; + for (i, (name, constant, value)) in scope.iter_raw().enumerate() { + #[cfg(not(feature = "no_closure"))] + let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; + #[cfg(feature = "no_closure")] + let value_is_shared = ""; - println!( - "[{}] {}{}{} = {:?}", - i + 1, - if constant { "const " } else { "" }, - name, - value_is_shared, - *value.read_lock::().unwrap(), - ) - }); + println!( + "[{}] {}{}{} = {:?}", + i + 1, + if constant { "const " } else { "" }, + name, + value_is_shared, + *value.read_lock::().unwrap(), + ) + } println!(); } diff --git a/examples/event_handler_main/main.rs b/examples/event_handler_main/main.rs index 750d6742..76348974 100644 --- a/examples/event_handler_main/main.rs +++ b/examples/event_handler_main/main.rs @@ -13,24 +13,21 @@ struct Handler { } fn print_scope(scope: &Scope) { - scope - .iter_raw() - .enumerate() - .for_each(|(i, (name, constant, value))| { - #[cfg(not(feature = "no_closure"))] - let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; - #[cfg(feature = "no_closure")] - let value_is_shared = ""; + for (i, (name, constant, value)) in scope.iter_raw().enumerate() { + #[cfg(not(feature = "no_closure"))] + let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; + #[cfg(feature = "no_closure")] + let value_is_shared = ""; - println!( - "[{}] {}{}{} = {:?}", - i + 1, - if constant { "const " } else { "" }, - name, - value_is_shared, - *value.read_lock::().unwrap(), - ) - }); + println!( + "[{}] {}{}{} = {:?}", + i + 1, + if constant { "const " } else { "" }, + name, + value_is_shared, + *value.read_lock::().unwrap(), + ) + } println!(); } diff --git a/examples/event_handler_map/main.rs b/examples/event_handler_map/main.rs index 62e0a15f..7227fcc7 100644 --- a/examples/event_handler_map/main.rs +++ b/examples/event_handler_map/main.rs @@ -16,24 +16,21 @@ struct Handler { } fn print_scope(scope: &Scope) { - scope - .iter_raw() - .enumerate() - .for_each(|(i, (name, constant, value))| { - #[cfg(not(feature = "no_closure"))] - let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; - #[cfg(feature = "no_closure")] - let value_is_shared = ""; + for (i, (name, constant, value)) in scope.iter_raw().enumerate() { + #[cfg(not(feature = "no_closure"))] + let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; + #[cfg(feature = "no_closure")] + let value_is_shared = ""; - println!( - "[{}] {}{}{} = {:?}", - i + 1, - if constant { "const " } else { "" }, - name, - value_is_shared, - *value.read_lock::().unwrap(), - ) - }); + println!( + "[{}] {}{}{} = {:?}", + i + 1, + if constant { "const " } else { "" }, + name, + value_is_shared, + *value.read_lock::().unwrap(), + ) + } println!(); } diff --git a/src/api/call_fn.rs b/src/api/call_fn.rs index 94108484..e501c68d 100644 --- a/src/api/call_fn.rs +++ b/src/api/call_fn.rs @@ -154,7 +154,8 @@ impl Engine { arg_values: impl AsMut<[Dynamic]>, ) -> RhaiResult { let state = &mut EvalState::new(); - let global = &mut GlobalRuntimeState::new(); + let global = &mut GlobalRuntimeState::new(self); + let statements = ast.statements(); let orig_scope_len = scope.len(); diff --git a/src/api/custom_syntax.rs b/src/api/custom_syntax.rs index c35c49af..0411f3f0 100644 --- a/src/api/custom_syntax.rs +++ b/src/api/custom_syntax.rs @@ -72,7 +72,9 @@ impl Expression<'_> { #[must_use] pub fn get_string_value(&self) -> Option<&str> { match self.0 { - Expr::Variable(_, _, x) if x.1.is_none() => Some(x.2.as_str()), + #[cfg(not(feature = "no_module"))] + Expr::Variable(_, _, x) if x.1.is_some() => None, + Expr::Variable(_, _, x) => Some(x.2.as_str()), Expr::StringConstant(x, _) => Some(x.as_str()), _ => None, } diff --git a/src/api/eval.rs b/src/api/eval.rs index e90b6421..7e6eb83c 100644 --- a/src/api/eval.rs +++ b/src/api/eval.rs @@ -184,7 +184,7 @@ impl Engine { scope: &mut Scope, ast: &AST, ) -> RhaiResultOf { - let global = &mut GlobalRuntimeState::new(); + let global = &mut GlobalRuntimeState::new(self); let result = self.eval_ast_with_scope_raw(scope, global, ast, 0)?; diff --git a/src/api/events.rs b/src/api/events.rs index 7b3eb67b..6fb5227c 100644 --- a/src/api/events.rs +++ b/src/api/events.rs @@ -12,8 +12,7 @@ impl Engine { /// /// The callback function signature takes the following form: /// - /// > `Fn(name: &str, index: usize, context: &EvalContext)` - /// > ` -> Result, Box> + 'static` + /// > `Fn(name: &str, index: usize, context: &EvalContext) -> Result, Box>` /// /// where: /// * `index`: an offset from the bottom of the current [`Scope`][crate::Scope] that the @@ -64,7 +63,7 @@ impl Engine { self.resolve_var = Some(Box::new(callback)); self } - /// _(internals)_ Provide a callback that will be invoked during parsing to remap certain tokens. + /// _(internals)_ Register a callback that will be invoked during parsing to remap certain tokens. /// Exported under the `internals` feature only. /// /// # Callback Function Signature @@ -261,4 +260,23 @@ impl Engine { self.debug = Some(Box::new(callback)); self } + /// _(debugging)_ Register callbacks for debugging. + /// Exported under the `debugging` feature only. + #[cfg(feature = "debugging")] + #[inline(always)] + pub fn on_debugger( + &mut self, + init: impl Fn() -> Dynamic + SendSync + 'static, + callback: impl Fn( + &mut EvalContext, + crate::ast::ASTNode, + Option<&str>, + Position, + ) -> RhaiResultOf + + SendSync + + 'static, + ) -> &mut Self { + self.debugger = Some((Box::new(init), Box::new(callback))); + self + } } diff --git a/src/api/register.rs b/src/api/register.rs index b489a2b8..8dc9ce0c 100644 --- a/src/api/register.rs +++ b/src/api/register.rs @@ -1037,9 +1037,10 @@ impl Engine { signatures.extend(self.global_namespace().gen_fn_signatures()); - self.global_sub_modules.iter().for_each(|(name, m)| { + #[cfg(not(feature = "no_module"))] + for (name, m) in &self.global_sub_modules { signatures.extend(m.gen_fn_signatures().map(|f| format!("{}::{}", name, f))) - }); + } signatures.extend( self.global_modules diff --git a/src/api/run.rs b/src/api/run.rs index f5ec3c78..7c556064 100644 --- a/src/api/run.rs +++ b/src/api/run.rs @@ -44,8 +44,8 @@ impl Engine { /// Evaluate an [`AST`] with own scope, returning any error (if any). #[inline] pub fn run_ast_with_scope(&self, scope: &mut Scope, ast: &AST) -> RhaiResultOf<()> { - let global = &mut GlobalRuntimeState::new(); - let mut state = EvalState::new(); + let state = &mut EvalState::new(); + let global = &mut GlobalRuntimeState::new(self); global.source = ast.source_raw().clone(); #[cfg(not(feature = "no_module"))] @@ -64,7 +64,7 @@ impl Engine { } else { &lib }; - self.eval_global_statements(scope, global, &mut state, statements, lib, 0)?; + self.eval_global_statements(scope, global, state, statements, lib, 0)?; } Ok(()) } diff --git a/src/ast/ast.rs b/src/ast/ast.rs index 3a7b96b9..948df827 100644 --- a/src/ast/ast.rs +++ b/src/ast/ast.rs @@ -811,7 +811,7 @@ impl AsRef> for AST { /// _(internals)_ An [`AST`] node, consisting of either an [`Expr`] or a [`Stmt`]. /// Exported under the `internals` feature only. -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Clone, Copy, Hash)] pub enum ASTNode<'a> { /// A statement ([`Stmt`]). Stmt(&'a Stmt), @@ -831,6 +831,19 @@ impl<'a> From<&'a Expr> for ASTNode<'a> { } } +impl PartialEq for ASTNode<'_> { + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Stmt(x), Self::Stmt(y)) => std::ptr::eq(*x, *y), + (Self::Expr(x), Self::Expr(y)) => std::ptr::eq(*x, *y), + _ => false, + } + } +} + +impl Eq for ASTNode<'_> {} + impl ASTNode<'_> { /// Get the [`Position`] of this [`ASTNode`]. pub const fn position(&self) -> Position { diff --git a/src/ast/expr.rs b/src/ast/expr.rs index d2256c5b..72dea97c 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -3,7 +3,6 @@ use super::{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::module::Namespace; use crate::tokenizer::Token; use crate::types::dynamic::Union; use crate::{calc_fn_hash, Dynamic, FnPtr, Identifier, ImmutableString, Position, StaticVec, INT}; @@ -33,6 +32,16 @@ pub struct BinaryExpr { pub rhs: Expr, } +impl From<(Expr, Expr)> for BinaryExpr { + #[inline(always)] + fn from(value: (Expr, Expr)) -> Self { + Self { + lhs: value.0, + rhs: value.1, + } + } +} + /// _(internals)_ A custom syntax expression. /// Exported under the `internals` feature only. #[derive(Debug, Clone, Hash)] @@ -159,7 +168,10 @@ impl FnCallHashes { #[derive(Debug, Clone, Default, Hash)] pub struct FnCallExpr { /// Namespace of the function, if any. - pub namespace: Option, + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] + pub namespace: Option, /// Function name. pub name: Identifier, /// Pre-calculated hashes. @@ -183,10 +195,15 @@ pub struct FnCallExpr { impl FnCallExpr { /// Does this function call contain a qualified namespace? + /// + /// Always `false` under `no_module`. #[inline(always)] #[must_use] pub const fn is_qualified(&self) -> bool { - self.namespace.is_some() + #[cfg(not(feature = "no_module"))] + return self.namespace.is_some(); + #[cfg(feature = "no_module")] + return false; } /// Convert this into an [`Expr::FnCall`]. #[inline(always)] @@ -358,15 +375,18 @@ pub enum Expr { Variable( Option, Position, - Box<(Option, Option<(Namespace, u64)>, Identifier)>, + #[cfg(not(feature = "no_module"))] + Box<( + Option, + Option<(crate::module::Namespace, u64)>, + Identifier, + )>, + #[cfg(feature = "no_module")] Box<(Option, (), Identifier)>, ), /// Property access - ((getter, hash), (setter, hash), prop) Property( - Box<( - (Identifier, u64), - (Identifier, u64), - (ImmutableString, Position), - )>, + Box<((Identifier, u64), (Identifier, u64), ImmutableString)>, + Position, ), /// Stack slot for function calls. See [`FnCallExpr`] for more details. /// @@ -378,9 +398,9 @@ pub enum Expr { Stmt(Box), /// func `(` expr `,` ... `)` FnCall(Box, Position), - /// lhs `.` rhs - bool variable is a dummy + /// lhs `.` rhs - boolean variable is a dummy Dot(Box, bool, Position), - /// expr `[` expr `]` - boolean indicates whether the dotting/indexing chain stops + /// lhs `[` rhs `]` - boolean indicates whether the dotting/indexing chain stops Index(Box, bool, Position), /// lhs `&&` rhs And(Box, Position), @@ -427,6 +447,8 @@ impl fmt::Debug for Expr { } Self::Variable(i, _, x) => { f.write_str("Variable(")?; + + #[cfg(not(feature = "no_module"))] if let Some((_, ref namespace)) = x.1 { write!(f, "{}{}", namespace, Token::DoubleColon.literal_syntax())? } @@ -436,7 +458,7 @@ impl fmt::Debug for Expr { } f.write_str(")") } - Self::Property(x) => write!(f, "Property({})", (x.2).0), + Self::Property(x, _) => write!(f, "Property({})", x.2), Self::Stack(x, _) => write!(f, "StackSlot({})", x), Self::Stmt(x) => { f.write_str("ExprStmtBlock")?; @@ -444,6 +466,7 @@ impl fmt::Debug for Expr { } Self::FnCall(x, _) => { let mut ff = f.debug_struct("FnCall"); + #[cfg(not(feature = "no_module"))] x.namespace.as_ref().map(|ns| ff.field("namespace", ns)); ff.field("name", &x.name) .field("hash", &x.hashes) @@ -595,6 +618,7 @@ impl Expr { Union::FnPtr(f, _, _) if !f.is_curried() => Self::FnCall( FnCallExpr { + #[cfg(not(feature = "no_module"))] namespace: None, name: KEYWORD_FN_PTR.into(), hashes: calc_fn_hash(f.fn_name(), 1).into(), @@ -610,20 +634,32 @@ impl Expr { } } /// Is the expression a simple variable access? + /// + /// `non_qualified` is ignored under `no_module`. #[inline] #[must_use] pub(crate) const fn is_variable_access(&self, non_qualified: bool) -> bool { + let _non_qualified = non_qualified; + match self { - Self::Variable(_, _, x) => !non_qualified || x.1.is_none(), + #[cfg(not(feature = "no_module"))] + Self::Variable(_, _, x) if _non_qualified && x.1.is_some() => false, + Self::Variable(_, _, _) => true, _ => false, } } /// Return the variable name if the expression a simple variable access. + /// + /// `non_qualified` is ignored under `no_module`. #[inline] #[must_use] pub(crate) fn get_variable_name(&self, non_qualified: bool) -> Option<&str> { + let _non_qualified = non_qualified; + match self { - Self::Variable(_, _, x) if !non_qualified || x.1.is_none() => Some(x.2.as_str()), + #[cfg(not(feature = "no_module"))] + Self::Variable(_, _, x) if _non_qualified && x.1.is_some() => None, + Self::Variable(_, _, x) => Some(x.2.as_str()), _ => None, } } @@ -648,9 +684,9 @@ impl Expr { | Self::FnCall(_, pos) | Self::Index(_, _, pos) | Self::Custom(_, pos) - | Self::InterpolatedString(_, pos) => *pos, + | Self::InterpolatedString(_, pos) + | Self::Property(_, pos) => *pos, - Self::Property(x) => (x.2).1, Self::Stmt(x) => x.position(), Self::And(x, _) | Self::Or(x, _) | Self::Dot(x, _, _) => x.lhs.position(), @@ -679,9 +715,9 @@ impl Expr { | Self::Stack(_, pos) | Self::FnCall(_, pos) | Self::Custom(_, pos) - | Self::InterpolatedString(_, pos) => *pos = new_pos, + | Self::InterpolatedString(_, pos) + | Self::Property(_, pos) => *pos = new_pos, - Self::Property(x) => (x.2).1 = new_pos, Self::Stmt(x) => x.set_position(new_pos), } @@ -781,7 +817,7 @@ impl Expr { _ => false, }, - Self::Property(_) => match token { + Self::Property(_, _) => match token { #[cfg(not(feature = "no_index"))] Token::LeftBracket => true, Token::LeftParen => true, diff --git a/src/ast/flags.rs b/src/ast/flags.rs index 0b548135..15b664e2 100644 --- a/src/ast/flags.rs +++ b/src/ast/flags.rs @@ -13,8 +13,12 @@ pub enum FnAccess { Private, } -/// A type that holds a configuration option with bit-flags. -/// Exported under the `internals` feature only. +/// 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); @@ -120,19 +124,20 @@ pub mod AST_OPTION_FLAGS { /// _(internals)_ The [`AST`][crate::AST] node is constant. /// Exported under the `internals` feature only. pub const AST_OPTION_CONSTANT: OptionFlags = OptionFlags(0b0000_0001); - /// _(internals)_ The [`AST`][crate::AST] node is public. + /// _(internals)_ The [`AST`][crate::AST] node is exported to the outside (i.e. public). /// Exported under the `internals` feature only. - pub const AST_OPTION_PUBLIC: OptionFlags = OptionFlags(0b0000_0010); - /// _(internals)_ The [`AST`][crate::AST] node is in negated mode. + 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_OUT: OptionFlags = OptionFlags(0b0000_1000); + 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_PUBLIC.0 | AST_OPTION_NEGATED.0 | AST_OPTION_BREAK_OUT.0, + AST_OPTION_CONSTANT.0 | AST_OPTION_EXPORTED.0 | AST_OPTION_NEGATED.0 | AST_OPTION_BREAK.0, ); impl std::fmt::Debug for OptionFlags { @@ -158,9 +163,9 @@ pub mod AST_OPTION_FLAGS { f.write_str("(")?; write_option(self, f, num_flags, AST_OPTION_CONSTANT, "Constant")?; - write_option(self, f, num_flags, AST_OPTION_PUBLIC, "Public")?; + 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_OUT, "Break")?; + write_option(self, f, num_flags, AST_OPTION_BREAK, "Break")?; f.write_str(")")?; Ok(()) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3b269a71..aebf79fc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -13,7 +13,7 @@ pub use flags::{FnAccess, OptionFlags, AST_OPTION_FLAGS}; pub use ident::Ident; #[cfg(not(feature = "no_function"))] pub use script_fn::{ScriptFnDef, ScriptFnMetadata}; -pub use stmt::{OpAssignment, Stmt, StmtBlock}; +pub use stmt::{ConditionalStmtBlock, OpAssignment, Stmt, StmtBlock, SwitchCases, TryCatchBlock}; #[cfg(not(feature = "no_float"))] pub use expr::FloatWrapper; diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index 136356a1..f9f6717c 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -1,6 +1,6 @@ //! Module defining script statements. -use super::{ASTNode, Expr, FnCallExpr, Ident, OptionFlags, AST_OPTION_FLAGS}; +use super::{ASTNode, BinaryExpr, Expr, FnCallExpr, Ident, OptionFlags, AST_OPTION_FLAGS::*}; use crate::engine::KEYWORD_EVAL; use crate::tokenizer::Token; use crate::{calc_fn_hash, Position, StaticVec, INT}; @@ -23,6 +23,8 @@ pub struct OpAssignment<'a> { /// Hash of the underlying operator call (for fallback). pub hash_op: u64, /// Op-assignment operator. + pub op_assign: &'a str, + /// Underlying operator. pub op: &'a str, } @@ -51,7 +53,8 @@ impl OpAssignment<'_> { Self { hash_op_assign: calc_fn_hash(op.literal_syntax(), 2), hash_op: calc_fn_hash(op_raw, 2), - op: op.literal_syntax(), + op_assign: op.literal_syntax(), + op: op_raw, } } /// Create a new [`OpAssignment`] from a base operator. @@ -76,6 +79,58 @@ impl OpAssignment<'_> { } } +/// A statements block with an optional condition. +#[derive(Debug, Clone, Hash)] +pub struct ConditionalStmtBlock { + /// Optional condition. + pub condition: Option, + /// Statements block. + pub statements: StmtBlock, +} + +impl> From<(Option, B)> for ConditionalStmtBlock { + #[inline(always)] + fn from(value: (Option, B)) -> Self { + Self { + condition: value.0, + statements: value.1.into(), + } + } +} + +impl ConditionalStmtBlock { + /// Does the condition exist? + #[inline(always)] + #[must_use] + pub const fn has_condition(&self) -> bool { + self.condition.is_some() + } +} + +/// _(internals)_ A type containing all cases for a `switch` statement. +/// Exported under the `internals` feature only. +#[derive(Debug, Clone, Hash)] +pub struct SwitchCases { + /// Dictionary mapping value hashes to [`ConditionalStmtBlock`]'s. + pub cases: BTreeMap>, + /// Statements block for the default case (there can be no condition for the default case). + pub def_case: StmtBlock, + /// List of range cases. + pub ranges: StaticVec<(INT, INT, bool, ConditionalStmtBlock)>, +} + +/// _(internals)_ A `try-catch` block. +/// Exported under the `internals` feature only. +#[derive(Debug, Clone, Hash)] +pub struct TryCatchBlock { + /// `try` block. + pub try_block: StmtBlock, + /// `catch` variable, if any. + pub catch_var: Option, + /// `catch` block. + pub catch_block: StmtBlock, +} + /// _(internals)_ A scoped block of statements. /// Exported under the `internals` feature only. #[derive(Clone, Hash, Default)] @@ -157,6 +212,20 @@ impl DerefMut for StmtBlock { } } +impl AsRef<[Stmt]> for StmtBlock { + #[inline(always)] + fn as_ref(&self) -> &[Stmt] { + &self.0 + } +} + +impl AsMut<[Stmt]> for StmtBlock { + #[inline(always)] + fn as_mut(&mut self) -> &mut [Stmt] { + &mut self.0 + } +} + impl fmt::Debug for StmtBlock { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("Block")?; @@ -211,15 +280,7 @@ pub enum Stmt { /// 0) Hash table for (condition, block) /// 1) Default block /// 2) List of ranges: (start, end, inclusive, condition, statement) - Switch( - Expr, - Box<( - BTreeMap, StmtBlock)>>, - StmtBlock, - StaticVec<(INT, INT, bool, Option, StmtBlock)>, - )>, - Position, - ), + Switch(Expr, Box, Position), /// `while` expr `{` stmt `}` | `loop` `{` stmt `}` /// /// If the guard expression is [`UNIT`][Expr::Unit], then it is a `loop` statement. @@ -228,8 +289,8 @@ pub enum Stmt { /// /// ### Option Flags /// - /// * [`AST_OPTION_NONE`][AST_OPTION_FLAGS::AST_OPTION_NONE] = `while` - /// * [`AST_OPTION_NEGATED`][AST_OPTION_FLAGS::AST_OPTION_NEGATED] = `until` + /// * [`AST_OPTION_NONE`] = `while` + /// * [`AST_OPTION_NEGATED`] = `until` Do(Box, Expr, OptionFlags, Position), /// `for` `(` id `,` counter `)` `in` expr `{` stmt `}` For(Expr, Box<(Ident, Option, StmtBlock)>, Position), @@ -237,11 +298,11 @@ pub enum Stmt { /// /// ### Option Flags /// - /// * [`AST_OPTION_PUBLIC`][AST_OPTION_FLAGS::AST_OPTION_PUBLIC] = `export` - /// * [`AST_OPTION_CONSTANT`][AST_OPTION_FLAGS::AST_OPTION_CONSTANT] = `const` + /// * [`AST_OPTION_EXPORTED`] = `export` + /// * [`AST_OPTION_CONSTANT`] = `const` Var(Expr, Box, OptionFlags, Position), /// expr op`=` expr - Assignment(Box<(Expr, Option>, Expr)>, Position), + Assignment(Box<(Option>, BinaryExpr)>, Position), /// func `(` expr `,` ... `)` /// /// Note - this is a duplicate of [`Expr::FnCall`] to cover the very common pattern of a single @@ -250,33 +311,33 @@ pub enum Stmt { /// `{` stmt`;` ... `}` Block(Box<[Stmt]>, Position), /// `try` `{` stmt; ... `}` `catch` `(` var `)` `{` stmt; ... `}` - TryCatch(Box<(StmtBlock, Option, StmtBlock)>, Position), + TryCatch(Box, Position), /// [expression][Expr] Expr(Expr), /// `continue`/`break` /// /// ### Option Flags /// - /// * [`AST_OPTION_NONE`][AST_OPTION_FLAGS::AST_OPTION_NONE] = `continue` - /// * [`AST_OPTION_BREAK_OUT`][AST_OPTION_FLAGS::AST_OPTION_BREAK_OUT] = `break` + /// * [`AST_OPTION_NONE`] = `continue` + /// * [`AST_OPTION_BREAK`] = `break` BreakLoop(OptionFlags, Position), /// `return`/`throw` /// /// ### Option Flags /// - /// * [`AST_OPTION_NONE`][AST_OPTION_FLAGS::AST_OPTION_NONE] = `return` - /// * [`AST_OPTION_BREAK_OUT`][AST_OPTION_FLAGS::AST_OPTION_BREAK_OUT] = `throw` + /// * [`AST_OPTION_NONE`] = `return` + /// * [`AST_OPTION_BREAK`] = `throw` Return(OptionFlags, Option, Position), /// `import` expr `as` var /// /// Not available under `no_module`. #[cfg(not(feature = "no_module"))] Import(Expr, Option>, Position), - /// `export` var `as` var `,` ... + /// `export` var `as` var /// /// Not available under `no_module`. #[cfg(not(feature = "no_module"))] - Export(Box<[(Ident, Ident)]>, Position), + Export(Box<(Ident, Ident)>, Position), /// Convert a variable to shared. /// /// Not available under `no_closure`. @@ -440,26 +501,26 @@ impl Stmt { Self::Expr(expr) => expr.is_pure(), Self::If(condition, x, _) => { condition.is_pure() - && (x.0).0.iter().all(Stmt::is_pure) - && (x.1).0.iter().all(Stmt::is_pure) + && x.0.iter().all(Stmt::is_pure) + && x.1.iter().all(Stmt::is_pure) } Self::Switch(expr, x, _) => { expr.is_pure() - && x.0.values().all(|block| { - block.0.as_ref().map(Expr::is_pure).unwrap_or(true) - && (block.1).0.iter().all(Stmt::is_pure) + && x.cases.values().all(|block| { + block.condition.as_ref().map(Expr::is_pure).unwrap_or(true) + && block.statements.iter().all(Stmt::is_pure) }) - && (x.2).iter().all(|(_, _, _, condition, stmt)| { - condition.as_ref().map(Expr::is_pure).unwrap_or(true) - && stmt.0.iter().all(Stmt::is_pure) + && x.ranges.iter().all(|(_, _, _, block)| { + block.condition.as_ref().map(Expr::is_pure).unwrap_or(true) + && block.statements.iter().all(Stmt::is_pure) }) - && (x.1).0.iter().all(Stmt::is_pure) + && x.def_case.iter().all(Stmt::is_pure) } // Loops that exit can be pure because it can never be infinite. Self::While(Expr::BoolConstant(false, _), _, _) => true, Self::Do(body, Expr::BoolConstant(x, _), options, _) - if *x == options.contains(AST_OPTION_FLAGS::AST_OPTION_NEGATED) => + if *x == options.contains(AST_OPTION_NEGATED) => { body.iter().all(Stmt::is_pure) } @@ -469,13 +530,13 @@ impl Stmt { // For loops can be pure because if the iterable is pure, it is finite, // so infinite loops can never occur. - Self::For(iterable, x, _) => iterable.is_pure() && (x.2).0.iter().all(Stmt::is_pure), + Self::For(iterable, x, _) => iterable.is_pure() && x.2.iter().all(Stmt::is_pure), Self::Var(_, _, _, _) | Self::Assignment(_, _) | Self::FnCall(_, _) => false, Self::Block(block, _) => block.iter().all(|stmt| stmt.is_pure()), Self::BreakLoop(_, _) | Self::Return(_, _, _) => false, Self::TryCatch(x, _) => { - (x.0).0.iter().all(Stmt::is_pure) && (x.2).0.iter().all(Stmt::is_pure) + x.try_block.iter().all(Stmt::is_pure) && x.catch_block.iter().all(Stmt::is_pure) } #[cfg(not(feature = "no_module"))] @@ -571,12 +632,12 @@ impl Stmt { if !e.walk(path, on_node) { return false; } - for s in &(x.0).0 { + for s in x.0.iter() { if !s.walk(path, on_node) { return false; } } - for s in &(x.1).0 { + for s in x.1.iter() { if !s.walk(path, on_node) { return false; } @@ -586,27 +647,37 @@ impl Stmt { if !e.walk(path, on_node) { return false; } - for b in x.0.values() { - if !b.0.as_ref().map(|e| e.walk(path, on_node)).unwrap_or(true) { + for b in x.cases.values() { + if !b + .condition + .as_ref() + .map(|e| e.walk(path, on_node)) + .unwrap_or(true) + { return false; } - for s in &(b.1).0 { + for s in b.statements.iter() { if !s.walk(path, on_node) { return false; } } } - for (_, _, _, c, stmt) in &x.2 { - if !c.as_ref().map(|e| e.walk(path, on_node)).unwrap_or(true) { + for (_, _, _, b) in &x.ranges { + if !b + .condition + .as_ref() + .map(|e| e.walk(path, on_node)) + .unwrap_or(true) + { return false; } - for s in &stmt.0 { + for s in b.statements.iter() { if !s.walk(path, on_node) { return false; } } } - for s in &(x.1).0 { + for s in x.def_case.iter() { if !s.walk(path, on_node) { return false; } @@ -626,17 +697,17 @@ impl Stmt { if !e.walk(path, on_node) { return false; } - for s in &(x.2).0 { + for s in x.2.iter() { if !s.walk(path, on_node) { return false; } } } Self::Assignment(x, _) => { - if !x.0.walk(path, on_node) { + if !x.1.lhs.walk(path, on_node) { return false; } - if !x.2.walk(path, on_node) { + if !x.1.rhs.walk(path, on_node) { return false; } } @@ -655,12 +726,12 @@ impl Stmt { } } Self::TryCatch(x, _) => { - for s in &(x.0).0 { + for s in x.try_block.iter() { if !s.walk(path, on_node) { return false; } } - for s in &(x.2).0 { + for s in x.catch_block.iter() { if !s.walk(path, on_node) { return false; } diff --git a/src/bin/README.md b/src/bin/README.md index a0afbd8f..e0648866 100644 --- a/src/bin/README.md +++ b/src/bin/README.md @@ -3,10 +3,38 @@ Rhai Tools Tools for running Rhai scripts. +| Tool | Required feature(s) | Description | +| -------------------------------------------------------------------------------- | :-----------------: | --------------------------------------------------- | +| [`rhai-run`](https://github.com/rhaiscript/rhai/blob/main/src/bin/rhai-run.rs) | | runs each filename passed to it as a Rhai script | +| [`rhai-repl`](https://github.com/rhaiscript/rhai/blob/main/src/bin/rhai-repl.rs) | `rustyline` | simple REPL that interactively evaluates statements | +| [`rhai-dbg`](https://github.com/rhaiscript/rhai/blob/main/src/bin/rhai-dbg.rs) | `debugging` | the _Rhai Debugger_ | + How to Run ---------- -```bash +```sh cargo run --bin sample_app_to_run ``` + +or with required features + +```sh +cargo run --bin sample_app_to_run --features feature1,feature2,feature3 +``` + + +How to Install +-------------- + +To install these all tools (with [`decimal`] and [`metadata`] support), use the following command: + +```sh +cargo install --path . --bins --features decimal,metadata,debugging,rustyline +``` + +or specifically: + +```sh +cargo install --path . --bin rhai-run --features decimal,metadata,debugging,rustyline +``` diff --git a/src/bin/rhai-dbg.rs b/src/bin/rhai-dbg.rs new file mode 100644 index 00000000..a506b640 --- /dev/null +++ b/src/bin/rhai-dbg.rs @@ -0,0 +1,543 @@ +use rhai::debugger::DebuggerCommand; +use rhai::{Dynamic, Engine, EvalAltResult, ImmutableString, Position, Scope}; + +use std::{ + env, + fs::File, + io::{stdin, stdout, Read, Write}, + path::Path, + process::exit, +}; + +/// Pretty-print source line. +fn print_source(lines: &[String], pos: Position, offset: usize) { + let line_no = if lines.len() > 1 { + if pos.is_none() { + "".to_string() + } else { + format!("{}: ", pos.line().unwrap()) + } + } else { + "".to_string() + }; + + // Print error position + if pos.is_none() { + // No position + println!(); + } else { + // Specific position - print line text + println!("{}{}", line_no, lines[pos.line().unwrap() - 1]); + + // Display position marker + if let Some(pos) = pos.position() { + println!("{0:>1$}", "^", line_no.len() + pos + offset); + } + } +} + +/// Pretty-print error. +fn print_error(input: &str, mut err: EvalAltResult) { + let lines: Vec<_> = input.trim().split('\n').collect(); + let pos = err.take_position(); + + let line_no = if lines.len() > 1 { + if pos.is_none() { + "".to_string() + } else { + format!("{}: ", pos.line().unwrap()) + } + } else { + "".to_string() + }; + + // Print error position + if pos.is_none() { + // No position + println!("{}", err); + } else { + // Specific position - print line text + println!("{}{}", line_no, lines[pos.line().unwrap() - 1]); + + // Display position marker + println!( + "{0:>1$} {2}", + "^", + line_no.len() + pos.position().unwrap(), + err + ); + } +} + +/// Print debug help. +fn print_debug_help() { + println!("help => print this help"); + println!("quit, exit, kill => quit"); + println!("scope => print the scope"); + println!("print => print all variables de-duplicated"); + println!("print => print the current value of a variable"); + #[cfg(not(feature = "no_module"))] + println!("imports => print all imported modules"); + println!("node => print the current AST node"); + println!("backtrace => print the current call-stack"); + println!("breakpoints => print all break-points"); + println!("enable => enable a break-point"); + println!("disable => disable a break-point"); + println!("delete => delete a break-point"); + println!("clear => delete all break-points"); + #[cfg(not(feature = "no_position"))] + println!("break => set a new break-point at the current position"); + #[cfg(not(feature = "no_position"))] + println!("break => set a new break-point at a line number"); + #[cfg(not(feature = "no_object"))] + println!("break . => set a new break-point for a property access"); + println!("break => set a new break-point for a function call"); + println!( + "break <#args> => set a new break-point for a function call with #args arguments" + ); + println!("throw [message] => throw an exception (message optional)"); + println!("run => restart the script evaluation from beginning"); + println!("step => go to the next expression, diving into functions"); + println!("over => go to the next expression, skipping oer functions"); + println!("next => go to the next statement, skipping over functions"); + println!("continue => continue normal execution"); + println!(); +} + +/// Display the scope. +fn print_scope(scope: &Scope, dedup: bool) { + let flattened_clone; + let scope = if dedup { + flattened_clone = scope.clone_visible(); + &flattened_clone + } else { + scope + }; + + for (i, (name, constant, value)) in scope.iter_raw().enumerate() { + #[cfg(not(feature = "no_closure"))] + let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; + #[cfg(feature = "no_closure")] + let value_is_shared = ""; + + if dedup { + println!( + "{}{}{} = {:?}", + if constant { "const " } else { "" }, + name, + value_is_shared, + *value.read_lock::().unwrap(), + ); + } else { + println!( + "[{}] {}{}{} = {:?}", + i + 1, + if constant { "const " } else { "" }, + name, + value_is_shared, + *value.read_lock::().unwrap(), + ); + } + } + + println!(); +} + +fn main() { + let title = format!("Rhai Debugger (version {})", env!("CARGO_PKG_VERSION")); + println!("{}", title); + println!("{0:=<1$}", "", title.len()); + + // Initialize scripting engine + let mut engine = Engine::new(); + + let mut script = String::new(); + let main_ast; + + { + // Load init scripts + if let Some(filename) = env::args().skip(1).next() { + let filename = match Path::new(&filename).canonicalize() { + Err(err) => { + eprintln!("Error script file path: {}\n{}", filename, err); + exit(1); + } + Ok(f) => { + match f.strip_prefix(std::env::current_dir().unwrap().canonicalize().unwrap()) { + Ok(f) => f.into(), + _ => f, + } + } + }; + + let mut f = match File::open(&filename) { + Err(err) => { + eprintln!( + "Error reading script file: {}\n{}", + filename.to_string_lossy(), + err + ); + exit(1); + } + Ok(f) => f, + }; + + if let Err(err) = f.read_to_string(&mut script) { + println!( + "Error reading script file: {}\n{}", + filename.to_string_lossy(), + err + ); + exit(1); + } + + let script = if script.starts_with("#!") { + // Skip shebang + &script[script.find('\n').unwrap_or(0)..] + } else { + &script[..] + }; + + main_ast = match engine + .compile(&script) + .map_err(Into::>::into) + { + Err(err) => { + print_error(&script, *err); + exit(1); + } + Ok(ast) => ast, + }; + + println!("Script '{}' loaded.", filename.to_string_lossy()); + println!(); + } else { + eprintln!("No script file specified."); + exit(1); + } + } + + // Hook up debugger + let lines: Vec<_> = script.trim().split('\n').map(|s| s.to_string()).collect(); + + engine.on_debugger( + // Store the current source in the debugger state + || "".into(), + // Main debugging interface + move |context, node, source, pos| { + { + let current_source = &mut *context + .global_runtime_state_mut() + .debugger + .state_mut() + .write_lock::() + .unwrap(); + + let src = source.unwrap_or(""); + + // Check source + if src != current_source { + println!(">>> Source => {}", source.unwrap_or("main script")); + *current_source = src.into(); + } + + if !src.is_empty() { + // Print just a line number for imported modules + println!("{} @ {:?}", src, pos); + } else { + // Print the current source line + print_source(&lines, pos, 0); + } + } + + // Read stdin for commands + let mut input = String::new(); + + loop { + print!("rhai-dbg> "); + stdout().flush().expect("couldn't flush stdout"); + + input.clear(); + + match stdin().read_line(&mut input) { + Ok(0) => break Ok(DebuggerCommand::Continue), + Ok(_) => match input + .trim() + .split_whitespace() + .collect::>() + .as_slice() + { + ["help", ..] => print_debug_help(), + ["exit", ..] | ["quit", ..] | ["kill", ..] => { + println!("Script terminated. Bye!"); + exit(0); + } + ["node", ..] => { + println!("{:?} {}@{:?}", node, source.unwrap_or_default(), pos); + println!(); + } + ["continue", ..] => break Ok(DebuggerCommand::Continue), + [] | ["step", ..] => break Ok(DebuggerCommand::StepInto), + ["over", ..] => break Ok(DebuggerCommand::StepOver), + ["next", ..] => break Ok(DebuggerCommand::Next), + ["scope", ..] => print_scope(context.scope(), false), + ["print", var_name, ..] => { + if let Some(value) = context.scope().get_value::(var_name) { + if value.is::<()>() { + println!("=> ()"); + } else { + println!("=> {}", value); + } + } else { + eprintln!("Variable not found: {}", var_name); + } + } + ["print", ..] => print_scope(context.scope(), true), + #[cfg(not(feature = "no_module"))] + ["imports", ..] => { + for (i, (name, module)) in context + .global_runtime_state() + .scan_imports_raw() + .enumerate() + { + println!( + "[{}] {} = {}", + i + 1, + name, + module.id().unwrap_or("") + ); + } + + println!(); + } + #[cfg(not(feature = "no_function"))] + ["backtrace", ..] => { + for frame in context + .global_runtime_state() + .debugger + .call_stack() + .iter() + .rev() + { + println!("{}", frame) + } + } + ["clear", ..] => { + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .clear(); + println!("All break-points cleared."); + } + ["breakpoints", ..] => Iterator::for_each( + context + .global_runtime_state() + .debugger + .break_points() + .iter() + .enumerate(), + |(i, bp)| match bp { + #[cfg(not(feature = "no_position"))] + rhai::debugger::BreakPoint::AtPosition { pos, .. } => { + let line_num = format!("[{}] line ", i + 1); + print!("{}", line_num); + print_source(&lines, *pos, line_num.len()); + } + _ => println!("[{}] {}", i + 1, bp), + }, + ), + ["enable", n, ..] => { + if let Ok(n) = n.parse::() { + let range = 1..=context + .global_runtime_state_mut() + .debugger + .break_points() + .len(); + if range.contains(&n) { + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .get_mut(n - 1) + .unwrap() + .enable(true); + println!("Break-point #{} enabled.", n) + } else { + eprintln!("Invalid break-point: {}", n); + } + } else { + eprintln!("Invalid break-point: '{}'", n); + } + } + ["disable", n, ..] => { + if let Ok(n) = n.parse::() { + let range = 1..=context + .global_runtime_state_mut() + .debugger + .break_points() + .len(); + if range.contains(&n) { + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .get_mut(n - 1) + .unwrap() + .enable(false); + println!("Break-point #{} disabled.", n) + } else { + eprintln!("Invalid break-point: {}", n); + } + } else { + eprintln!("Invalid break-point: '{}'", n); + } + } + ["delete", n, ..] => { + if let Ok(n) = n.parse::() { + let range = 1..=context + .global_runtime_state_mut() + .debugger + .break_points() + .len(); + if range.contains(&n) { + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .remove(n - 1); + println!("Break-point #{} deleted.", n) + } else { + eprintln!("Invalid break-point: {}", n); + } + } else { + eprintln!("Invalid break-point: '{}'", n); + } + } + ["break", fn_name, args, ..] => { + if let Ok(args) = args.parse::() { + let bp = rhai::debugger::BreakPoint::AtFunctionCall { + name: fn_name.trim().into(), + args, + enabled: true, + }; + println!("Break-point added for {}", bp); + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .push(bp); + } else { + eprintln!("Invalid number of arguments: '{}'", args); + } + } + // Property name + #[cfg(not(feature = "no_object"))] + ["break", param] if param.starts_with('.') && param.len() > 1 => { + let bp = rhai::debugger::BreakPoint::AtProperty { + name: param[1..].into(), + enabled: true, + }; + println!("Break-point added for {}", bp); + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .push(bp); + } + // Numeric parameter + #[cfg(not(feature = "no_position"))] + ["break", param] if param.parse::().is_ok() => { + let n = param.parse::().unwrap(); + let range = if source.is_none() { + 1..=lines.len() + } else { + 1..=(u16::MAX as usize) + }; + + if range.contains(&n) { + let bp = rhai::debugger::BreakPoint::AtPosition { + source: source.unwrap_or("").into(), + pos: Position::new(n as u16, 0), + enabled: true, + }; + println!("Break-point added {}", bp); + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .push(bp); + } else { + eprintln!("Invalid line number: {}", n); + } + } + // Function name parameter + ["break", param] => { + let bp = rhai::debugger::BreakPoint::AtFunctionName { + name: param.trim().into(), + enabled: true, + }; + println!("Break-point added for {}", bp); + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .push(bp); + } + #[cfg(not(feature = "no_position"))] + ["break", ..] => { + let bp = rhai::debugger::BreakPoint::AtPosition { + source: source.unwrap_or("").into(), + pos, + enabled: true, + }; + println!("Break-point added {}", bp); + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .push(bp); + } + ["throw"] => { + break Err(EvalAltResult::ErrorRuntime(Dynamic::UNIT, pos).into()) + } + ["throw", _msg, ..] => { + let msg = input.trim().splitn(2, ' ').skip(1).next().unwrap_or(""); + break Err(EvalAltResult::ErrorRuntime(msg.trim().into(), pos).into()); + } + ["run", ..] => { + println!("Restarting script..."); + break Err(EvalAltResult::ErrorTerminated(Dynamic::UNIT, pos).into()); + } + [cmd, ..] => eprintln!("Invalid debugger command: '{}'", cmd), + }, + Err(err) => panic!("input error: {}", err), + } + } + }, + ); + + // Set a file module resolver without caching + #[cfg(not(feature = "no_module"))] + #[cfg(not(feature = "no_std"))] + { + let mut resolver = rhai::module_resolvers::FileModuleResolver::new(); + resolver.enable_cache(false); + engine.set_module_resolver(resolver); + } + + print_debug_help(); + + // Evaluate + while let Err(err) = engine.run_ast_with_scope(&mut Scope::new(), &main_ast) { + match *err { + // Loop back to restart + EvalAltResult::ErrorTerminated(_, _) => (), + // Break evaluation + _ => { + print_error(&script, *err); + break; + } + } + } +} diff --git a/src/bin/rhai-repl.rs b/src/bin/rhai-repl.rs index 2169b642..6e964182 100644 --- a/src/bin/rhai-repl.rs +++ b/src/bin/rhai-repl.rs @@ -1,12 +1,12 @@ use rhai::{Dynamic, Engine, EvalAltResult, Module, Scope, AST, INT}; +use rustyline::config::Builder; +use rustyline::error::ReadlineError; +use rustyline::{Cmd, Editor, Event, EventHandler, KeyCode, KeyEvent, Modifiers, Movement}; +use smallvec::smallvec; -use std::{ - env, - fs::File, - io::{stdin, stdout, Read, Write}, - path::Path, - process::exit, -}; +use std::{env, fs::File, io::Read, path::Path, process::exit}; + +const HISTORY_FILE: &str = ".rhai-repl-history"; /// Pretty-print error. fn print_error(input: &str, mut err: EvalAltResult) { @@ -44,7 +44,9 @@ fn print_error(input: &str, mut err: EvalAltResult) { /// Print help text. fn print_help() { println!("help => print this help"); + println!("keys => print list of key bindings"); println!("quit, exit => quit"); + println!("history => print lines history"); println!("scope => print all variables in the scope"); println!("strict => toggle on/off Strict Variables Mode"); #[cfg(not(feature = "no_optimize"))] @@ -55,34 +57,216 @@ fn print_help() { println!("json => output all functions in JSON format"); println!("ast => print the last AST (optimized)"); println!("astu => print the last raw, un-optimized AST"); - println!(r"end a line with '\' to continue to the next line."); + println!(); + println!("press Shift-Enter to continue to the next line,"); + println!(r"or end a line with '\' (e.g. when pasting code)."); + println!(); +} + +/// Print key bindings. +fn print_keys() { + println!("Home => move to beginning of line"); + println!("Ctrl-Home => move to beginning of input"); + println!("End => move to end of line"); + println!("Ctrl-End => move to end of input"); + println!("Left => move left"); + println!("Ctrl-Left => move left by one word"); + println!("Right => move right by one word"); + println!("Ctrl-Right => move right"); + println!("Up => previous line or history"); + println!("Ctrl-Up => previous history"); + println!("Down => next line or history"); + println!("Ctrl-Down => next history"); + println!("Ctrl-R => reverse search history"); + println!(" (Ctrl-S forward, Ctrl-G cancel)"); + println!("Ctrl-L => clear screen"); + println!("Escape => clear all input"); + println!("Ctrl-C => exit"); + println!("Ctrl-D => EOF (when line empty)"); + println!("Ctrl-H, Backspace => backspace"); + println!("Ctrl-D, Del => delete character"); + println!("Ctrl-U => delete from start"); + println!("Ctrl-W => delete previous word"); + println!("Ctrl-T => transpose characters"); + println!("Ctrl-V => insert special character"); + println!("Ctrl-Y => paste yank"); + println!("Ctrl-Z => suspend (Unix), undo (Windows)"); + println!("Ctrl-_ => undo"); + println!("Enter => run code"); + println!("Shift-Ctrl-Enter => continue to next line"); + println!(); + println!("Plus all standard Emacs key bindings"); println!(); } /// Display the scope. fn print_scope(scope: &Scope) { - scope - .iter_raw() - .enumerate() - .for_each(|(i, (name, constant, value))| { - #[cfg(not(feature = "no_closure"))] - let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; - #[cfg(feature = "no_closure")] - let value_is_shared = ""; + for (i, (name, constant, value)) in scope.iter_raw().enumerate() { + #[cfg(not(feature = "no_closure"))] + let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; + #[cfg(feature = "no_closure")] + let value_is_shared = ""; - println!( - "[{}] {}{}{} = {:?}", - i + 1, - if constant { "const " } else { "" }, - name, - value_is_shared, - *value.read_lock::().unwrap(), - ) - }); + println!( + "[{}] {}{}{} = {:?}", + i + 1, + if constant { "const " } else { "" }, + name, + value_is_shared, + *value.read_lock::().unwrap(), + ) + } println!(); } +// Load script files specified in the command line. +#[cfg(not(feature = "no_module"))] +#[cfg(not(feature = "no_std"))] +fn load_script_files(engine: &mut Engine) { + // Load init scripts + let mut contents = String::new(); + let mut has_init_scripts = false; + + for filename in env::args().skip(1) { + let filename = match Path::new(&filename).canonicalize() { + Err(err) => { + eprintln!("Error script file path: {}\n{}", filename, err); + exit(1); + } + Ok(f) => { + match f.strip_prefix(std::env::current_dir().unwrap().canonicalize().unwrap()) { + Ok(f) => f.into(), + _ => f, + } + } + }; + + contents.clear(); + + let mut f = match File::open(&filename) { + Err(err) => { + eprintln!( + "Error reading script file: {}\n{}", + filename.to_string_lossy(), + err + ); + exit(1); + } + Ok(f) => f, + }; + + if let Err(err) = f.read_to_string(&mut contents) { + println!( + "Error reading script file: {}\n{}", + filename.to_string_lossy(), + err + ); + exit(1); + } + + let module = match engine + .compile(&contents) + .map_err(|err| err.into()) + .and_then(|mut ast| { + ast.set_source(filename.to_string_lossy().to_string()); + Module::eval_ast_as_new(Scope::new(), &ast, &engine) + }) { + Err(err) => { + let filename = filename.to_string_lossy(); + + eprintln!("{:=<1$}", "", filename.len()); + eprintln!("{}", filename); + eprintln!("{:=<1$}", "", filename.len()); + eprintln!(""); + + print_error(&contents, *err); + exit(1); + } + Ok(m) => m, + }; + + engine.register_global_module(module.into()); + + has_init_scripts = true; + + println!("Script '{}' loaded.", filename.to_string_lossy()); + } + + if has_init_scripts { + println!(); + } +} + +// Setup the Rustyline editor. +fn setup_editor() -> Editor<()> { + let config = Builder::new() + .tab_stop(4) + .indent_size(4) + .bracketed_paste(true) + .build(); + let mut rl = Editor::<()>::with_config(config); + + // Bind more keys + + // On Windows, Esc clears the input buffer + #[cfg(target_family = "windows")] + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent(KeyCode::Esc, Modifiers::empty())]), + EventHandler::Simple(Cmd::Kill(Movement::WholeBuffer)), + ); + // On Windows, Ctrl-Z is undo + #[cfg(target_family = "windows")] + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent::ctrl('z')]), + EventHandler::Simple(Cmd::Undo(1)), + ); + // Map Shift-Return to insert a new line - bypass need for `\` continuation + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent( + KeyCode::Char('m'), + Modifiers::CTRL_SHIFT + )]), + EventHandler::Simple(Cmd::Newline), + ); + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent( + KeyCode::Char('j'), + Modifiers::CTRL_SHIFT + )]), + EventHandler::Simple(Cmd::Newline), + ); + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent(KeyCode::Enter, Modifiers::SHIFT)]), + EventHandler::Simple(Cmd::Newline), + ); + // Map Ctrl-Home and Ctrl-End for beginning/end of input + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent(KeyCode::Home, Modifiers::CTRL)]), + EventHandler::Simple(Cmd::Move(Movement::BeginningOfBuffer)), + ); + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent(KeyCode::End, Modifiers::CTRL)]), + EventHandler::Simple(Cmd::Move(Movement::EndOfBuffer)), + ); + // Map Ctrl-Up and Ctrl-Down to skip up/down the history, even through multi-line histories + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent(KeyCode::Down, Modifiers::CTRL)]), + EventHandler::Simple(Cmd::NextHistory), + ); + rl.bind_sequence( + Event::KeySeq(smallvec![KeyEvent(KeyCode::Up, Modifiers::CTRL)]), + EventHandler::Simple(Cmd::PreviousHistory), + ); + + // Load the history file + if rl.load_history(HISTORY_FILE).is_err() { + eprintln!("! No previous lines history!"); + } + + rl +} + fn main() { let title = format!("Rhai REPL tool (version {})", env!("CARGO_PKG_VERSION")); println!("{}", title); @@ -96,80 +280,7 @@ fn main() { #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_std"))] - { - // Load init scripts - let mut contents = String::new(); - let mut has_init_scripts = false; - - for filename in env::args().skip(1) { - let filename = match Path::new(&filename).canonicalize() { - Err(err) => { - eprintln!("Error script file path: {}\n{}", filename, err); - exit(1); - } - Ok(f) => { - match f.strip_prefix(std::env::current_dir().unwrap().canonicalize().unwrap()) { - Ok(f) => f.into(), - _ => f, - } - } - }; - - contents.clear(); - - let mut f = match File::open(&filename) { - Err(err) => { - eprintln!( - "Error reading script file: {}\n{}", - filename.to_string_lossy(), - err - ); - exit(1); - } - Ok(f) => f, - }; - - if let Err(err) = f.read_to_string(&mut contents) { - println!( - "Error reading script file: {}\n{}", - filename.to_string_lossy(), - err - ); - exit(1); - } - - let module = match engine - .compile(&contents) - .map_err(|err| err.into()) - .and_then(|mut ast| { - ast.set_source(filename.to_string_lossy().to_string()); - Module::eval_ast_as_new(Scope::new(), &ast, &engine) - }) { - Err(err) => { - let filename = filename.to_string_lossy(); - - eprintln!("{:=<1$}", "", filename.len()); - eprintln!("{}", filename); - eprintln!("{:=<1$}", "", filename.len()); - eprintln!(""); - - print_error(&contents, *err); - exit(1); - } - Ok(m) => m, - }; - - engine.register_global_module(module.into()); - - has_init_scripts = true; - - println!("Script '{}' loaded.", filename.to_string_lossy()); - } - - if has_init_scripts { - println!(); - } - } + load_script_files(&mut engine); // Setup Engine #[cfg(not(feature = "no_optimize"))] @@ -194,6 +305,9 @@ fn main() { // Create scope let mut scope = Scope::new(); + // REPL line editor setup + let mut rl = setup_editor(); + // REPL loop let mut input = String::new(); let mut main_ast = AST::empty(); @@ -203,31 +317,37 @@ fn main() { print_help(); 'main_loop: loop { - print!("rhai-repl> "); - stdout().flush().expect("couldn't flush stdout"); - input.clear(); loop { - match stdin().read_line(&mut input) { - Ok(0) => break 'main_loop, - Ok(_) => (), - Err(err) => panic!("input error: {}", err), - } - - let line = input.as_str().trim_end(); - - // Allow line continuation - if line.ends_with('\\') { - let len = line.len(); - input.truncate(len - 1); - input.push('\n'); + let prompt = if input.is_empty() { + "rhai-repl> " } else { - break; - } + " > " + }; - print!("> "); - stdout().flush().expect("couldn't flush stdout"); + match rl.readline(prompt) { + // Line continuation + Ok(mut line) if line.ends_with("\\") => { + line.pop(); + input += line.trim_end(); + input.push('\n'); + } + Ok(line) => { + input += line.trim_end(); + if !input.is_empty() { + rl.add_history_entry(input.clone()); + } + break; + } + + Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break 'main_loop, + + Err(err) => { + eprintln!("Error: {:?}", err); + break 'main_loop; + } + } } let script = input.trim(); @@ -242,7 +362,29 @@ fn main() { print_help(); continue; } + "keys" => { + print_keys(); + continue; + } "exit" | "quit" => break, // quit + "history" => { + for (i, h) in rl.history().iter().enumerate() { + match &h.split('\n').collect::>()[..] { + [line] => println!("[{}] {}", i + 1, line), + lines => { + for (x, line) in lines.iter().enumerate() { + let number = format!("[{}]", i + 1); + if x == 0 { + println!("{} {}", number, line.trim_end()); + } else { + println!("{0:>1$} {2}", "", number.len(), line.trim_end()); + } + } + } + } + } + continue; + } "strict" if engine.strict_variables() => { engine.set_strict_variables(false); println!("Strict Variables Mode turned OFF."); @@ -282,13 +424,14 @@ fn main() { #[cfg(feature = "metadata")] "functions" => { // print a list of all registered functions - engine - .gen_fn_signatures(false) - .into_iter() - .for_each(|f| println!("{}", f)); + for f in engine.gen_fn_signatures(false) { + println!("{}", f) + } #[cfg(not(feature = "no_function"))] - main_ast.iter_functions().for_each(|f| println!("{}", f)); + for f in main_ast.iter_functions() { + println!("{}", f) + } println!(); continue; @@ -343,4 +486,8 @@ fn main() { // Throw away all the statements, leaving only the functions main_ast.clear_statements(); } + + rl.save_history(HISTORY_FILE).unwrap(); + + println!("Bye!"); } diff --git a/src/engine.rs b/src/engine.rs index 58abe68b..14206d6f 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -96,6 +96,7 @@ pub struct Engine { /// A collection of all modules loaded into the global namespace of the Engine. pub(crate) global_modules: StaticVec>, /// A collection of all sub-modules directly loaded into the Engine. + #[cfg(not(feature = "no_module"))] pub(crate) global_sub_modules: BTreeMap>, /// A module resolution service. @@ -115,17 +116,17 @@ pub struct Engine { /// Custom syntax. pub(crate) custom_syntax: BTreeMap>, /// Callback closure for resolving variable access. - pub(crate) resolve_var: Option, + pub(crate) resolve_var: Option>, /// Callback closure to remap tokens during parsing. pub(crate) token_mapper: Option>, /// Callback closure for implementing the `print` command. - pub(crate) print: Option, + pub(crate) print: Option>, /// Callback closure for implementing the `debug` command. - pub(crate) debug: Option, + pub(crate) debug: Option>, /// Callback closure for progress reporting. #[cfg(not(feature = "unchecked"))] - pub(crate) progress: Option, + pub(crate) progress: Option>, /// Optimize the [`AST`][crate::AST] after compilation. #[cfg(not(feature = "no_optimize"))] @@ -137,6 +138,13 @@ pub struct Engine { /// Max limits. #[cfg(not(feature = "unchecked"))] pub(crate) limits: crate::api::limits::Limits, + + /// Callback closure for debugging. + #[cfg(feature = "debugging")] + pub(crate) debugger: Option<( + Box, + Box, + )>, } impl fmt::Debug for Engine { @@ -144,11 +152,11 @@ impl fmt::Debug for Engine { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut f = f.debug_struct("Engine"); - f.field("global_modules", &self.global_modules) - .field("global_sub_modules", &self.global_sub_modules); + f.field("global_modules", &self.global_modules); #[cfg(not(feature = "no_module"))] - f.field("module_resolver", &self.module_resolver.is_some()); + f.field("global_sub_modules", &self.global_sub_modules) + .field("module_resolver", &self.module_resolver.is_some()); f.field("type_names", &self.type_names) .field("disabled_symbols", &self.disabled_symbols) @@ -226,7 +234,7 @@ impl Engine { engine.print = Some(Box::new(|s| println!("{}", s))); engine.debug = Some(Box::new(|s, source, pos| { if let Some(source) = source { - println!("{}{:?} | {}", source, pos, s); + println!("{} @ {:?} | {}", source, pos, s); } else if pos.is_none() { println!("{}", s); } else { @@ -253,6 +261,8 @@ impl Engine { pub fn new_raw() -> Self { let mut engine = Self { global_modules: StaticVec::new_const(), + + #[cfg(not(feature = "no_module"))] global_sub_modules: BTreeMap::new(), #[cfg(not(feature = "no_module"))] @@ -280,6 +290,9 @@ impl Engine { #[cfg(not(feature = "unchecked"))] limits: crate::api::limits::Limits::new(), + + #[cfg(feature = "debugging")] + debugger: None, }; // Add the global namespace module diff --git a/src/eval/chaining.rs b/src/eval/chaining.rs index ee225503..53cd1767 100644 --- a/src/eval/chaining.rs +++ b/src/eval/chaining.rs @@ -125,6 +125,7 @@ impl Engine { this_ptr: &mut Option<&mut Dynamic>, target: &mut Target, root: (&str, Position), + parent: &Expr, rhs: &Expr, terminate_chaining: bool, idx_values: &mut StaticVec, @@ -132,12 +133,16 @@ impl Engine { level: usize, new_val: Option<((Dynamic, Position), (Option, Position))>, ) -> RhaiResultOf<(Dynamic, bool)> { + let _parent = parent; let is_ref_mut = target.is_ref(); let _terminate_chaining = terminate_chaining; // Pop the last index value let idx_val = idx_values.pop().unwrap(); + #[cfg(feature = "debugging")] + let scope = &mut Scope::new(); + match chain_type { #[cfg(not(feature = "no_index"))] ChainType::Indexing => { @@ -150,6 +155,9 @@ impl Engine { Expr::Dot(x, term, x_pos) | Expr::Index(x, term, x_pos) if !_terminate_chaining => { + #[cfg(feature = "debugging")] + self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?; + let mut idx_val_for_setter = idx_val.clone(); let idx_pos = x.lhs.position(); let rhs_chain = rhs.into(); @@ -162,7 +170,7 @@ impl Engine { let obj_ptr = &mut obj; match self.eval_dot_index_chain_helper( - global, state, lib, this_ptr, obj_ptr, root, &x.rhs, *term, + global, state, lib, this_ptr, obj_ptr, root, rhs, &x.rhs, *term, idx_values, rhs_chain, level, new_val, ) { Ok((result, true)) if is_obj_temp_val => { @@ -195,6 +203,9 @@ impl Engine { } // xxx[rhs] op= new_val _ if new_val.is_some() => { + #[cfg(feature = "debugging")] + self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?; + let ((new_val, new_pos), (op_info, op_pos)) = new_val.expect("`Some`"); let mut idx_val_for_setter = idx_val.clone(); @@ -236,11 +247,15 @@ impl Engine { Ok((Dynamic::UNIT, true)) } // xxx[rhs] - _ => self - .get_indexed_mut( + _ => { + #[cfg(feature = "debugging")] + self.run_debugger(scope, global, state, lib, this_ptr, _parent, level)?; + + self.get_indexed_mut( global, state, lib, target, idx_val, pos, false, true, level, ) - .map(|v| (v.take_or_clone(), false)), + .map(|v| (v.take_or_clone(), false)) + } } } @@ -251,9 +266,20 @@ impl Engine { Expr::FnCall(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(); - self.make_method_call( + + #[cfg(feature = "debugging")] + let reset_debugger = self.run_debugger_with_reset( + scope, global, state, lib, this_ptr, rhs, level, + )?; + + let result = self.make_method_call( global, state, lib, name, *hashes, target, call_args, *pos, level, - ) + ); + + #[cfg(feature = "debugging")] + global.debugger.reset_status(reset_debugger); + + result } // xxx.fn_name(...) = ??? Expr::FnCall(_, _) if new_val.is_some() => { @@ -264,10 +290,12 @@ impl Engine { unreachable!("function call in dot chain should not be namespace-qualified") } // {xxx:map}.id op= ??? - Expr::Property(x) if target.is::() && new_val.is_some() => { - let (name, pos) = &x.2; + Expr::Property(x, pos) if target.is::() && new_val.is_some() => { + #[cfg(feature = "debugging")] + self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; + + let index = x.2.clone().into(); let ((new_val, new_pos), (op_info, op_pos)) = new_val.expect("`Some`"); - let index = name.into(); { let val_target = &mut self.get_indexed_mut( global, state, lib, target, index, *pos, true, false, level, @@ -282,17 +310,22 @@ impl Engine { Ok((Dynamic::UNIT, true)) } // {xxx:map}.id - Expr::Property(x) if target.is::() => { - let (name, pos) = &x.2; - let index = name.into(); + Expr::Property(x, pos) if target.is::() => { + #[cfg(feature = "debugging")] + self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; + + let index = x.2.clone().into(); let val = self.get_indexed_mut( global, state, lib, target, index, *pos, false, false, level, )?; Ok((val.take_or_clone(), false)) } // xxx.id op= ??? - Expr::Property(x) if new_val.is_some() => { - let ((getter, hash_get), (setter, hash_set), (name, pos)) = x.as_ref(); + Expr::Property(x, pos) if new_val.is_some() => { + #[cfg(feature = "debugging")] + self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; + + let ((getter, hash_get), (setter, hash_set), name) = x.as_ref(); let ((mut new_val, new_pos), (op_info, op_pos)) = new_val.expect("`Some`"); if op_info.is_some() { @@ -367,8 +400,11 @@ impl Engine { }) } // xxx.id - Expr::Property(x) => { - let ((getter, hash_get), _, (name, pos)) = x.as_ref(); + Expr::Property(x, pos) => { + #[cfg(feature = "debugging")] + self.run_debugger(scope, global, state, lib, this_ptr, rhs, level)?; + + let ((getter, hash_get), _, name) = x.as_ref(); let hash = crate::ast::FnCallHashes::from_native(*hash_get); let args = &mut [target.as_mut()]; self.exec_fn_call( @@ -401,23 +437,39 @@ impl Engine { Expr::Index(x, term, x_pos) | Expr::Dot(x, term, x_pos) if target.is::() => { + let _node = &x.lhs; + let val_target = &mut match x.lhs { - Expr::Property(ref p) => { - let (name, pos) = &p.2; - let index = name.into(); + Expr::Property(ref p, pos) => { + #[cfg(feature = "debugging")] + self.run_debugger( + scope, global, state, lib, this_ptr, _node, level, + )?; + + let index = p.2.clone().into(); self.get_indexed_mut( - global, state, lib, target, index, *pos, false, true, level, + global, state, lib, target, index, pos, false, true, level, )? } // {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() => { let crate::ast::FnCallExpr { name, hashes, .. } = x.as_ref(); let call_args = &mut idx_val.into_fn_call_args(); - let (val, _) = self.make_method_call( + + #[cfg(feature = "debugging")] + let reset_debugger = self.run_debugger_with_reset( + scope, global, state, lib, this_ptr, _node, level, + )?; + + let result = self.make_method_call( global, state, lib, name, *hashes, target, call_args, pos, level, - )?; - val.into() + ); + + #[cfg(feature = "debugging")] + global.debugger.reset_status(reset_debugger); + + result?.0.into() } // {xxx:map}.module::fn_name(...) - syntax error Expr::FnCall(_, _) => unreachable!( @@ -429,18 +481,24 @@ impl Engine { let rhs_chain = rhs.into(); self.eval_dot_index_chain_helper( - global, state, lib, this_ptr, val_target, root, &x.rhs, *term, + global, state, lib, this_ptr, val_target, root, rhs, &x.rhs, *term, 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) => { + let _node = &x.lhs; + match x.lhs { // xxx.prop[expr] | xxx.prop.expr - Expr::Property(ref p) => { - let ((getter, hash_get), (setter, hash_set), (name, pos)) = - p.as_ref(); + Expr::Property(ref p, pos) => { + #[cfg(feature = "debugging")] + self.run_debugger( + scope, global, state, lib, this_ptr, _node, level, + )?; + + let ((getter, hash_get), (setter, hash_set), name) = p.as_ref(); let rhs_chain = rhs.into(); let hash_get = crate::ast::FnCallHashes::from_native(*hash_get); let hash_set = crate::ast::FnCallHashes::from_native(*hash_set); @@ -451,15 +509,15 @@ impl Engine { let (mut val, _) = self .exec_fn_call( global, state, lib, getter, hash_get, args, is_ref_mut, - true, *pos, None, level, + true, pos, None, level, ) .or_else(|err| match *err { // Try an indexer if property does not exist ERR::ErrorDotExpr(_, _) => { let prop = name.into(); self.get_indexed_mut( - global, state, lib, target, prop, *pos, false, - true, level, + global, state, lib, target, prop, pos, false, true, + level, ) .map(|v| (v.take_or_clone(), false)) .map_err( @@ -482,6 +540,7 @@ impl Engine { this_ptr, &mut val.into(), root, + rhs, &x.rhs, *term, idx_values, @@ -498,7 +557,7 @@ impl Engine { let args = &mut arg_values; self.exec_fn_call( global, state, lib, setter, hash_set, args, is_ref_mut, - true, *pos, None, level, + true, pos, None, level, ) .or_else( |err| match *err { @@ -513,7 +572,7 @@ impl Engine { ); self.exec_fn_call( global, state, lib, fn_name, hash_set, args, - is_ref_mut, true, *pos, None, level, + is_ref_mut, true, pos, None, level, ) .or_else(|idx_err| match *idx_err { ERR::ErrorIndexingType(_, _) => { @@ -536,14 +595,24 @@ impl Engine { let crate::ast::FnCallExpr { name, hashes, .. } = f.as_ref(); let rhs_chain = rhs.into(); let args = &mut idx_val.into_fn_call_args(); - let (mut val, _) = self.make_method_call( - global, state, lib, name, *hashes, target, args, pos, level, + + #[cfg(feature = "debugging")] + let reset_debugger = self.run_debugger_with_reset( + scope, global, state, lib, this_ptr, _node, level, )?; - let val = &mut val; + + let result = self.make_method_call( + global, state, lib, name, *hashes, target, args, pos, level, + ); + + #[cfg(feature = "debugging")] + global.debugger.reset_status(reset_debugger); + + let val = &mut result?.0; let target = &mut val.into(); self.eval_dot_index_chain_helper( - global, state, lib, this_ptr, target, root, &x.rhs, *term, + global, state, lib, this_ptr, target, root, rhs, &x.rhs, *term, idx_values, rhs_chain, level, new_val, ) .map_err(|err| err.fill_position(pos)) @@ -594,6 +663,9 @@ impl Engine { match lhs { // id.??? or id[???] Expr::Variable(_, var_pos, x) => { + #[cfg(feature = "debugging")] + self.run_debugger(scope, global, state, lib, this_ptr, lhs, level)?; + #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, *var_pos)?; @@ -604,7 +676,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, rhs, term, idx_values, + global, state, lib, &mut None, obj_ptr, root, expr, rhs, term, idx_values, chain_type, level, new_val, ) .map(|(v, _)| v) @@ -618,8 +690,8 @@ impl Engine { let obj_ptr = &mut value.into(); let root = ("", expr.position()); self.eval_dot_index_chain_helper( - global, state, lib, this_ptr, obj_ptr, root, rhs, term, idx_values, chain_type, - level, new_val, + global, state, lib, this_ptr, obj_ptr, root, expr, rhs, term, idx_values, + chain_type, level, new_val, ) .map(|(v, _)| if is_assignment { Dynamic::UNIT } else { v }) .map_err(|err| err.fill_position(op_pos)) @@ -678,10 +750,10 @@ impl Engine { } #[cfg(not(feature = "no_object"))] - Expr::Property(x) if _parent_chain_type == ChainType::Dotting => { - idx_values.push(super::ChainArgument::Property((x.2).1)) + Expr::Property(_, pos) if _parent_chain_type == ChainType::Dotting => { + idx_values.push(super::ChainArgument::Property(*pos)) } - Expr::Property(_) => unreachable!("unexpected Expr::Property for indexing"), + Expr::Property(_, _) => unreachable!("unexpected Expr::Property for indexing"), Expr::Index(x, term, _) | Expr::Dot(x, term, _) if !terminate_chaining => { let crate::ast::BinaryExpr { lhs, rhs, .. } = x.as_ref(); @@ -689,10 +761,10 @@ impl Engine { // Evaluate in left-to-right order let lhs_arg_val = match lhs { #[cfg(not(feature = "no_object"))] - Expr::Property(x) if _parent_chain_type == ChainType::Dotting => { - super::ChainArgument::Property((x.2).1) + Expr::Property(_, pos) if _parent_chain_type == ChainType::Dotting => { + super::ChainArgument::Property(*pos) } - Expr::Property(_) => unreachable!("unexpected Expr::Property for indexing"), + Expr::Property(_, _) => unreachable!("unexpected Expr::Property for indexing"), #[cfg(not(feature = "no_object"))] Expr::FnCall(x, _) diff --git a/src/eval/debugger.rs b/src/eval/debugger.rs new file mode 100644 index 00000000..b6c1e487 --- /dev/null +++ b/src/eval/debugger.rs @@ -0,0 +1,455 @@ +//! Module defining the debugging interface. +#![cfg(feature = "debugging")] + +use super::{EvalContext, EvalState, GlobalRuntimeState}; +use crate::ast::{ASTNode, Expr, Stmt}; +use crate::{Dynamic, Engine, Identifier, Module, Position, RhaiResultOf, Scope}; +use std::fmt; +#[cfg(feature = "no_std")] +use std::prelude::v1::*; + +/// Callback function to initialize the debugger. +#[cfg(not(feature = "sync"))] +pub type OnDebuggingInit = dyn Fn() -> Dynamic; +/// Callback function to initialize the debugger. +#[cfg(feature = "sync")] +pub type OnDebuggingInit = dyn Fn() -> Dynamic + Send + Sync; + +/// Callback function for debugging. +#[cfg(not(feature = "sync"))] +pub type OnDebuggerCallback = + dyn Fn(&mut EvalContext, ASTNode, Option<&str>, Position) -> RhaiResultOf; +/// Callback function for debugging. +#[cfg(feature = "sync")] +pub type OnDebuggerCallback = dyn Fn(&mut EvalContext, ASTNode, Option<&str>, Position) -> RhaiResultOf + + Send + + Sync; + +/// A command for the debugger on the next iteration. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum DebuggerCommand { + // Continue normal execution. + Continue, + // Step into the next expression, diving into functions. + StepInto, + // Run to the next expression or statement, stepping over functions. + StepOver, + // Run to the next statement, skipping over functions. + Next, +} + +/// A break-point for debugging. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum BreakPoint { + /// Break at a particular position under a particular source. + /// + /// Not available under `no_position`. + /// + /// Source is empty if not available. + #[cfg(not(feature = "no_position"))] + AtPosition { + source: Identifier, + pos: Position, + enabled: bool, + }, + /// Break at a particular function call. + AtFunctionName { name: Identifier, enabled: bool }, + /// Break at a particular function call with a particular number of arguments. + AtFunctionCall { + name: Identifier, + args: usize, + enabled: bool, + }, + /// Break at a particular property . + /// + /// Not available under `no_object`. + #[cfg(not(feature = "no_object"))] + AtProperty { name: Identifier, enabled: bool }, +} + +impl fmt::Display for BreakPoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + #[cfg(not(feature = "no_position"))] + Self::AtPosition { + source, + pos, + enabled, + } => { + if !source.is_empty() { + write!(f, "{} @ {:?}", source, pos)?; + } else { + write!(f, "@ {:?}", pos)?; + } + if !*enabled { + f.write_str(" (disabled)")?; + } + Ok(()) + } + Self::AtFunctionName { + name: fn_name, + enabled, + } => { + write!(f, "{} (...)", fn_name)?; + if !*enabled { + f.write_str(" (disabled)")?; + } + Ok(()) + } + Self::AtFunctionCall { + name: fn_name, + args, + enabled, + } => { + write!( + f, + "{} ({})", + fn_name, + std::iter::repeat("_") + .take(*args) + .collect::>() + .join(", ") + )?; + if !*enabled { + f.write_str(" (disabled)")?; + } + Ok(()) + } + #[cfg(not(feature = "no_object"))] + Self::AtProperty { + name: prop, + enabled, + } => { + write!(f, ".{}", prop)?; + if !*enabled { + f.write_str(" (disabled)")?; + } + Ok(()) + } + } + } +} + +impl BreakPoint { + /// Is this [`BreakPoint`] enabled? + #[inline(always)] + pub fn is_enabled(&self) -> bool { + match self { + #[cfg(not(feature = "no_position"))] + Self::AtPosition { enabled, .. } => *enabled, + Self::AtFunctionName { enabled, .. } | Self::AtFunctionCall { enabled, .. } => *enabled, + #[cfg(not(feature = "no_object"))] + Self::AtProperty { enabled, .. } => *enabled, + } + } + /// Enable/disable this [`BreakPoint`]. + #[inline(always)] + pub fn enable(&mut self, value: bool) { + match self { + #[cfg(not(feature = "no_position"))] + Self::AtPosition { enabled, .. } => *enabled = value, + Self::AtFunctionName { enabled, .. } | Self::AtFunctionCall { enabled, .. } => { + *enabled = value + } + #[cfg(not(feature = "no_object"))] + Self::AtProperty { enabled, .. } => *enabled = value, + } + } +} + +/// A function call. +#[cfg(not(feature = "no_function"))] +#[derive(Debug, Clone, Hash)] +pub struct CallStackFrame { + /// Function name. + pub fn_name: Identifier, + /// Copies of function call arguments, if any. + pub args: crate::StaticVec, + /// Source of the function, empty if none. + pub source: Identifier, + /// [Position][`Position`] of the function call. + pub pos: Position, +} + +#[cfg(not(feature = "no_function"))] +impl fmt::Display for CallStackFrame { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut fp = f.debug_tuple(&self.fn_name); + + for arg in &self.args { + fp.field(arg); + } + + fp.finish()?; + + if !self.pos.is_none() { + if self.source.is_empty() { + write!(f, " @ {:?}", self.pos)?; + } else { + write!(f, ": {} @ {:?}", self.source, self.pos)?; + } + } + + Ok(()) + } +} + +/// A type providing debugging facilities. +#[derive(Debug, Clone, Hash)] +pub struct Debugger { + /// The current status command. + status: DebuggerCommand, + /// The current state. + state: Dynamic, + /// The current set of break-points. + break_points: Vec, + /// The current function call stack. + #[cfg(not(feature = "no_function"))] + call_stack: Vec, +} + +impl Debugger { + /// Create a new [`Debugger`] based on an [`Engine`]. + #[inline(always)] + #[must_use] + pub fn new(engine: &Engine) -> Self { + Self { + status: if engine.debugger.is_some() { + DebuggerCommand::StepInto + } else { + DebuggerCommand::Continue + }, + state: if let Some((ref init, _)) = engine.debugger { + init() + } else { + Dynamic::UNIT + }, + break_points: Vec::new(), + #[cfg(not(feature = "no_function"))] + call_stack: Vec::new(), + } + } + /// Get a reference to the current state. + #[inline(always)] + #[must_use] + pub fn state(&self) -> &Dynamic { + &self.state + } + /// Get a mutable reference to the current state. + #[inline(always)] + #[must_use] + pub fn state_mut(&mut self) -> &mut Dynamic { + &mut self.state + } + /// Get the current call stack. + /// + /// Not available under `no_function`. + #[cfg(not(feature = "no_function"))] + #[inline(always)] + #[must_use] + pub fn call_stack(&self) -> &[CallStackFrame] { + &self.call_stack + } + /// Rewind the function call stack to a particular depth. + /// + /// Not available under `no_function`. + #[cfg(not(feature = "no_function"))] + #[inline(always)] + pub(crate) fn rewind_call_stack(&mut self, len: usize) { + self.call_stack.truncate(len); + } + /// Add a new frame to the function call stack. + /// + /// Not available under `no_function`. + #[cfg(not(feature = "no_function"))] + #[inline(always)] + pub(crate) fn push_call_stack_frame( + &mut self, + fn_name: impl Into, + args: crate::StaticVec, + source: impl Into, + pos: Position, + ) { + self.call_stack.push(CallStackFrame { + fn_name: fn_name.into(), + args, + source: source.into(), + pos, + }); + } + /// Get the current status of this [`Debugger`]. + #[inline(always)] + #[must_use] + pub fn status(&self) -> DebuggerCommand { + self.status + } + /// Get a mutable reference to the current status of this [`Debugger`]. + #[inline(always)] + #[must_use] + pub fn status_mut(&mut self) -> &mut DebuggerCommand { + &mut self.status + } + /// Set the status of this [`Debugger`]. + #[inline(always)] + pub fn reset_status(&mut self, status: Option) { + if let Some(cmd) = status { + self.status = cmd; + } + } + /// Does a particular [`AST` Node][ASTNode] trigger a break-point? + #[must_use] + pub fn is_break_point(&self, src: &str, node: ASTNode) -> bool { + let _src = src; + + self.break_points() + .iter() + .filter(|&bp| bp.is_enabled()) + .any(|bp| match bp { + #[cfg(not(feature = "no_position"))] + BreakPoint::AtPosition { pos, .. } if pos.is_none() => false, + #[cfg(not(feature = "no_position"))] + BreakPoint::AtPosition { source, pos, .. } if pos.is_beginning_of_line() => { + node.position().line().unwrap_or(0) == pos.line().unwrap() && _src == source + } + #[cfg(not(feature = "no_position"))] + BreakPoint::AtPosition { source, pos, .. } => { + node.position() == *pos && _src == source + } + BreakPoint::AtFunctionName { name, .. } => match node { + ASTNode::Expr(Expr::FnCall(x, _)) | ASTNode::Stmt(Stmt::FnCall(x, _)) => { + x.name == *name + } + _ => false, + }, + BreakPoint::AtFunctionCall { name, args, .. } => match node { + ASTNode::Expr(Expr::FnCall(x, _)) | ASTNode::Stmt(Stmt::FnCall(x, _)) => { + x.args.len() == *args && x.name == *name + } + _ => false, + }, + #[cfg(not(feature = "no_object"))] + BreakPoint::AtProperty { name, .. } => match node { + ASTNode::Expr(Expr::Property(x, _)) => x.2 == *name, + _ => false, + }, + }) + } + /// Get a slice of all [`BreakPoint`]'s. + #[inline(always)] + #[must_use] + pub fn break_points(&self) -> &[BreakPoint] { + &self.break_points + } + /// Get the underlying [`Vec`] holding all [`BreakPoint`]'s. + #[inline(always)] + #[must_use] + pub fn break_points_mut(&mut self) -> &mut Vec { + &mut self.break_points + } +} + +impl Engine { + /// Run the debugger callback. + #[inline(always)] + pub(crate) fn run_debugger<'a>( + &self, + scope: &mut Scope, + global: &mut GlobalRuntimeState, + state: &mut EvalState, + lib: &[&Module], + this_ptr: &mut Option<&mut Dynamic>, + node: impl Into>, + level: usize, + ) -> RhaiResultOf<()> { + if let Some(cmd) = + self.run_debugger_with_reset(scope, global, state, lib, this_ptr, node, level)? + { + global.debugger.status = cmd; + } + + Ok(()) + } + /// Run the debugger callback. + /// + /// Returns `true` if the debugger needs to be reactivated at the end of the block, statement or + /// function call. + /// + /// # Note + /// + /// When the debugger callback return [`DebuggerCommand::StepOver`], the debugger if temporarily + /// disabled and `true` is returned. + /// + /// It is up to the [`Engine`] to reactivate the debugger. + #[inline] + #[must_use] + pub(crate) fn run_debugger_with_reset<'a>( + &self, + scope: &mut Scope, + global: &mut GlobalRuntimeState, + state: &mut EvalState, + lib: &[&Module], + this_ptr: &mut Option<&mut Dynamic>, + node: impl Into>, + level: usize, + ) -> RhaiResultOf> { + if let Some((_, ref on_debugger)) = self.debugger { + let node = node.into(); + + // Skip transitive nodes + match node { + ASTNode::Expr(Expr::Stmt(_)) | ASTNode::Stmt(Stmt::Expr(_)) => return Ok(None), + _ => (), + } + + let stop = match global.debugger.status { + DebuggerCommand::Continue => false, + DebuggerCommand::Next => matches!(node, ASTNode::Stmt(_)), + DebuggerCommand::StepInto | DebuggerCommand::StepOver => true, + }; + + if !stop && !global.debugger.is_break_point(&global.source, node) { + return Ok(None); + } + + let source = global.source.clone(); + let source = if source.is_empty() { + None + } else { + Some(source.as_str()) + }; + + let mut context = crate::EvalContext { + engine: self, + scope, + global, + state, + lib, + this_ptr, + level, + }; + + let command = on_debugger(&mut context, node, source, node.position())?; + + match command { + DebuggerCommand::Continue => { + global.debugger.status = DebuggerCommand::Continue; + Ok(None) + } + DebuggerCommand::Next => { + global.debugger.status = DebuggerCommand::Continue; + Ok(Some(DebuggerCommand::Next)) + } + DebuggerCommand::StepInto => { + global.debugger.status = DebuggerCommand::StepInto; + Ok(None) + } + DebuggerCommand::StepOver => { + global.debugger.status = DebuggerCommand::Continue; + Ok(Some(DebuggerCommand::StepOver)) + } + } + } else { + Ok(None) + } + } +} diff --git a/src/eval/eval_context.rs b/src/eval/eval_context.rs index 7b0aa5dd..2191bb44 100644 --- a/src/eval/eval_context.rs +++ b/src/eval/eval_context.rs @@ -24,7 +24,7 @@ pub struct EvalContext<'a, 'x, 'px, 'm, 'pm, 's, 'ps, 'b, 't, 'pt> { pub(crate) level: usize, } -impl<'x, 'px, 'pt> EvalContext<'_, 'x, 'px, '_, '_, '_, '_, '_, '_, 'pt> { +impl<'x, 'px, 'm, 'pm, 'pt> EvalContext<'_, 'x, 'px, 'm, 'pm, '_, '_, '_, '_, 'pt> { /// The current [`Engine`]. #[inline(always)] #[must_use] @@ -46,13 +46,14 @@ impl<'x, 'px, 'pt> EvalContext<'_, 'x, 'px, '_, '_, '_, '_, '_, '_, 'pt> { pub const fn scope(&self) -> &Scope<'px> { self.scope } - /// Mutable reference to the current [`Scope`]. + /// Get a mutable reference to the current [`Scope`]. #[inline(always)] #[must_use] pub fn scope_mut(&mut self) -> &mut &'x mut Scope<'px> { &mut self.scope } - /// Get an iterator over the current set of modules imported via `import` statements. + /// Get an iterator over the current set of modules imported via `import` statements, + /// in reverse order (i.e. modules imported last come first). #[cfg(not(feature = "no_module"))] #[inline(always)] pub fn iter_imports(&self) -> impl Iterator { @@ -61,12 +62,19 @@ impl<'x, 'px, 'pt> EvalContext<'_, 'x, 'px, '_, '_, '_, '_, '_, '_, 'pt> { /// _(internals)_ The current [`GlobalRuntimeState`]. /// Exported under the `internals` feature only. #[cfg(feature = "internals")] - #[cfg(not(feature = "no_module"))] #[inline(always)] #[must_use] pub const fn global_runtime_state(&self) -> &GlobalRuntimeState { self.global } + /// _(internals)_ Get a mutable reference to the current [`GlobalRuntimeState`]. + /// Exported under the `internals` feature only. + #[cfg(feature = "internals")] + #[inline(always)] + #[must_use] + pub fn global_runtime_state_mut(&mut self) -> &mut &'m mut GlobalRuntimeState<'pm> { + &mut self.global + } /// Get an iterator over the namespaces containing definition of all script-defined functions. #[inline] pub fn iter_namespaces(&self) -> impl Iterator { diff --git a/src/eval/eval_state.rs b/src/eval/eval_state.rs index d0e9b2bc..943831b7 100644 --- a/src/eval/eval_state.rs +++ b/src/eval/eval_state.rs @@ -7,16 +7,17 @@ use std::marker::PhantomData; #[cfg(feature = "no_std")] use std::prelude::v1::*; -/// _(internals)_ A type that holds all the current states of the [`Engine`]. +/// _(internals)_ A type that holds all the current states of the [`Engine`][crate::Engine]. /// Exported under the `internals` feature only. #[derive(Debug, Clone)] pub struct EvalState<'a> { - /// Force a [`Scope`] search by name. + /// Force a [`Scope`][crate::Scope] search by name. /// - /// Normally, access to variables are parsed with a relative offset into the [`Scope`] to avoid a lookup. + /// Normally, access to variables are parsed with a relative offset into the + /// [`Scope`][crate::Scope] to avoid a lookup. /// - /// In some situation, e.g. after running an `eval` statement, or after a custom syntax statement, - /// subsequent offsets may become mis-aligned. + /// In some situation, e.g. after running an `eval` statement, or after a custom syntax + /// statement, subsequent offsets may become mis-aligned. /// /// When that happens, this flag is turned on. pub always_search_scope: bool, diff --git a/src/eval/expr.rs b/src/eval/expr.rs index 47fbffef..6b8a1287 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -3,25 +3,23 @@ use super::{EvalContext, EvalState, GlobalRuntimeState, Target}; use crate::ast::{Expr, FnCallExpr, OpAssignment}; use crate::engine::{KEYWORD_THIS, OP_CONCAT}; -use crate::module::Namespace; use crate::types::dynamic::AccessMode; -use crate::{ - Dynamic, Engine, Module, Position, RhaiResult, RhaiResultOf, Scope, Shared, StaticVec, ERR, -}; +use crate::{Dynamic, Engine, Module, Position, RhaiResult, RhaiResultOf, Scope, StaticVec, ERR}; use std::num::NonZeroUsize; #[cfg(feature = "no_std")] use std::prelude::v1::*; impl Engine { /// Search for a module within an imports stack. + #[cfg(not(feature = "no_module"))] #[inline] #[must_use] pub(crate) fn search_imports( &self, global: &GlobalRuntimeState, state: &mut EvalState, - namespace: &Namespace, - ) -> Option> { + namespace: &crate::module::Namespace, + ) -> Option> { let root = &namespace[0].name; // Qualified - check if the root module is directly indexed @@ -59,13 +57,16 @@ impl Engine { } Expr::Variable(None, _var_pos, v) => match v.as_ref() { // Normal variable access + #[cfg(not(feature = "no_module"))] (_, None, _) => self.search_scope_only(scope, global, state, lib, this_ptr, expr), + #[cfg(feature = "no_module")] + (_, (), _) => self.search_scope_only(scope, global, state, lib, this_ptr, expr), // Qualified variable access #[cfg(not(feature = "no_module"))] (_, Some((namespace, hash_var)), var_name) => { + // foo:bar::baz::VARIABLE if let Some(module) = self.search_imports(global, state, namespace) { - // foo:bar::baz::VARIABLE return match module.get_qualified_var(*hash_var) { Ok(target) => { let mut target = target.clone(); @@ -89,13 +90,13 @@ impl Engine { }; } + // global::VARIABLE #[cfg(not(feature = "no_function"))] if namespace.len() == 1 && namespace[0].name == crate::engine::KEYWORD_GLOBAL { - // global::VARIABLE - let global_constants = global.constants_mut(); - - if let Some(mut guard) = global_constants { - if let Some(value) = guard.get_mut(var_name) { + if let Some(ref constants) = global.constants { + if let Some(value) = + crate::func::locked_write(constants).get_mut(var_name) + { let mut target: Target = value.clone().into(); // Module variables are constant target.set_access_mode(AccessMode::ReadOnly); @@ -117,9 +118,6 @@ impl Engine { Err(ERR::ErrorModuleNotFound(namespace.to_string(), namespace[0].pos).into()) } - - #[cfg(feature = "no_module")] - (_, Some((_, _)), _) => unreachable!("qualified access under no_module"), }, _ => unreachable!("Expr::Variable expected but gets {:?}", expr), } @@ -211,6 +209,7 @@ impl Engine { ) -> RhaiResult { let FnCallExpr { name, + #[cfg(not(feature = "no_module"))] namespace, capture_parent_scope: capture, hashes, @@ -219,29 +218,37 @@ impl Engine { .. } = expr; + #[cfg(not(feature = "no_module"))] if let Some(namespace) = namespace.as_ref() { // Qualified function call let hash = hashes.native; - self.make_qualified_function_call( + return self.make_qualified_function_call( scope, global, state, lib, this_ptr, namespace, name, args, constants, hash, pos, level, - ) - } else { - // Normal function call - let (first_arg, args) = args.split_first().map_or_else( - || (None, args.as_ref()), - |(first, rest)| (Some(first), rest), ); - - self.make_function_call( - scope, global, state, lib, this_ptr, name, first_arg, args, constants, *hashes, - pos, *capture, level, - ) } + + // Normal function call + let (first_arg, args) = args.split_first().map_or_else( + || (None, args.as_ref()), + |(first, rest)| (Some(first), rest), + ); + + self.make_function_call( + scope, global, state, lib, this_ptr, name, first_arg, args, constants, *hashes, pos, + *capture, level, + ) } /// Evaluate an expression. + // + // # Implementation Notes + // + // Do not use the `?` operator within the main body as it makes this function return early, + // possibly by-passing important cleanup tasks at the end. + // + // Errors that are not recoverable, such as system errors or safety errors, can use `?`. pub(crate) fn eval_expr( &self, scope: &mut Scope, @@ -258,16 +265,29 @@ impl Engine { // Function calls should account for a relatively larger portion of expressions because // binary operators are also function calls. if let Expr::FnCall(x, pos) = expr { + #[cfg(feature = "debugging")] + let reset_debugger = + self.run_debugger_with_reset(scope, global, state, lib, this_ptr, expr, level)?; + #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, expr.position())?; - return self.eval_fn_call_expr(scope, global, state, lib, this_ptr, x, *pos, level); + let result = + self.eval_fn_call_expr(scope, global, state, lib, this_ptr, x, *pos, level); + + #[cfg(feature = "debugging")] + global.debugger.reset_status(reset_debugger); + + return result; } // Then variable access. // We shouldn't do this for too many variants because, soon or later, the added comparisons // will cost more than the mis-predicted `match` branch. if let Expr::Variable(index, var_pos, x) = expr { + #[cfg(feature = "debugging")] + self.run_debugger(scope, global, state, lib, this_ptr, expr, level)?; + #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, expr.position())?; @@ -282,10 +302,14 @@ impl Engine { }; } + #[cfg(feature = "debugging")] + let reset_debugger = + self.run_debugger_with_reset(scope, global, state, lib, this_ptr, expr, level)?; + #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, expr.position())?; - match expr { + let result = match expr { // Constants Expr::DynamicConstant(x, _) => Ok(x.as_ref().clone()), Expr::IntegerConstant(x, _) => Ok((*x).into()), @@ -299,47 +323,62 @@ impl Engine { // `... ${...} ...` Expr::InterpolatedString(x, pos) => { let mut pos = *pos; - let mut result: Dynamic = self.const_empty_string().into(); + let mut concat: Dynamic = self.const_empty_string().into(); + let mut result = Ok(Dynamic::UNIT); for expr in x.iter() { - let item = self.eval_expr(scope, global, state, lib, this_ptr, expr, level)?; + let item = + match self.eval_expr(scope, global, state, lib, this_ptr, expr, level) { + Ok(r) => r, + err => { + result = err; + break; + } + }; - self.eval_op_assignment( + if let Err(err) = self.eval_op_assignment( global, state, lib, Some(OpAssignment::new(OP_CONCAT)), pos, - &mut (&mut result).into(), + &mut (&mut concat).into(), ("", Position::NONE), item, - ) - .map_err(|err| err.fill_position(expr.position()))?; + ) { + result = Err(err.fill_position(expr.position())); + break; + } pos = expr.position(); } - Ok(result) + result.map(|_| concat) } #[cfg(not(feature = "no_index"))] Expr::Array(x, _) => { - let mut arr = Dynamic::from_array(crate::Array::with_capacity(x.len())); + let mut arr = crate::Array::with_capacity(x.len()); + let mut result = Ok(Dynamic::UNIT); #[cfg(not(feature = "unchecked"))] let mut sizes = (0, 0, 0); for item_expr in x.iter() { - let value = self - .eval_expr(scope, global, state, lib, this_ptr, item_expr, level)? - .flatten(); + let value = match self + .eval_expr(scope, global, state, lib, this_ptr, item_expr, level) + { + Ok(r) => r.flatten(), + err => { + result = err; + break; + } + }; #[cfg(not(feature = "unchecked"))] let val_sizes = Self::calc_data_sizes(&value, true); - arr.write_lock::() - .expect("`Array`") - .push(value); + arr.push(value); #[cfg(not(feature = "unchecked"))] if self.has_data_size_limit() { @@ -352,29 +391,33 @@ impl Engine { } } - Ok(arr) + result.map(|_| arr.into()) } #[cfg(not(feature = "no_object"))] Expr::Map(x, _) => { - let mut map = Dynamic::from_map(x.1.clone()); + let mut map = x.1.clone(); + let mut result = Ok(Dynamic::UNIT); #[cfg(not(feature = "unchecked"))] let mut sizes = (0, 0, 0); for (crate::ast::Ident { name, .. }, value_expr) in x.0.iter() { let key = name.as_str(); - let value = self - .eval_expr(scope, global, state, lib, this_ptr, value_expr, level)? - .flatten(); + let value = match self + .eval_expr(scope, global, state, lib, this_ptr, value_expr, level) + { + Ok(r) => r.flatten(), + err => { + result = err; + break; + } + }; #[cfg(not(feature = "unchecked"))] let val_sizes = Self::calc_data_sizes(&value, true); - *map.write_lock::() - .expect("`Map`") - .get_mut(key) - .unwrap() = value; + *map.get_mut(key).unwrap() = value; #[cfg(not(feature = "unchecked"))] if self.has_data_size_limit() { @@ -387,33 +430,53 @@ impl Engine { } } - Ok(map) + result.map(|_| map.into()) } Expr::And(x, _) => { - Ok((self - .eval_expr(scope, global, state, lib, this_ptr, &x.lhs, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, x.lhs.position()))? - && // Short-circuit using && - self - .eval_expr(scope, global, state, lib, this_ptr, &x.rhs, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, x.rhs.position()))?) - .into()) + let lhs = self + .eval_expr(scope, global, state, lib, this_ptr, &x.lhs, level) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::(typ, x.lhs.position()) + }) + }); + + if let Ok(true) = lhs { + self.eval_expr(scope, global, state, lib, this_ptr, &x.rhs, level) + .and_then(|v| { + v.as_bool() + .map_err(|typ| { + self.make_type_mismatch_err::(typ, x.rhs.position()) + }) + .map(Into::into) + }) + } else { + lhs.map(Into::into) + } } Expr::Or(x, _) => { - Ok((self - .eval_expr(scope, global, state, lib, this_ptr, &x.lhs, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, x.lhs.position()))? - || // Short-circuit using || - self - .eval_expr(scope, global, state, lib, this_ptr, &x.rhs, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, x.rhs.position()))?) - .into()) + let lhs = self + .eval_expr(scope, global, state, lib, this_ptr, &x.lhs, level) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::(typ, x.lhs.position()) + }) + }); + + if let Ok(false) = lhs { + self.eval_expr(scope, global, state, lib, this_ptr, &x.rhs, level) + .and_then(|v| { + v.as_bool() + .map_err(|typ| { + self.make_type_mismatch_err::(typ, x.rhs.position()) + }) + .map(Into::into) + }) + } else { + lhs.map(Into::into) + } } Expr::Custom(custom, pos) => { @@ -459,6 +522,11 @@ impl Engine { } _ => unreachable!("expression cannot be evaluated: {:?}", expr), - } + }; + + #[cfg(feature = "debugging")] + global.debugger.reset_status(reset_debugger); + + return result; } } diff --git a/src/eval/global_state.rs b/src/eval/global_state.rs index b2dc0181..fa7979b7 100644 --- a/src/eval/global_state.rs +++ b/src/eval/global_state.rs @@ -1,33 +1,30 @@ //! Global runtime state. -use crate::func::{CallableFunction, IteratorFn}; -use crate::{Identifier, Module, Shared, StaticVec}; +use crate::{Engine, Identifier}; #[cfg(feature = "no_std")] use std::prelude::v1::*; -use std::{ - any::TypeId, - fmt, - iter::{FromIterator, Rev, Zip}, - marker::PhantomData, -}; +use std::{fmt, marker::PhantomData}; -/// _(internals)_ A stack of imported [modules][Module] plus mutable global runtime states. +/// _(internals)_ Global runtime states. /// Exported under the `internals` feature only. // // # Implementation Notes // -// This implementation splits the module names from the shared modules to improve data locality. -// Most usage will be looking up a particular key from the list and then getting the module that -// corresponds to that key. +// This implementation for imported [modules][crate::Module] splits the module names from the shared +// modules to improve data locality. Most usage will be looking up a particular key from the list +// and then getting the module that corresponds to that key. #[derive(Clone)] pub struct GlobalRuntimeState<'a> { /// Stack of module names. - // - // We cannot use Cow here because `eval` may load a [module][Module] and - // the module name will live beyond the AST of the eval script text. - keys: StaticVec, - /// Stack of imported [modules][Module]. - modules: StaticVec>, + /// + /// 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. /// No source if the string is empty. pub source: Identifier, @@ -38,33 +35,37 @@ pub struct GlobalRuntimeState<'a> { /// Function call hashes to index getters and setters. #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] fn_hash_indexing: (u64, u64), - /// Embedded [module][Module] resolver. + /// Embedded [crate::Module][crate::Module] resolver. #[cfg(not(feature = "no_module"))] - pub embedded_module_resolver: Option>, + pub embedded_module_resolver: + Option>, /// Cache of globally-defined constants. + /// + /// Interior mutability is needed because it is shared in order to aid in cloning. #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_function"))] - constants: - Option>>>, + pub(crate) constants: Option< + crate::Shared>>, + >, + /// Debugging interface. + #[cfg(feature = "debugging")] + pub debugger: super::Debugger, /// Take care of the lifetime parameter. dummy: PhantomData<&'a ()>, } -impl Default for GlobalRuntimeState<'_> { - #[inline(always)] - fn default() -> Self { - Self::new() - } -} - impl GlobalRuntimeState<'_> { - /// Create a new [`GlobalRuntimeState`]. + /// Create a new [`GlobalRuntimeState`] based on an [`Engine`]. #[inline(always)] #[must_use] - pub fn new() -> Self { + pub fn new(engine: &Engine) -> Self { + let _engine = engine; + Self { - keys: StaticVec::new_const(), - modules: StaticVec::new_const(), + #[cfg(not(feature = "no_module"))] + keys: crate::StaticVec::new_const(), + #[cfg(not(feature = "no_module"))] + modules: crate::StaticVec::new_const(), source: Identifier::new_const(), num_operations: 0, num_modules_loaded: 0, @@ -75,29 +76,46 @@ impl GlobalRuntimeState<'_> { #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_function"))] constants: None, + #[cfg(feature = "debugging")] + debugger: crate::eval::Debugger::new(_engine), dummy: PhantomData::default(), } } - /// Get the length of the stack of globally-imported [modules][Module]. + /// Get the length of the stack of globally-imported [modules][crate::Module]. + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] #[inline(always)] #[must_use] pub fn num_imports(&self) -> usize { self.keys.len() } - /// Get the globally-imported [module][Module] at a particular index. + /// Get the globally-imported [crate::Module][crate::Module] at a particular index. + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] #[inline(always)] #[must_use] - pub fn get_shared_import(&self, index: usize) -> Option> { + pub fn get_shared_import(&self, index: usize) -> Option> { self.modules.get(index).cloned() } - /// Get a mutable reference to the globally-imported [module][Module] at a particular index. + /// Get a mutable reference to the globally-imported [crate::Module][crate::Module] at a particular index. + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] #[allow(dead_code)] #[inline(always)] #[must_use] - pub(crate) fn get_shared_import_mut(&mut self, index: usize) -> Option<&mut Shared> { + pub(crate) fn get_shared_import_mut( + &mut self, + index: usize, + ) -> Option<&mut crate::Shared> { self.modules.get_mut(index) } - /// Get the index of a globally-imported [module][Module] by name. + /// Get the index of a globally-imported [crate::Module][crate::Module] by name. + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] #[inline] #[must_use] pub fn find_import(&self, name: &str) -> Option { @@ -111,101 +129,121 @@ impl GlobalRuntimeState<'_> { } }) } - /// Push an imported [module][Module] onto the stack. + /// Push an imported [crate::Module][crate::Module] onto the stack. + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] #[inline(always)] - pub fn push_import(&mut self, name: impl Into, module: impl Into>) { + pub fn push_import( + &mut self, + name: impl Into, + module: impl Into>, + ) { self.keys.push(name.into()); self.modules.push(module.into()); } - /// Truncate the stack of globally-imported [modules][Module] to a particular length. + /// Truncate the stack of globally-imported [modules][crate::Module] to a particular length. + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] #[inline(always)] pub fn truncate_imports(&mut self, size: usize) { self.keys.truncate(size); self.modules.truncate(size); } - /// Get an iterator to the stack of globally-imported [modules][Module] in reverse order. + /// Get an iterator to the stack of globally-imported [modules][crate::Module] in reverse order. + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] #[allow(dead_code)] #[inline] - pub fn iter_imports(&self) -> impl Iterator { + pub fn iter_imports(&self) -> impl Iterator { self.keys .iter() .rev() .zip(self.modules.iter().rev()) .map(|(name, module)| (name.as_str(), module.as_ref())) } - /// Get an iterator to the stack of globally-imported [modules][Module] in reverse order. + /// Get an iterator to the stack of globally-imported [modules][crate::Module] in reverse order. + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] #[allow(dead_code)] #[inline] - pub(crate) fn iter_imports_raw(&self) -> impl Iterator)> { + pub(crate) fn iter_imports_raw( + &self, + ) -> impl Iterator)> { self.keys.iter().rev().zip(self.modules.iter().rev()) } - /// Get an iterator to the stack of globally-imported [modules][Module] in forward order. + /// Get an iterator to the stack of globally-imported [modules][crate::Module] in forward order. + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] #[allow(dead_code)] #[inline] - pub(crate) fn scan_imports_raw(&self) -> impl Iterator)> { + pub fn scan_imports_raw( + &self, + ) -> impl Iterator)> { self.keys.iter().zip(self.modules.iter()) } - /// Does the specified function hash key exist in the stack of globally-imported [modules][Module]? + /// Does the specified function hash key exist in the stack of globally-imported [modules][crate::Module]? + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] #[allow(dead_code)] #[inline] #[must_use] pub fn contains_qualified_fn(&self, hash: u64) -> bool { self.modules.iter().any(|m| m.contains_qualified_fn(hash)) } - /// Get the specified function via its hash key from the stack of globally-imported [modules][Module]. + /// Get the specified function via its hash key from the stack of globally-imported [modules][crate::Module]. + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] #[inline] #[must_use] - pub fn get_qualified_fn(&self, hash: u64) -> Option<(&CallableFunction, Option<&str>)> { + pub fn get_qualified_fn( + &self, + hash: u64, + ) -> Option<(&crate::func::CallableFunction, Option<&str>)> { self.modules .iter() .rev() .find_map(|m| m.get_qualified_fn(hash).map(|f| (f, m.id()))) } /// Does the specified [`TypeId`][std::any::TypeId] iterator exist in the stack of - /// globally-imported [modules][Module]? + /// globally-imported [modules][crate::Module]? + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] #[allow(dead_code)] #[inline] #[must_use] - pub fn contains_iter(&self, id: TypeId) -> bool { + pub fn contains_iter(&self, id: std::any::TypeId) -> bool { self.modules.iter().any(|m| m.contains_qualified_iter(id)) } /// Get the specified [`TypeId`][std::any::TypeId] iterator from the stack of globally-imported - /// [modules][Module]. + /// [modules][crate::Module]. + /// + /// Not available under `no_module`. + #[cfg(not(feature = "no_module"))] #[inline] #[must_use] - pub fn get_iter(&self, id: TypeId) -> Option { + pub fn get_iter(&self, id: std::any::TypeId) -> Option<&crate::func::IteratorFn> { self.modules .iter() .rev() .find_map(|m| m.get_qualified_iter(id)) } - /// Get a mutable reference to the cache of globally-defined constants. - #[cfg(not(feature = "no_module"))] - #[cfg(not(feature = "no_function"))] + /// Get the current source. + #[inline] #[must_use] - pub(crate) fn constants_mut<'a>( - &'a mut self, - ) -> Option< - impl std::ops::DerefMut> + 'a, - > { - if let Some(ref global_constants) = self.constants { - Some(crate::func::native::shared_write_lock(global_constants)) - } else { - None + pub fn source(&self) -> Option<&str> { + match self.source.as_str() { + "" => None, + s => Some(s), } } - /// Set a constant into the cache of globally-defined constants. - #[cfg(not(feature = "no_module"))] - #[cfg(not(feature = "no_function"))] - pub(crate) fn set_constant(&mut self, name: impl Into, value: crate::Dynamic) { - if self.constants.is_none() { - let dict: crate::Locked<_> = std::collections::BTreeMap::new().into(); - self.constants = Some(dict.into()); - } - - crate::func::native::shared_write_lock(self.constants.as_mut().expect("`Some`")) - .insert(name.into(), value); - } /// Get the pre-calculated index getter hash. #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] #[must_use] @@ -234,10 +272,13 @@ impl GlobalRuntimeState<'_> { } } +#[cfg(not(feature = "no_module"))] impl IntoIterator for GlobalRuntimeState<'_> { - type Item = (Identifier, Shared); - type IntoIter = - Zip>, Rev; 3]>>>; + type Item = (Identifier, crate::Shared); + type IntoIter = std::iter::Zip< + std::iter::Rev>, + std::iter::Rev; 3]>>, + >; #[inline] fn into_iter(self) -> Self::IntoIter { @@ -248,22 +289,16 @@ impl IntoIterator for GlobalRuntimeState<'_> { } } -impl, M: Into>> FromIterator<(K, M)> for GlobalRuntimeState<'_> { - #[inline] - fn from_iter>(iter: T) -> Self { - let mut lib = Self::new(); - lib.extend(iter); - lib - } -} - -impl, M: Into>> Extend<(K, M)> for GlobalRuntimeState<'_> { +#[cfg(not(feature = "no_module"))] +impl, M: Into>> Extend<(K, M)> + for GlobalRuntimeState<'_> +{ #[inline] fn extend>(&mut self, iter: T) { - iter.into_iter().for_each(|(k, m)| { + for (k, m) in iter { self.keys.push(k.into()); self.modules.push(m.into()); - }) + } } } @@ -272,8 +307,10 @@ impl fmt::Debug for GlobalRuntimeState<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut f = f.debug_struct("GlobalRuntimeState"); - f.field("imports", &self.keys.iter().zip(self.modules.iter())) - .field("source", &self.source) + #[cfg(not(feature = "no_module"))] + f.field("imports", &self.keys.iter().zip(self.modules.iter())); + + f.field("source", &self.source) .field("num_operations", &self.num_operations) .field("num_modules_loaded", &self.num_modules_loaded); diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 9429672e..b76619e4 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -1,5 +1,6 @@ mod chaining; mod data_check; +mod debugger; mod eval_context; mod eval_state; mod expr; @@ -9,6 +10,11 @@ mod target; #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] pub use chaining::{ChainArgument, ChainType}; +#[cfg(feature = "debugging")] +#[cfg(not(feature = "no_function"))] +pub use debugger::CallStackFrame; +#[cfg(feature = "debugging")] +pub use debugger::{BreakPoint, Debugger, DebuggerCommand, OnDebuggerCallback, OnDebuggingInit}; pub use eval_context::EvalContext; pub use eval_state::EvalState; pub use global_state::GlobalRuntimeState; diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index 96aa85bc..a5dd2da4 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -1,7 +1,9 @@ //! Module defining functions for evaluating a statement. use super::{EvalState, GlobalRuntimeState, Target}; -use crate::ast::{Expr, Ident, OpAssignment, Stmt, AST_OPTION_FLAGS::*}; +use crate::ast::{ + BinaryExpr, Expr, Ident, OpAssignment, Stmt, SwitchCases, TryCatchBlock, AST_OPTION_FLAGS::*, +}; use crate::func::get_hasher; use crate::types::dynamic::{AccessMode, Union}; use crate::{Dynamic, Engine, Module, Position, RhaiResult, RhaiResultOf, Scope, ERR, INT}; @@ -11,6 +13,13 @@ use std::prelude::v1::*; impl Engine { /// Evaluate a statements block. + // + // # Implementation Notes + // + // Do not use the `?` operator within the main body as it makes this function return early, + // possibly by-passing important cleanup tasks at the end. + // + // Errors that are not recoverable, such as system errors or safety errors, can use `?`. pub(crate) fn eval_stmt_block( &self, scope: &mut Scope, @@ -28,7 +37,8 @@ impl Engine { let orig_always_search_scope = state.always_search_scope; let orig_scope_len = scope.len(); - let orig_mods_len = global.num_imports(); + #[cfg(not(feature = "no_module"))] + let orig_imports_len = global.num_imports(); let orig_fn_resolution_caches_len = state.fn_resolution_caches_len(); if restore_orig_state { @@ -38,7 +48,8 @@ impl Engine { let mut result = Ok(Dynamic::UNIT); for stmt in statements { - let _mods_len = global.num_imports(); + #[cfg(not(feature = "no_module"))] + let imports_len = global.num_imports(); result = self.eval_stmt( scope, @@ -61,7 +72,7 @@ impl Engine { // Without global functions, the extra modules never affect function resolution. if global .scan_imports_raw() - .skip(_mods_len) + .skip(imports_len) .any(|(_, m)| m.contains_indexed_global_functions()) { if state.fn_resolution_caches_len() > orig_fn_resolution_caches_len { @@ -86,7 +97,8 @@ impl Engine { if restore_orig_state { scope.rewind(orig_scope_len); state.scope_level -= 1; - global.truncate_imports(orig_mods_len); + #[cfg(not(feature = "no_module"))] + global.truncate_imports(orig_imports_len); // The impact of new local variables goes away at the end of a block // because any new variables introduced will go out of scope @@ -119,6 +131,7 @@ impl Engine { if let Some(OpAssignment { hash_op_assign, hash_op, + op_assign, op, }) = op_info { @@ -140,21 +153,23 @@ impl Engine { let hash = hash_op_assign; let args = &mut [lhs_ptr_inner, &mut new_val]; - match self.call_native_fn(global, state, lib, op, hash, args, true, true, op_pos) { + match self.call_native_fn( + global, state, lib, op_assign, hash, args, true, true, op_pos, + ) { Ok(_) => { #[cfg(not(feature = "unchecked"))] - self.check_data_size(&mut args[0], root.1)?; + self.check_data_size(&args[0], root.1)?; } - Err(err) if matches!(*err, ERR::ErrorFunctionNotFound(ref f, _) if f.starts_with(op)) => + Err(err) if matches!(*err, ERR::ErrorFunctionNotFound(ref f, _) if f.starts_with(op_assign)) => { // Expand to `var = var op rhs` - let op = &op[..op.len() - 1]; // extract operator without = - - // Run function let (value, _) = self.call_native_fn( global, state, lib, op, hash_op, args, true, false, op_pos, )?; + #[cfg(not(feature = "unchecked"))] + self.check_data_size(&value, root.1)?; + *args[0] = value.flatten(); } Err(err) => return Err(err), @@ -168,11 +183,13 @@ impl Engine { } /// Evaluate a statement. - /// - /// # Safety - /// - /// This method uses some unsafe code, mainly for avoiding cloning of local variable names via - /// direct lifetime casting. + // + // # Implementation Notes + // + // Do not use the `?` operator within the main body as it makes this function return early, + // possibly by-passing important cleanup tasks at the end. + // + // Errors that are not recoverable, such as system errors or safety errors, can use `?`. pub(crate) fn eval_stmt( &self, scope: &mut Scope, @@ -184,6 +201,10 @@ impl Engine { rewind_scope: bool, level: usize, ) -> RhaiResult { + #[cfg(feature = "debugging")] + let reset_debugger = + self.run_debugger_with_reset(scope, global, state, lib, this_ptr, stmt, level)?; + // Coded this way for better branch prediction. // Popular branches are lifted out of the `match` statement into their own branches. @@ -192,7 +213,13 @@ impl Engine { #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, stmt.position())?; - return self.eval_fn_call_expr(scope, global, state, lib, this_ptr, x, *pos, level); + let result = + self.eval_fn_call_expr(scope, global, state, lib, this_ptr, x, *pos, level); + + #[cfg(feature = "debugging")] + global.debugger.reset_status(reset_debugger); + + return result; } // Then assignments. @@ -202,81 +229,103 @@ impl Engine { #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, stmt.position())?; - return if x.0.is_variable_access(false) { - let (lhs_expr, op_info, rhs_expr) = x.as_ref(); - let rhs_val = self - .eval_expr(scope, global, state, lib, this_ptr, rhs_expr, level)? - .flatten(); - let (mut lhs_ptr, pos) = - self.search_namespace(scope, global, state, lib, this_ptr, lhs_expr)?; + let result = if x.1.lhs.is_variable_access(false) { + let (op_info, BinaryExpr { lhs, rhs }) = x.as_ref(); - let var_name = lhs_expr.get_variable_name(false).expect("`Expr::Variable`"); + let rhs_result = self + .eval_expr(scope, global, state, lib, this_ptr, rhs, level) + .map(Dynamic::flatten); - if !lhs_ptr.is_ref() { - return Err(ERR::ErrorAssignmentToConstant(var_name.to_string(), pos).into()); + if let Ok(rhs_val) = rhs_result { + let search_result = + self.search_namespace(scope, global, state, lib, this_ptr, lhs); + + if let Ok(search_val) = search_result { + let (mut lhs_ptr, pos) = search_val; + + let var_name = lhs.get_variable_name(false).expect("`Expr::Variable`"); + + if !lhs_ptr.is_ref() { + return Err( + ERR::ErrorAssignmentToConstant(var_name.to_string(), pos).into() + ); + } + + #[cfg(not(feature = "unchecked"))] + self.inc_operations(&mut global.num_operations, pos)?; + + self.eval_op_assignment( + global, + state, + lib, + *op_info, + *op_pos, + &mut lhs_ptr, + (var_name, pos), + rhs_val, + ) + .map_err(|err| err.fill_position(rhs.position())) + .map(|_| Dynamic::UNIT) + } else { + search_result.map(|_| Dynamic::UNIT) + } + } else { + rhs_result } - - #[cfg(not(feature = "unchecked"))] - self.inc_operations(&mut global.num_operations, pos)?; - - self.eval_op_assignment( - global, - state, - lib, - *op_info, - *op_pos, - &mut lhs_ptr, - (var_name, pos), - rhs_val, - ) - .map_err(|err| err.fill_position(rhs_expr.position()))?; - - Ok(Dynamic::UNIT) } else { - let (lhs_expr, op_info, rhs_expr) = x.as_ref(); - let rhs_val = self - .eval_expr(scope, global, state, lib, this_ptr, rhs_expr, level)? - .flatten(); - let _new_val = Some(((rhs_val, rhs_expr.position()), (*op_info, *op_pos))); + let (op_info, BinaryExpr { lhs, rhs }) = x.as_ref(); - // Must be either `var[index] op= val` or `var.prop op= val` - match lhs_expr { - // name op= rhs (handled above) - Expr::Variable(_, _, _) => { - unreachable!("Expr::Variable case is already handled") + let rhs_result = self + .eval_expr(scope, global, state, lib, this_ptr, rhs, level) + .map(Dynamic::flatten); + + if let Ok(rhs_val) = rhs_result { + let _new_val = Some(((rhs_val, rhs.position()), (*op_info, *op_pos))); + + // Must be either `var[index] op= val` or `var.prop op= val` + match lhs { + // name op= rhs (handled above) + Expr::Variable(_, _, _) => { + unreachable!("Expr::Variable case is already handled") + } + // idx_lhs[idx_expr] op= rhs + #[cfg(not(feature = "no_index"))] + Expr::Index(_, _, _) => self + .eval_dot_index_chain( + scope, global, state, lib, this_ptr, lhs, level, _new_val, + ) + .map(|_| Dynamic::UNIT), + // dot_lhs.dot_rhs op= rhs + #[cfg(not(feature = "no_object"))] + Expr::Dot(_, _, _) => self + .eval_dot_index_chain( + scope, global, state, lib, this_ptr, lhs, level, _new_val, + ) + .map(|_| Dynamic::UNIT), + _ => unreachable!("cannot assign to expression: {:?}", lhs), } - // idx_lhs[idx_expr] op= rhs - #[cfg(not(feature = "no_index"))] - Expr::Index(_, _, _) => { - self.eval_dot_index_chain( - scope, global, state, lib, this_ptr, lhs_expr, level, _new_val, - )?; - Ok(Dynamic::UNIT) - } - // dot_lhs.dot_rhs op= rhs - #[cfg(not(feature = "no_object"))] - Expr::Dot(_, _, _) => { - self.eval_dot_index_chain( - scope, global, state, lib, this_ptr, lhs_expr, level, _new_val, - )?; - Ok(Dynamic::UNIT) - } - _ => unreachable!("cannot assign to expression: {:?}", lhs_expr), + } else { + rhs_result } }; + + #[cfg(feature = "debugging")] + global.debugger.reset_status(reset_debugger); + + return result; } #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, stmt.position())?; - match stmt { + let result = match stmt { // No-op Stmt::Noop(_) => Ok(Dynamic::UNIT), // Expression as statement - Stmt::Expr(expr) => Ok(self - .eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .flatten()), + Stmt::Expr(expr) => self + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .map(Dynamic::flatten), // Block scope Stmt::Block(statements, _) if statements.is_empty() => Ok(Dynamic::UNIT), @@ -287,108 +336,146 @@ impl Engine { // If statement Stmt::If(expr, x, _) => { let guard_val = self - .eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, expr.position()))?; + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::(typ, expr.position()) + }) + }); - if guard_val { - if !x.0.is_empty() { - self.eval_stmt_block(scope, global, state, lib, this_ptr, &x.0, true, level) - } else { - Ok(Dynamic::UNIT) + match guard_val { + Ok(true) => { + if !x.0.is_empty() { + self.eval_stmt_block( + scope, global, state, lib, this_ptr, &x.0, true, level, + ) + } else { + Ok(Dynamic::UNIT) + } } - } else { - if !x.1.is_empty() { - self.eval_stmt_block(scope, global, state, lib, this_ptr, &x.1, true, level) - } else { - Ok(Dynamic::UNIT) + Ok(false) => { + if !x.1.is_empty() { + self.eval_stmt_block( + scope, global, state, lib, this_ptr, &x.1, true, level, + ) + } else { + Ok(Dynamic::UNIT) + } } + err => err.map(Into::into), } } // Switch statement Stmt::Switch(match_expr, x, _) => { - let (table, def_stmt, ranges) = x.as_ref(); + let SwitchCases { + cases, + def_case, + ranges, + } = x.as_ref(); - let value = - self.eval_expr(scope, global, state, lib, this_ptr, match_expr, level)?; + let value_result = + self.eval_expr(scope, global, state, lib, this_ptr, match_expr, level); - let stmt_block = if value.is_hashable() { - let hasher = &mut get_hasher(); - value.hash(hasher); - let hash = hasher.finish(); + if let Ok(value) = value_result { + let stmt_block_result = if value.is_hashable() { + let hasher = &mut get_hasher(); + value.hash(hasher); + let hash = hasher.finish(); - // First check hashes - if let Some(t) = table.get(&hash) { - if let Some(ref c) = t.0 { - if self - .eval_expr(scope, global, state, lib, this_ptr, &c, level)? - .as_bool() - .map_err(|typ| { - self.make_type_mismatch_err::(typ, c.position()) - })? + // First check hashes + if let Some(t) = cases.get(&hash) { + let cond_result = t + .condition + .as_ref() + .map(|cond| { + self.eval_expr(scope, global, state, lib, this_ptr, cond, level) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::( + typ, + cond.position(), + ) + }) + }) + }) + .unwrap_or(Ok(true)); + + match cond_result { + Ok(true) => Ok(Some(&t.statements)), + Ok(false) => Ok(None), + _ => cond_result.map(|_| None), + } + } else if value.is::() && !ranges.is_empty() { + // Then check integer ranges + let value = value.as_int().expect("`INT`"); + let mut result = Ok(None); + + for (_, _, _, block) in + ranges.iter().filter(|&&(start, end, inclusive, _)| { + (!inclusive && (start..end).contains(&value)) + || (inclusive && (start..=end).contains(&value)) + }) { - Some(&t.1) - } else { - None - } - } else { - Some(&t.1) - } - } else if value.is::() && !ranges.is_empty() { - // Then check integer ranges - let value = value.as_int().expect("`INT`"); - let mut result = None; + let cond_result = block + .condition + .as_ref() + .map(|cond| { + self.eval_expr( + scope, global, state, lib, this_ptr, cond, level, + ) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::( + typ, + cond.position(), + ) + }) + }) + }) + .unwrap_or(Ok(true)); - for (_, _, _, condition, stmt_block) in - ranges.iter().filter(|&&(start, end, inclusive, _, _)| { - (!inclusive && (start..end).contains(&value)) - || (inclusive && (start..=end).contains(&value)) - }) - { - if let Some(c) = condition { - if !self - .eval_expr(scope, global, state, lib, this_ptr, &c, level)? - .as_bool() - .map_err(|typ| { - self.make_type_mismatch_err::(typ, c.position()) - })? - { - continue; + match cond_result { + Ok(true) => result = Ok(Some(&block.statements)), + Ok(false) => continue, + _ => result = cond_result.map(|_| None), } + + break; } - result = Some(stmt_block); - break; + result + } else { + // Nothing matches + Ok(None) } - - result } else { - // Nothing matches - None + // Non-hashable + Ok(None) + }; + + if let Ok(Some(statements)) = stmt_block_result { + if !statements.is_empty() { + self.eval_stmt_block( + scope, global, state, lib, this_ptr, statements, true, level, + ) + } else { + Ok(Dynamic::UNIT) + } + } else if let Ok(None) = stmt_block_result { + // Default match clause + if !def_case.is_empty() { + self.eval_stmt_block( + scope, global, state, lib, this_ptr, def_case, true, level, + ) + } else { + Ok(Dynamic::UNIT) + } + } else { + stmt_block_result.map(|_| Dynamic::UNIT) } } else { - // Non-hashable - None - }; - - if let Some(statements) = stmt_block { - if !statements.is_empty() { - self.eval_stmt_block( - scope, global, state, lib, this_ptr, statements, true, level, - ) - } else { - Ok(Dynamic::UNIT) - } - } else { - // Default match clause - if !def_stmt.is_empty() { - self.eval_stmt_block( - scope, global, state, lib, this_ptr, def_stmt, true, level, - ) - } else { - Ok(Dynamic::UNIT) - } + value_result } } @@ -401,8 +488,8 @@ impl Engine { Ok(_) => (), Err(err) => match *err { ERR::LoopBreak(false, _) => (), - ERR::LoopBreak(true, _) => return Ok(Dynamic::UNIT), - _ => return Err(err), + ERR::LoopBreak(true, _) => break Ok(Dynamic::UNIT), + _ => break Err(err), }, } } else { @@ -414,24 +501,29 @@ impl Engine { // While loop Stmt::While(expr, body, _) => loop { let condition = self - .eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, expr.position()))?; + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::(typ, expr.position()) + }) + }); - if !condition { - return Ok(Dynamic::UNIT); - } - if !body.is_empty() { - match self - .eval_stmt_block(scope, global, state, lib, this_ptr, body, true, level) - { - Ok(_) => (), - Err(err) => match *err { - ERR::LoopBreak(false, _) => (), - ERR::LoopBreak(true, _) => return Ok(Dynamic::UNIT), - _ => return Err(err), - }, + match condition { + Ok(false) => break Ok(Dynamic::UNIT), + Ok(true) if body.is_empty() => (), + Ok(true) => { + match self + .eval_stmt_block(scope, global, state, lib, this_ptr, body, true, level) + { + Ok(_) => (), + Err(err) => match *err { + ERR::LoopBreak(false, _) => (), + ERR::LoopBreak(true, _) => break Ok(Dynamic::UNIT), + _ => break Err(err), + }, + } } + err => break err.map(|_| Dynamic::UNIT), } }, @@ -446,157 +538,173 @@ impl Engine { Ok(_) => (), Err(err) => match *err { ERR::LoopBreak(false, _) => continue, - ERR::LoopBreak(true, _) => return Ok(Dynamic::UNIT), - _ => return Err(err), + ERR::LoopBreak(true, _) => break Ok(Dynamic::UNIT), + _ => break Err(err), }, } } let condition = self - .eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, expr.position()))?; + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::(typ, expr.position()) + }) + }); - if condition ^ is_while { - return Ok(Dynamic::UNIT); + match condition { + Ok(condition) if condition ^ is_while => break Ok(Dynamic::UNIT), + Ok(_) => (), + err => break err.map(|_| Dynamic::UNIT), } }, // For loop Stmt::For(expr, x, _) => { let (Ident { name: var_name, .. }, counter, statements) = x.as_ref(); - let iter_obj = self - .eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .flatten(); - let iter_type = iter_obj.type_id(); - // lib should only contain scripts, so technically they cannot have iterators + let iter_result = self + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .map(Dynamic::flatten); - // Search order: - // 1) Global namespace - functions registered via Engine::register_XXX - // 2) Global modules - packages - // 3) Imported modules - functions marked with global namespace - // 4) Global sub-modules - functions marked with global namespace - let func = self - .global_modules - .iter() - .find_map(|m| m.get_iter(iter_type)) - .or_else(|| global.get_iter(iter_type)) - .or_else(|| { + if let Ok(iter_obj) = iter_result { + let iter_type = iter_obj.type_id(); + + // lib should only contain scripts, so technically they cannot have iterators + + // Search order: + // 1) Global namespace - functions registered via Engine::register_XXX + // 2) Global modules - packages + // 3) Imported modules - functions marked with global namespace + // 4) Global sub-modules - functions marked with global namespace + let func = self + .global_modules + .iter() + .find_map(|m| m.get_iter(iter_type)); + + #[cfg(not(feature = "no_module"))] + let func = func.or_else(|| global.get_iter(iter_type)).or_else(|| { self.global_sub_modules .values() .find_map(|m| m.get_qualified_iter(iter_type)) }); - if let Some(func) = func { - // Add the loop variables - let orig_scope_len = scope.len(); - let counter_index = if let Some(counter) = counter { - scope.push(counter.name.clone(), 0 as INT); - scope.len() - 1 - } else { - usize::MAX - }; + if let Some(func) = func { + // Add the loop variables + let orig_scope_len = scope.len(); + let counter_index = if let Some(counter) = counter { + scope.push(counter.name.clone(), 0 as INT); + scope.len() - 1 + } else { + usize::MAX + }; - scope.push(var_name.clone(), ()); - let index = scope.len() - 1; + scope.push(var_name.clone(), ()); + let index = scope.len() - 1; - let mut loop_result = Ok(Dynamic::UNIT); + let mut loop_result = Ok(Dynamic::UNIT); - for (x, iter_value) in func(iter_obj).enumerate() { - // Increment counter - if counter_index < usize::MAX { - #[cfg(not(feature = "unchecked"))] - if x > INT::MAX as usize { - loop_result = Err(ERR::ErrorArithmetic( - format!("for-loop counter overflow: {}", x), - counter.as_ref().expect("`Some`").pos, - ) - .into()); - break; + for (x, iter_value) in func(iter_obj).enumerate() { + // Increment counter + if counter_index < usize::MAX { + #[cfg(not(feature = "unchecked"))] + if x > INT::MAX as usize { + loop_result = Err(ERR::ErrorArithmetic( + format!("for-loop counter overflow: {}", x), + counter.as_ref().expect("`Some`").pos, + ) + .into()); + break; + } + + let index_value = (x as INT).into(); + + #[cfg(not(feature = "no_closure"))] + { + let index_var = scope.get_mut_by_index(counter_index); + if index_var.is_shared() { + *index_var.write_lock().expect("`Dynamic`") = index_value; + } else { + *index_var = index_value; + } + } + #[cfg(feature = "no_closure")] + { + *scope.get_mut_by_index(counter_index) = index_value; + } } - let index_value = (x as INT).into(); + let value = iter_value.flatten(); #[cfg(not(feature = "no_closure"))] { - let index_var = scope.get_mut_by_index(counter_index); - if index_var.is_shared() { - *index_var.write_lock().expect("`Dynamic`") = index_value; + let loop_var = scope.get_mut_by_index(index); + if loop_var.is_shared() { + *loop_var.write_lock().expect("`Dynamic`") = value; } else { - *index_var = index_value; + *loop_var = value; } } #[cfg(feature = "no_closure")] { - *scope.get_mut_by_index(counter_index) = index_value; + *scope.get_mut_by_index(index) = value; + } + + #[cfg(not(feature = "unchecked"))] + if let Err(err) = self + .inc_operations(&mut global.num_operations, statements.position()) + { + loop_result = Err(err); + break; + } + + if statements.is_empty() { + continue; + } + + let result = self.eval_stmt_block( + scope, global, state, lib, this_ptr, statements, true, level, + ); + + match result { + Ok(_) => (), + Err(err) => match *err { + ERR::LoopBreak(false, _) => (), + ERR::LoopBreak(true, _) => break, + _ => { + loop_result = Err(err); + break; + } + }, } } - let value = iter_value.flatten(); + scope.rewind(orig_scope_len); - #[cfg(not(feature = "no_closure"))] - { - let loop_var = scope.get_mut_by_index(index); - if loop_var.is_shared() { - *loop_var.write_lock().expect("`Dynamic`") = value; - } else { - *loop_var = value; - } - } - #[cfg(feature = "no_closure")] - { - *scope.get_mut_by_index(index) = value; - } - - #[cfg(not(feature = "unchecked"))] - if let Err(err) = - self.inc_operations(&mut global.num_operations, statements.position()) - { - loop_result = Err(err); - break; - } - - if statements.is_empty() { - continue; - } - - let result = self.eval_stmt_block( - scope, global, state, lib, this_ptr, statements, true, level, - ); - - match result { - Ok(_) => (), - Err(err) => match *err { - ERR::LoopBreak(false, _) => (), - ERR::LoopBreak(true, _) => break, - _ => { - loop_result = Err(err); - break; - } - }, - } + loop_result + } else { + Err(ERR::ErrorFor(expr.position()).into()) } - - scope.rewind(orig_scope_len); - - loop_result } else { - Err(ERR::ErrorFor(expr.position()).into()) + iter_result } } // Continue/Break statement Stmt::BreakLoop(options, pos) => { - Err(ERR::LoopBreak(options.contains(AST_OPTION_BREAK_OUT), *pos).into()) + Err(ERR::LoopBreak(options.contains(AST_OPTION_BREAK), *pos).into()) } // Try/Catch statement Stmt::TryCatch(x, _) => { - let (try_stmt, err_var_name, catch_stmt) = x.as_ref(); + let TryCatchBlock { + try_block, + catch_var, + catch_block, + } = x.as_ref(); let result = self - .eval_stmt_block(scope, global, state, lib, this_ptr, try_stmt, true, level) + .eval_stmt_block(scope, global, state, lib, this_ptr, try_block, true, level) .map(|_| Dynamic::UNIT); match result { @@ -623,17 +731,15 @@ impl Engine { err_map.insert("source".into(), global.source.clone().into()); } - if err_pos.is_none() { - // No position info - } else { - let line = err_pos.line().unwrap() as INT; - let position = if err_pos.is_beginning_of_line() { - 0 - } else { - err_pos.position().unwrap() - } as INT; - err_map.insert("line".into(), line.into()); - err_map.insert("position".into(), position.into()); + if !err_pos.is_none() { + err_map.insert( + "line".into(), + (err_pos.line().unwrap() as INT).into(), + ); + err_map.insert( + "position".into(), + (err_pos.position().unwrap_or(0) as INT).into(), + ); } err.dump_fields(&mut err_map); @@ -643,12 +749,19 @@ impl Engine { let orig_scope_len = scope.len(); - err_var_name + catch_var .as_ref() .map(|Ident { name, .. }| scope.push(name.clone(), err_value)); let result = self.eval_stmt_block( - scope, global, state, lib, this_ptr, catch_stmt, true, level, + scope, + global, + state, + lib, + this_ptr, + catch_block, + true, + level, ); scope.rewind(orig_scope_len); @@ -669,27 +782,19 @@ impl Engine { } // Throw value - Stmt::Return(options, Some(expr), pos) if options.contains(AST_OPTION_BREAK_OUT) => { - Err(ERR::ErrorRuntime( - self.eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .flatten(), - *pos, - ) - .into()) - } + Stmt::Return(options, Some(expr), pos) if options.contains(AST_OPTION_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(options, None, pos) if options.contains(AST_OPTION_BREAK_OUT) => { + Stmt::Return(options, None, pos) if options.contains(AST_OPTION_BREAK) => { Err(ERR::ErrorRuntime(Dynamic::UNIT, *pos).into()) } // Return value - Stmt::Return(_, Some(expr), pos) => Err(ERR::Return( - self.eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .flatten(), - *pos, - ) - .into()), + Stmt::Return(_, Some(expr), pos) => self + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .and_then(|v| Err(ERR::Return(v.flatten(), *pos).into())), // Empty return Stmt::Return(_, None, pos) => Err(ERR::Return(Dynamic::UNIT, *pos).into()), @@ -702,42 +807,51 @@ impl Engine { } else { AccessMode::ReadWrite }; - let export = options.contains(AST_OPTION_PUBLIC); + let export = options.contains(AST_OPTION_EXPORTED); - let value = self - .eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .flatten(); + let value_result = self + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .map(Dynamic::flatten); - let _alias = if !rewind_scope { - #[cfg(not(feature = "no_function"))] - #[cfg(not(feature = "no_module"))] - if state.scope_level == 0 - && entry_type == AccessMode::ReadOnly - && lib.iter().any(|&m| !m.is_empty()) - { - // Add a global constant if at top level and there are functions - global.set_constant(var_name.clone(), value.clone()); - } + if let Ok(value) = value_result { + let _alias = if !rewind_scope { + #[cfg(not(feature = "no_function"))] + #[cfg(not(feature = "no_module"))] + if state.scope_level == 0 + && entry_type == AccessMode::ReadOnly + && lib.iter().any(|&m| !m.is_empty()) + { + if global.constants.is_none() { + global.constants = Some(crate::Shared::new(crate::Locked::new( + std::collections::BTreeMap::new(), + ))); + } + crate::func::locked_write(global.constants.as_ref().unwrap()) + .insert(var_name.clone(), value.clone()); + } - if export { - Some(var_name) + if export { + Some(var_name) + } else { + None + } + } else if export { + unreachable!("exported variable not on global level"); } else { None + }; + + scope.push_dynamic_value(var_name.clone(), entry_type, value); + + #[cfg(not(feature = "no_module"))] + if let Some(alias) = _alias { + scope.add_entry_alias(scope.len() - 1, alias.clone()); } - } else if export { - unreachable!("exported variable not on global level"); + + Ok(Dynamic::UNIT) } else { - None - }; - - scope.push_dynamic_value(var_name.clone(), entry_type, value); - - #[cfg(not(feature = "no_module"))] - if let Some(alias) = _alias { - scope.add_entry_alias(scope.len() - 1, alias.clone()); + value_result } - - Ok(Dynamic::UNIT) } // Import statement @@ -749,71 +863,76 @@ impl Engine { return Err(ERR::ErrorTooManyModules(*_pos).into()); } - if let Some(path) = self - .eval_expr(scope, global, state, lib, this_ptr, &expr, level)? - .try_cast::() - { + let path_result = self + .eval_expr(scope, global, state, lib, this_ptr, &expr, level) + .and_then(|v| { + v.try_cast::().ok_or_else(|| { + self.make_type_mismatch_err::( + "", + expr.position(), + ) + }) + }); + + if let Ok(path) = path_result { use crate::ModuleResolver; - let source = match global.source.as_str() { - "" => None, - s => Some(s), - }; let path_pos = expr.position(); - let module = global - .embedded_module_resolver + let resolver = global.embedded_module_resolver.clone(); + + let module_result = resolver .as_ref() - .and_then(|r| match r.resolve(self, source, &path, path_pos) { + .and_then(|r| match r.resolve_raw(self, global, &path, path_pos) { Err(err) if matches!(*err, ERR::ErrorModuleNotFound(_, _)) => None, result => Some(result), }) .or_else(|| { self.module_resolver .as_ref() - .map(|r| r.resolve(self, source, &path, path_pos)) + .map(|r| r.resolve_raw(self, global, &path, path_pos)) }) .unwrap_or_else(|| { Err(ERR::ErrorModuleNotFound(path.to_string(), path_pos).into()) - })?; + }); - if let Some(name) = export.as_ref().map(|x| x.name.clone()) { - if !module.is_indexed() { - // Index the module (making a clone copy if necessary) if it is not indexed - let mut module = crate::func::native::shared_take_or_clone(module); - module.build_index(); - global.push_import(name, module); - } else { - global.push_import(name, module); + if let Ok(module) = module_result { + if let Some(name) = export.as_ref().map(|x| x.name.clone()) { + if !module.is_indexed() { + // Index the module (making a clone copy if necessary) if it is not indexed + let mut module = crate::func::native::shared_take_or_clone(module); + module.build_index(); + global.push_import(name, module); + } else { + global.push_import(name, module); + } } + + global.num_modules_loaded += 1; + + Ok(Dynamic::UNIT) + } else { + module_result.map(|_| Dynamic::UNIT) } - - global.num_modules_loaded += 1; - - Ok(Dynamic::UNIT) } else { - Err(self.make_type_mismatch_err::("", expr.position())) + path_result.map(|_| Dynamic::UNIT) } } // Export statement #[cfg(not(feature = "no_module"))] - Stmt::Export(list, _) => { - list.iter().try_for_each( - |(Ident { name, pos, .. }, Ident { name: rename, .. })| { - // Mark scope variables as public - if let Some((index, _)) = scope.get_index(name) { - scope.add_entry_alias( - index, - if rename.is_empty() { name } else { rename }.clone(), - ); - Ok(()) as RhaiResultOf<_> - } else { - Err(ERR::ErrorVariableNotFound(name.to_string(), *pos).into()) - } - }, - )?; - Ok(Dynamic::UNIT) + Stmt::Export(x, _) => { + let (Ident { name, pos, .. }, Ident { name: alias, .. }) = x.as_ref(); + // Mark scope variables as public + if let Some((index, _)) = scope.get_index(name) { + scope.add_entry_alias( + index, + if alias.is_empty() { name } else { alias }.clone(), + ); + Ok(Dynamic::UNIT) + } else { + Err(ERR::ErrorVariableNotFound(name.to_string(), *pos).into()) + } } // Share statement @@ -831,6 +950,11 @@ impl Engine { } _ => unreachable!("statement cannot be evaluated: {:?}", stmt), - } + }; + + #[cfg(feature = "debugging")] + global.debugger.reset_status(reset_debugger); + + return result; } } diff --git a/src/func/call.rs b/src/func/call.rs index b20a34f5..f7c45e27 100644 --- a/src/func/call.rs +++ b/src/func/call.rs @@ -10,8 +10,6 @@ use crate::engine::{ KEYWORD_IS_DEF_VAR, KEYWORD_PRINT, KEYWORD_TYPE_OF, }; use crate::eval::{EvalState, GlobalRuntimeState}; -use crate::module::Namespace; -use crate::tokenizer::Token; use crate::{ calc_fn_hash, calc_fn_params_hash, combine_hashes, Dynamic, Engine, FnArgsVec, FnPtr, Identifier, ImmutableString, Module, Position, RhaiResult, RhaiResultOf, Scope, ERR, @@ -150,18 +148,26 @@ impl Engine { #[must_use] fn gen_call_signature( &self, - namespace: Option<&Namespace>, + #[cfg(not(feature = "no_module"))] namespace: Option<&crate::module::Namespace>, fn_name: &str, args: &[&mut Dynamic], ) -> String { - format!( - "{}{}{} ({})", + #[cfg(not(feature = "no_module"))] + let (ns, sep) = ( namespace.map_or_else(|| String::new(), |ns| ns.to_string()), if namespace.is_some() { - Token::DoubleColon.literal_syntax() + crate::tokenizer::Token::DoubleColon.literal_syntax() } else { "" }, + ); + #[cfg(feature = "no_module")] + let (ns, sep) = ("", ""); + + format!( + "{}{}{} ({})", + ns, + sep, fn_name, args.iter() .map(|a| if a.is::() { @@ -194,6 +200,8 @@ impl Engine { allow_dynamic: bool, is_op_assignment: bool, ) -> Option<&'s FnResolutionCacheEntry> { + let _global = global; + if hash_script == 0 { return None; } @@ -233,9 +241,12 @@ impl Engine { source: m.id_raw().clone(), }) }) - }) + }); + + #[cfg(not(feature = "no_module"))] + let func = func .or_else(|| { - global.get_qualified_fn(hash).map(|(func, source)| { + _global.get_qualified_fn(hash).map(|(func, source)| { FnResolutionCacheEntry { func: func.clone(), source: source @@ -506,9 +517,16 @@ impl Engine { } // Raise error - _ => Err( - ERR::ErrorFunctionNotFound(self.gen_call_signature(None, name, args), pos).into(), - ), + _ => Err(ERR::ErrorFunctionNotFound( + self.gen_call_signature( + #[cfg(not(feature = "no_module"))] + None, + name, + args, + ), + pos, + ) + .into()), } } @@ -889,8 +907,12 @@ impl Engine { ) -> RhaiResultOf<(Dynamic, Position)> { Ok(( if let Expr::Stack(slot, _) = arg_expr { + #[cfg(feature = "debugging")] + self.run_debugger(scope, global, state, lib, this_ptr, arg_expr, level)?; constants[*slot].clone() } else if let Some(value) = arg_expr.get_literal_value() { + #[cfg(feature = "debugging")] + self.run_debugger(scope, global, state, lib, this_ptr, arg_expr, level)?; value } else { self.eval_expr(scope, global, state, lib, this_ptr, arg_expr, level)? @@ -1133,9 +1155,12 @@ impl Engine { // convert to method-call style in order to leverage potential &mut first argument and // avoid cloning the value if curry.is_empty() && first_arg.map_or(false, |expr| expr.is_variable_access(false)) { - // func(x, ...) -> x.func(...) let first_expr = first_arg.unwrap(); + #[cfg(feature = "debugging")] + self.run_debugger(scope, global, state, lib, this_ptr, first_expr, level)?; + + // func(x, ...) -> x.func(...) a_expr.iter().try_for_each(|expr| { self.get_arg_value(scope, global, state, lib, this_ptr, level, expr, constants) .map(|(value, _)| arg_values.push(value.flatten())) @@ -1189,6 +1214,7 @@ impl Engine { } /// Call a namespace-qualified function in normal function-call style. + #[cfg(not(feature = "no_module"))] pub(crate) fn make_qualified_function_call( &self, scope: &mut Scope, @@ -1196,7 +1222,7 @@ impl Engine { state: &mut EvalState, lib: &[&Module], this_ptr: &mut Option<&mut Dynamic>, - namespace: &Namespace, + namespace: &crate::module::Namespace, fn_name: &str, args_expr: &[Expr], constants: &[Dynamic], @@ -1215,6 +1241,9 @@ impl Engine { // If so, convert to method-call style in order to leverage potential // &mut first argument and avoid cloning the value if !args_expr.is_empty() && args_expr[0].is_variable_access(true) { + #[cfg(feature = "debugging")] + self.run_debugger(scope, global, state, lib, this_ptr, &args_expr[0], level)?; + // func(x, ...) -> x.func(...) arg_values.push(Dynamic::UNIT); diff --git a/src/func/callable_function.rs b/src/func/callable_function.rs index 0402ba87..3febb448 100644 --- a/src/func/callable_function.rs +++ b/src/func/callable_function.rs @@ -17,7 +17,7 @@ pub enum CallableFunction { /// and the rest passed by value. Method(Shared), /// An iterator function. - Iterator(IteratorFn), + Iterator(Shared), /// A plugin function, Plugin(Shared), /// A script-defined function. @@ -177,9 +177,9 @@ impl CallableFunction { /// Get a reference to an iterator function. #[inline] #[must_use] - pub fn get_iter_fn(&self) -> Option { + pub fn get_iter_fn(&self) -> Option<&IteratorFn> { match self { - Self::Iterator(f) => Some(*f), + Self::Iterator(f) => Some(f.as_ref()), Self::Pure(_) | Self::Method(_) | Self::Plugin(_) => None, #[cfg(not(feature = "no_function"))] @@ -218,13 +218,6 @@ impl CallableFunction { } } -impl From for CallableFunction { - #[inline(always)] - fn from(func: IteratorFn) -> Self { - Self::Iterator(func) - } -} - #[cfg(not(feature = "no_function"))] impl From for CallableFunction { #[inline(always)] diff --git a/src/func/mod.rs b/src/func/mod.rs index dcae0482..f4779861 100644 --- a/src/func/mod.rs +++ b/src/func/mod.rs @@ -22,7 +22,7 @@ pub use hashing::{ combine_hashes, get_hasher, }; pub use native::{ - shared_make_mut, shared_take, shared_take_or_clone, shared_try_take, shared_write_lock, FnAny, + locked_write, shared_make_mut, shared_take, shared_take_or_clone, shared_try_take, FnAny, FnPlugin, IteratorFn, Locked, NativeCallContext, SendSync, Shared, }; pub use plugin::PluginFunction; diff --git a/src/func/native.rs b/src/func/native.rs index 17b93eac..7938f4cd 100644 --- a/src/func/native.rs +++ b/src/func/native.rs @@ -215,7 +215,6 @@ impl<'a> NativeCallContext<'a> { /// /// Not available under `no_module`. #[cfg(feature = "internals")] - #[cfg(not(feature = "no_module"))] #[inline(always)] #[must_use] pub const fn global_runtime_state(&self) -> Option<&GlobalRuntimeState> { @@ -286,26 +285,29 @@ impl<'a> NativeCallContext<'a> { is_method_call: bool, args: &mut [&mut Dynamic], ) -> RhaiResult { + let mut global = self + .global + .cloned() + .unwrap_or_else(|| GlobalRuntimeState::new(self.engine())); + let mut state = EvalState::new(); + let fn_name = fn_name.as_ref(); - let len = args.len(); + let args_len = args.len(); let hash = if is_method_call { FnCallHashes::from_all( #[cfg(not(feature = "no_function"))] - calc_fn_hash(fn_name, len - 1), - calc_fn_hash(fn_name, len), + calc_fn_hash(fn_name, args_len - 1), + calc_fn_hash(fn_name, args_len), ) } else { - calc_fn_hash(fn_name, len).into() + calc_fn_hash(fn_name, args_len).into() }; self.engine() .exec_fn_call( - &mut self - .global - .cloned() - .unwrap_or_else(|| GlobalRuntimeState::new()), - &mut EvalState::new(), + &mut global, + &mut state, self.lib, fn_name, hash, @@ -356,11 +358,11 @@ pub fn shared_take(value: Shared) -> T { shared_try_take(value).ok().expect("not shared") } -/// Lock a [`Shared`] resource. +/// Lock a [`Locked`] resource. #[inline(always)] #[must_use] #[allow(dead_code)] -pub fn shared_write_lock<'a, T>(value: &'a Locked) -> LockGuard<'a, T> { +pub fn locked_write<'a, T>(value: &'a Locked) -> LockGuard<'a, T> { #[cfg(not(feature = "sync"))] return value.borrow_mut(); @@ -368,60 +370,62 @@ pub fn shared_write_lock<'a, T>(value: &'a Locked) -> LockGuard<'a, T> { return value.write().unwrap(); } -/// A general function trail object. +/// General function trail object. #[cfg(not(feature = "sync"))] pub type FnAny = dyn Fn(NativeCallContext, &mut FnCallArgs) -> RhaiResult; -/// A general function trail object. +/// General function trail object. #[cfg(feature = "sync")] pub type FnAny = dyn Fn(NativeCallContext, &mut FnCallArgs) -> RhaiResult + Send + Sync; -/// A trail object for built-in functions. +/// Trail object for built-in functions. pub type FnBuiltin = fn(NativeCallContext, &mut FnCallArgs) -> RhaiResult; -/// A standard function that gets an iterator from a type. -pub type IteratorFn = fn(Dynamic) -> Box>; +/// Function that gets an iterator from a type. +#[cfg(not(feature = "sync"))] +pub type IteratorFn = dyn Fn(Dynamic) -> Box>; +/// Function that gets an iterator from a type. +#[cfg(feature = "sync")] +pub type IteratorFn = dyn Fn(Dynamic) -> Box> + Send + Sync; #[cfg(not(feature = "sync"))] pub type FnPlugin = dyn PluginFunction; #[cfg(feature = "sync")] pub type FnPlugin = dyn PluginFunction + Send + Sync; -/// A standard callback function for progress reporting. +/// Callback function for progress reporting. #[cfg(not(feature = "unchecked"))] #[cfg(not(feature = "sync"))] -pub type OnProgressCallback = Box Option + 'static>; -/// A standard callback function for progress reporting. +pub type OnProgressCallback = dyn Fn(u64) -> Option; +/// Callback function for progress reporting. #[cfg(not(feature = "unchecked"))] #[cfg(feature = "sync")] -pub type OnProgressCallback = Box Option + Send + Sync + 'static>; +pub type OnProgressCallback = dyn Fn(u64) -> Option + Send + Sync; -/// A standard callback function for printing. +/// Callback function for printing. #[cfg(not(feature = "sync"))] -pub type OnPrintCallback = Box; -/// A standard callback function for printing. +pub type OnPrintCallback = dyn Fn(&str); +/// Callback function for printing. #[cfg(feature = "sync")] -pub type OnPrintCallback = Box; +pub type OnPrintCallback = dyn Fn(&str) + Send + Sync; -/// A standard callback function for debugging. +/// Callback function for debugging. #[cfg(not(feature = "sync"))] -pub type OnDebugCallback = Box, Position) + 'static>; -/// A standard callback function for debugging. +pub type OnDebugCallback = dyn Fn(&str, Option<&str>, Position); +/// Callback function for debugging. #[cfg(feature = "sync")] -pub type OnDebugCallback = Box, Position) + Send + Sync + 'static>; +pub type OnDebugCallback = dyn Fn(&str, Option<&str>, Position) + Send + Sync; -/// A standard callback function for mapping tokens during parsing. +/// Callback function for mapping tokens during parsing. #[cfg(not(feature = "sync"))] pub type OnParseTokenCallback = dyn Fn(Token, Position, &TokenizeState) -> Token; -/// A standard callback function for mapping tokens during parsing. +/// Callback function for mapping tokens during parsing. #[cfg(feature = "sync")] -pub type OnParseTokenCallback = - dyn Fn(Token, Position, &TokenizeState) -> Token + Send + Sync + 'static; +pub type OnParseTokenCallback = dyn Fn(Token, Position, &TokenizeState) -> Token + Send + Sync; -/// A standard callback function for variable access. +/// Callback function for variable access. #[cfg(not(feature = "sync"))] -pub type OnVarCallback = - Box RhaiResultOf> + 'static>; -/// A standard callback function for variable access. +pub type OnVarCallback = dyn Fn(&str, usize, &EvalContext) -> RhaiResultOf>; +/// Callback function for variable access. #[cfg(feature = "sync")] pub type OnVarCallback = - Box RhaiResultOf> + Send + Sync + 'static>; + dyn Fn(&str, usize, &EvalContext) -> RhaiResultOf> + Send + Sync; diff --git a/src/func/script.rs b/src/func/script.rs index 396dfedf..c087053e 100644 --- a/src/func/script.rs +++ b/src/func/script.rs @@ -70,7 +70,12 @@ impl Engine { } let orig_scope_len = scope.len(); - let orig_mods_len = global.num_imports(); + #[cfg(not(feature = "no_module"))] + let orig_imports_len = global.num_imports(); + + #[cfg(feature = "debugging")] + #[cfg(not(feature = "no_function"))] + let orig_call_stack_len = global.debugger.call_stack().len(); // Put arguments into scope as variables scope.extend(fn_def.params.iter().cloned().zip(args.into_iter().map(|v| { @@ -78,6 +83,20 @@ impl Engine { mem::take(*v) }))); + // Push a new call stack frame + #[cfg(feature = "debugging")] + #[cfg(not(feature = "no_function"))] + global.debugger.push_call_stack_frame( + fn_def.name.clone(), + scope + .iter() + .skip(orig_scope_len) + .map(|(_, _, v)| v.clone()) + .collect(), + global.source.clone(), + pos, + ); + // Merge in encapsulated environment, if any let mut lib_merged = StaticVec::with_capacity(lib.len() + 1); let orig_fn_resolution_caches_len = state.fn_resolution_caches_len(); @@ -97,10 +116,9 @@ impl Engine { #[cfg(not(feature = "no_module"))] if let Some(ref modules) = fn_def.global { - modules - .iter() - .cloned() - .for_each(|(n, m)| global.push_import(n, m)); + for (n, m) in modules.iter().cloned() { + global.push_import(n, m) + } } // Evaluate the function @@ -144,11 +162,17 @@ impl Engine { // Remove arguments only, leaving new variables in the scope scope.remove_range(orig_scope_len, args.len()) } - global.truncate_imports(orig_mods_len); + #[cfg(not(feature = "no_module"))] + global.truncate_imports(orig_imports_len); // Restore state state.rewind_fn_resolution_caches(orig_fn_resolution_caches_len); + // Pop the call stack + #[cfg(feature = "debugging")] + #[cfg(not(feature = "no_function"))] + global.debugger.rewind_call_stack(orig_call_stack_len); + result } @@ -161,6 +185,8 @@ impl Engine { lib: &[&Module], hash_script: u64, ) -> bool { + let _global = global; + let cache = state.fn_resolution_cache_mut(); if let Some(result) = cache.get(&hash_script).map(|v| v.is_some()) { @@ -170,9 +196,12 @@ impl Engine { // First check script-defined functions let result = lib.iter().any(|&m| m.contains_fn(hash_script)) // Then check the global namespace and packages - || self.global_modules.iter().any(|m| m.contains_fn(hash_script)) + || self.global_modules.iter().any(|m| m.contains_fn(hash_script)); + + #[cfg(not(feature = "no_module"))] + let result = result || // Then check imported modules - || global.map_or(false, |m| m.contains_qualified_fn(hash_script)) + _global.map_or(false, |m| m.contains_qualified_fn(hash_script)) // Then check sub-modules || self.global_sub_modules.values().any(|m| m.contains_qualified_fn(hash_script)); diff --git a/src/lib.rs b/src/lib.rs index df685645..3e514dd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,6 +123,7 @@ type UNSIGNED_INT = u64; type UNSIGNED_INT = u32; /// The system floating-point type. It is defined as [`f64`]. +/// /// Not available under `no_float`. /// /// If the `f32_float` feature is enabled, this will be [`f32`] instead. @@ -132,6 +133,7 @@ pub type FLOAT = f64; /// The system floating-point type. /// It is defined as [`f32`] since the `f32_float` feature is used. +/// /// Not available under `no_float`. /// /// If the `f32_float` feature is not used, this will be `f64` instead. @@ -156,6 +158,15 @@ pub use types::{ Dynamic, EvalAltResult, FnPtr, ImmutableString, LexError, ParseError, ParseErrorType, Scope, }; +/// _(debugging)_ Module containing types for debugging. +/// Exported under the `debugging` feature only. +#[cfg(feature = "debugging")] +pub mod debugger { + #[cfg(not(feature = "no_function"))] + pub use super::eval::CallStackFrame; + pub use super::eval::{BreakPoint, Debugger, DebuggerCommand}; +} + /// An identifier in Rhai. [`SmartString`](https://crates.io/crates/smartstring) is used because most /// identifiers are ASCII and short, fewer than 23 characters, so they can be stored inline. #[cfg(not(feature = "internals"))] @@ -188,16 +199,19 @@ pub use func::Func; pub use ast::ScriptFnMetadata; /// Variable-sized array of [`Dynamic`] values. +/// /// Not available under `no_index`. #[cfg(not(feature = "no_index"))] pub type Array = Vec; /// Variable-sized array of [`u8`] values (byte array). +/// /// Not available under `no_index`. #[cfg(not(feature = "no_index"))] pub type Blob = Vec; /// A dictionary of [`Dynamic`] values with string keys. +/// /// Not available under `no_object`. /// /// [`SmartString`](https://crates.io/crates/smartstring) is used as the key type because most @@ -240,8 +254,9 @@ pub use parser::ParseState; #[cfg(feature = "internals")] pub use ast::{ - ASTNode, BinaryExpr, CustomExpr, Expr, FnCallExpr, FnCallHashes, Ident, OpAssignment, - OptionFlags, ScriptFnDef, Stmt, StmtBlock, AST_OPTION_FLAGS::*, + ASTNode, BinaryExpr, ConditionalStmtBlock, CustomExpr, Expr, FnCallExpr, FnCallHashes, Ident, + OpAssignment, OptionFlags, ScriptFnDef, Stmt, StmtBlock, SwitchCases, TryCatchBlock, + AST_OPTION_FLAGS::*, }; #[cfg(feature = "internals")] @@ -255,6 +270,7 @@ pub use eval::{EvalState, GlobalRuntimeState}; pub use func::call::{FnResolutionCache, FnResolutionCacheEntry}; #[cfg(feature = "internals")] +#[cfg(not(feature = "no_module"))] pub use module::Namespace; /// Alias to [`smallvec::SmallVec<[T; 3]>`](https://crates.io/crates/smallvec), which is a diff --git a/src/module/mod.rs b/src/module/mod.rs index 7e79214e..e842fc1d 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -1,11 +1,10 @@ //! Module defining external-loaded modules for Rhai. -use crate::ast::{FnAccess, Ident}; +use crate::ast::FnAccess; use crate::func::{ shared_take_or_clone, CallableFunction, FnCallArgs, IteratorFn, RegisterNativeFunction, SendSync, }; -use crate::tokenizer::Token; use crate::types::dynamic::Variant; use crate::{ calc_fn_params_hash, calc_qualified_fn_hash, combine_hashes, Dynamic, Identifier, @@ -19,8 +18,7 @@ use std::{ collections::{BTreeMap, BTreeSet}, fmt, iter::{empty, once}, - num::NonZeroUsize, - ops::{Add, AddAssign, Deref, DerefMut}, + ops::{Add, AddAssign}, }; /// A type representing the namespace of a function. @@ -29,6 +27,8 @@ pub enum FnNamespace { /// Expose to global namespace. Global, /// Module namespace only. + /// + /// Ignored under `no_module`. Internal, } @@ -151,7 +151,7 @@ impl FuncInfo { ty => ty.into(), } } - /// Generate a signature of the function. + /// _(metadata)_ Generate a signature of the function. /// Exported under the `metadata` feature only. #[cfg(feature = "metadata")] #[must_use] @@ -244,9 +244,9 @@ pub struct Module { /// including those in sub-modules. all_functions: BTreeMap>, /// Iterator functions, keyed by the type producing the iterator. - type_iterators: BTreeMap, + type_iterators: BTreeMap>, /// Flattened collection of iterator functions, including those in sub-modules. - all_type_iterators: BTreeMap, + all_type_iterators: BTreeMap>, /// Is the [`Module`] indexed? indexed: bool, /// Does the [`Module`] contain indexed functions that have been exposed to the global namespace? @@ -462,7 +462,7 @@ impl Module { self.indexed } - /// Generate signatures for all the non-private functions in the [`Module`]. + /// _(metadata)_ Generate signatures for all the non-private functions in the [`Module`]. /// Exported under the `metadata` feature only. #[cfg(feature = "metadata")] #[inline] @@ -759,8 +759,7 @@ impl Module { self } - /// _(metadata)_ Update the metadata (parameter names/types, return type and doc-comments) of a - /// registered function. + /// _(metadata)_ Update the metadata (parameter names/types, return type and doc-comments) of a registered function. /// Exported under the `metadata` feature only. /// /// The [`u64`] hash is returned by the [`set_native_fn`][Module::set_native_fn] call. @@ -1373,6 +1372,7 @@ impl Module { /// Get a namespace-qualified function. /// /// The [`u64`] hash is calculated by [`build_index`][Module::build_index]. + #[cfg(not(feature = "no_module"))] #[inline] #[must_use] pub(crate) fn get_qualified_fn(&self, hash_qualified_fn: u64) -> Option<&CallableFunction> { @@ -1402,9 +1402,9 @@ impl Module { /// Sub-modules are flattened onto the root [`Module`], with higher level overriding lower level. #[inline] pub fn combine_flatten(&mut self, other: Self) -> &mut Self { - other.modules.into_iter().for_each(|(_, m)| { + for (_, m) in other.modules.into_iter() { self.combine_flatten(shared_take_or_clone(m)); - }); + } self.variables.extend(other.variables.into_iter()); self.functions.extend(other.functions.into_iter()); self.type_iterators.extend(other.type_iterators.into_iter()); @@ -1420,22 +1420,22 @@ impl Module { /// Only items not existing in this [`Module`] are added. #[inline] pub fn fill_with(&mut self, other: &Self) -> &mut Self { - other.modules.iter().for_each(|(k, v)| { + for (k, v) in &other.modules { if !self.modules.contains_key(k) { self.modules.insert(k.clone(), v.clone()); } - }); - other.variables.iter().for_each(|(k, v)| { + } + for (k, v) in &other.variables { if !self.variables.contains_key(k) { self.variables.insert(k.clone(), v.clone()); } - }); - other.functions.iter().for_each(|(&k, v)| { + } + for (&k, v) in &other.functions { self.functions.entry(k).or_insert_with(|| v.clone()); - }); - other.type_iterators.iter().for_each(|(&k, &v)| { - self.type_iterators.entry(k).or_insert(v); - }); + } + for (&k, v) in &other.type_iterators { + self.type_iterators.entry(k).or_insert_with(|| v.clone()); + } self.all_functions.clear(); self.all_variables.clear(); self.all_type_iterators.clear(); @@ -1456,12 +1456,11 @@ impl Module { other: &Self, _filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool + Copy, ) -> &mut Self { - #[cfg(not(feature = "no_function"))] - other.modules.iter().for_each(|(k, v)| { + for (k, v) in &other.modules { let mut m = Self::new(); m.merge_filtered(v, _filter); self.set_sub_module(k.clone(), m); - }); + } #[cfg(feature = "no_function")] self.modules .extend(other.modules.iter().map(|(k, v)| (k.clone(), v.clone()))); @@ -1484,7 +1483,8 @@ impl Module { .map(|(&k, v)| (k, v.clone())), ); - self.type_iterators.extend(other.type_iterators.iter()); + self.type_iterators + .extend(other.type_iterators.iter().map(|(&k, v)| (k, v.clone()))); self.all_functions.clear(); self.all_variables.clear(); self.all_type_iterators.clear(); @@ -1658,56 +1658,94 @@ impl Module { /// # } /// ``` #[cfg(not(feature = "no_module"))] + #[inline(always)] pub fn eval_ast_as_new( scope: crate::Scope, ast: &crate::AST, engine: &crate::Engine, + ) -> RhaiResultOf { + let global = &mut crate::eval::GlobalRuntimeState::new(engine); + + Self::eval_ast_as_new_raw(engine, scope, global, ast) + } + /// Create a new [`Module`] by evaluating an [`AST`][crate::AST]. + /// + /// The entire [`AST`][crate::AST] is encapsulated into each function, allowing functions + /// to cross-call each other. Functions in the global namespace, plus all functions + /// defined in the [`Module`], are _merged_ into a _unified_ namespace before each call. + /// Therefore, all functions will be found. + #[cfg(not(feature = "no_module"))] + pub(crate) fn eval_ast_as_new_raw( + engine: &crate::Engine, + scope: crate::Scope, + global: &mut crate::eval::GlobalRuntimeState, + ast: &crate::AST, ) -> RhaiResultOf { let mut scope = scope; - let mut global = crate::eval::GlobalRuntimeState::new(); - let orig_mods_len = global.num_imports(); + + // Save global state + let orig_imports_len = global.num_imports(); + let orig_source = global.source.clone(); + #[cfg(not(feature = "no_function"))] + let orig_constants = std::mem::take(&mut global.constants); // Run the script - engine.eval_ast_with_scope_raw(&mut scope, &mut global, &ast, 0)?; + let result = engine.eval_ast_with_scope_raw(&mut scope, global, &ast, 0); // Create new module - let mut module = - scope - .into_iter() - .fold(Module::new(), |mut module, (_, value, mut aliases)| { - // Variables with an alias left in the scope become module variables - match aliases.len() { - 0 => (), - 1 => { - let alias = aliases.pop().unwrap(); - module.set_var(alias, value); - } - _ => { - let last_alias = aliases.pop().unwrap(); - aliases.into_iter().for_each(|alias| { - module.set_var(alias, value.clone()); - }); - // Avoid cloning the last value - module.set_var(last_alias, value); - } - } - module - }); + let mut module = Module::new(); - // Extra modules left in the scope become sub-modules + // Extra modules left become sub-modules #[cfg(not(feature = "no_function"))] let mut func_global = None; - global.into_iter().skip(orig_mods_len).for_each(|kv| { - #[cfg(not(feature = "no_function"))] - if func_global.is_none() { - func_global = Some(StaticVec::new()); - } - #[cfg(not(feature = "no_function"))] - func_global.as_mut().expect("`Some`").push(kv.clone()); + if result.is_ok() { + global + .scan_imports_raw() + .skip(orig_imports_len) + .for_each(|(k, m)| { + #[cfg(not(feature = "no_function"))] + if func_global.is_none() { + func_global = Some(StaticVec::new()); + } + #[cfg(not(feature = "no_function"))] + func_global + .as_mut() + .expect("`Some`") + .push((k.clone(), m.clone())); - module.set_sub_module(kv.0, kv.1); - }); + module.set_sub_module(k.clone(), m.clone()); + }); + } + + // Restore global state + #[cfg(not(feature = "no_function"))] + { + global.constants = orig_constants; + } + global.truncate_imports(orig_imports_len); + global.source = orig_source; + + result?; + + // Variables with an alias left in the scope become module variables + for (_, value, mut aliases) in scope { + match aliases.len() { + 0 => (), + 1 => { + let alias = aliases.pop().unwrap(); + module.set_var(alias, value); + } + _ => { + let last_alias = aliases.pop().unwrap(); + for alias in aliases { + module.set_var(alias, value.clone()); + } + // Avoid cloning the last value + module.set_var(last_alias, value); + } + } + } #[cfg(not(feature = "no_function"))] let func_global = func_global.map(|v| v.into_boxed_slice()); @@ -1765,33 +1803,33 @@ impl Module { path: &mut Vec<&'a str>, variables: &mut BTreeMap, functions: &mut BTreeMap>, - type_iterators: &mut BTreeMap, + type_iterators: &mut BTreeMap>, ) -> bool { let mut contains_indexed_global_functions = false; - module.modules.iter().for_each(|(name, m)| { + for (name, m) in &module.modules { // Index all the sub-modules first. path.push(name); if index_module(m, path, variables, functions, type_iterators) { contains_indexed_global_functions = true; } path.pop(); - }); + } // Index all variables - module.variables.iter().for_each(|(var_name, value)| { + for (var_name, value) in &module.variables { let hash_var = crate::calc_qualified_var_hash(path.iter().copied(), var_name); variables.insert(hash_var, value.clone()); - }); + } // Index type iterators - module.type_iterators.iter().for_each(|(&type_id, func)| { - type_iterators.insert(type_id, *func); + for (&type_id, func) in &module.type_iterators { + type_iterators.insert(type_id, func.clone()); contains_indexed_global_functions = true; - }); + } // Index all Rust functions - module.functions.iter().for_each(|(&hash, f)| { + for (&hash, f) in &module.functions { match f.metadata.namespace { FnNamespace::Global => { // Flatten all functions with global namespace @@ -1802,7 +1840,7 @@ impl Module { } match f.metadata.access { FnAccess::Public => (), - FnAccess::Private => return, // Do not index private functions + FnAccess::Private => continue, // Do not index private functions } if !f.func.is_script() { @@ -1820,7 +1858,7 @@ impl Module { ); functions.insert(hash_qualified_script, f.func.clone()); } - }); + } contains_indexed_global_functions } @@ -1865,10 +1903,33 @@ impl Module { } /// Set a type iterator into the [`Module`]. + #[cfg(not(feature = "sync"))] #[inline] - pub fn set_iter(&mut self, type_id: TypeId, func: IteratorFn) -> &mut Self { + pub fn set_iter( + &mut self, + type_id: TypeId, + func: impl Fn(Dynamic) -> Box> + 'static, + ) -> &mut Self { + let func = Shared::new(func); if self.indexed { - self.all_type_iterators.insert(type_id, func); + self.all_type_iterators.insert(type_id, func.clone()); + self.contains_indexed_global_functions = true; + } + self.type_iterators.insert(type_id, func); + self + } + + /// Set a type iterator into the [`Module`]. + #[cfg(feature = "sync")] + #[inline] + pub fn set_iter( + &mut self, + type_id: TypeId, + func: impl Fn(Dynamic) -> Box> + SendSync + 'static, + ) -> &mut Self { + let func = Shared::new(func); + if self.indexed { + self.all_type_iterators.insert(type_id, func.clone()); self.contains_indexed_global_functions = true; } self.type_iterators.insert(type_id, func); @@ -1900,126 +1961,29 @@ impl Module { } /// Get the specified type iterator. + #[cfg(not(feature = "no_module"))] #[inline] #[must_use] - pub(crate) fn get_qualified_iter(&self, id: TypeId) -> Option { - self.all_type_iterators.get(&id).cloned() + pub(crate) fn get_qualified_iter(&self, id: TypeId) -> Option<&IteratorFn> { + self.all_type_iterators.get(&id).map(|f| f.as_ref()) } /// Get the specified type iterator. #[inline] #[must_use] - pub(crate) fn get_iter(&self, id: TypeId) -> Option { - self.type_iterators.get(&id).cloned() + pub(crate) fn get_iter(&self, id: TypeId) -> Option<&IteratorFn> { + self.type_iterators.get(&id).map(|f| f.as_ref()) } } -/// _(internals)_ A chain of [module][Module] names to namespace-qualify a variable or function -/// call. Exported under the `internals` feature only. -/// -/// A [`u64`] offset to the current [stack of imported modules][crate::GlobalRuntimeState] is -/// cached for quick search purposes. -/// -/// A [`StaticVec`] is used because the vast majority of namespace-qualified access contains only -/// 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, -} - -impl fmt::Debug for Namespace { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(index) = self.index { - write!(f, "{} -> ", index)?; - } - - f.write_str( - &self - .path - .iter() - .map(|Ident { name, .. }| name.as_str()) - .collect::>() - .join(Token::DoubleColon.literal_syntax()), - ) - } -} - -impl fmt::Display for Namespace { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str( - &self - .path - .iter() - .map(|Ident { name, .. }| name.as_str()) - .collect::>() - .join(Token::DoubleColon.literal_syntax()), - ) - } -} - -impl Deref for Namespace { - type Target = StaticVec; - - #[inline(always)] - fn deref(&self) -> &Self::Target { - &self.path - } -} - -impl DerefMut for Namespace { - #[inline(always)] - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.path - } -} - -impl From> for Namespace { - #[inline(always)] - fn from(mut path: Vec) -> Self { - path.shrink_to_fit(); - Self { - index: None, - path: path.into(), - } - } -} - -impl From> for Namespace { - #[inline(always)] - fn from(mut path: StaticVec) -> Self { - path.shrink_to_fit(); - Self { index: None, path } - } -} - -impl Namespace { - /// Create a new [`Namespace`]. - #[inline(always)] - #[must_use] - pub const fn new() -> Self { - Self { - index: None, - path: StaticVec::new_const(), - } - } - /// Get the [`Scope`][crate::Scope] index offset. - #[inline(always)] - #[must_use] - pub(crate) const fn index(&self) -> Option { - self.index - } - /// Set the [`Scope`][crate::Scope] index offset. - #[cfg(not(feature = "no_module"))] - #[inline(always)] - pub(crate) fn set_index(&mut self, index: Option) { - self.index = index - } -} - -#[cfg(not(feature = "no_module"))] -pub use resolvers::ModuleResolver; +mod namespace; /// Module containing all built-in [module resolvers][ModuleResolver]. #[cfg(not(feature = "no_module"))] pub mod resolvers; + +#[cfg(not(feature = "no_module"))] +pub use namespace::Namespace; + +#[cfg(not(feature = "no_module"))] +pub use resolvers::ModuleResolver; diff --git a/src/module/namespace.rs b/src/module/namespace.rs new file mode 100644 index 00000000..abfb9901 --- /dev/null +++ b/src/module/namespace.rs @@ -0,0 +1,117 @@ +//! Namespace reference type. +#![cfg(not(feature = "no_module"))] + +use crate::ast::Ident; +use crate::tokenizer::Token; +use crate::StaticVec; +#[cfg(feature = "no_std")] +use std::prelude::v1::*; +use std::{ + fmt, + num::NonZeroUsize, + ops::{Deref, DerefMut}, +}; + +/// _(internals)_ A chain of [module][Module] names to namespace-qualify a variable or function call. +/// Exported under the `internals` feature only. +/// +/// Not available under `no_module`. +/// +/// A [`u64`] offset to the current [stack of imported modules][crate::GlobalRuntimeState] is +/// cached for quick search purposes. +/// +/// A [`StaticVec`] is used because the vast majority of namespace-qualified access contains only +/// 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, +} + +impl fmt::Debug for Namespace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(index) = self.index { + write!(f, "{} -> ", index)?; + } + + f.write_str( + &self + .path + .iter() + .map(|Ident { name, .. }| name.as_str()) + .collect::>() + .join(Token::DoubleColon.literal_syntax()), + ) + } +} + +impl fmt::Display for Namespace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str( + &self + .path + .iter() + .map(|Ident { name, .. }| name.as_str()) + .collect::>() + .join(Token::DoubleColon.literal_syntax()), + ) + } +} + +impl Deref for Namespace { + type Target = StaticVec; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.path + } +} + +impl DerefMut for Namespace { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.path + } +} + +impl From> for Namespace { + #[inline(always)] + fn from(mut path: Vec) -> Self { + path.shrink_to_fit(); + Self { + index: None, + path: path.into(), + } + } +} + +impl From> for Namespace { + #[inline(always)] + fn from(mut path: StaticVec) -> Self { + path.shrink_to_fit(); + Self { index: None, path } + } +} + +impl Namespace { + /// Create a new [`Namespace`]. + #[inline(always)] + #[must_use] + pub const fn new() -> Self { + Self { + index: None, + path: StaticVec::new_const(), + } + } + /// Get the [`Scope`][crate::Scope] index offset. + #[inline(always)] + #[must_use] + pub(crate) const fn index(&self) -> Option { + self.index + } + /// Set the [`Scope`][crate::Scope] index offset. + #[inline(always)] + pub(crate) fn set_index(&mut self, index: Option) { + self.index = index + } +} diff --git a/src/module/resolvers/collection.rs b/src/module/resolvers/collection.rs index 6deb7c3d..8495fb8d 100644 --- a/src/module/resolvers/collection.rs +++ b/src/module/resolvers/collection.rs @@ -124,7 +124,7 @@ impl ModuleResolver for ModuleResolversCollection { path: &str, pos: Position, ) -> RhaiResultOf> { - for resolver in self.0.iter() { + for resolver in &self.0 { match resolver.resolve(engine, source_path, path, pos) { Ok(module) => return Ok(module), Err(err) => match *err { diff --git a/src/module/resolvers/file.rs b/src/module/resolvers/file.rs index 71828d81..1f06fb94 100644 --- a/src/module/resolvers/file.rs +++ b/src/module/resolvers/file.rs @@ -1,7 +1,8 @@ #![cfg(not(feature = "no_std"))] #![cfg(not(target_family = "wasm"))] -use crate::func::native::shared_write_lock; +use crate::eval::GlobalRuntimeState; +use crate::func::native::locked_write; use crate::{ Engine, Identifier, Module, ModuleResolver, Position, RhaiResultOf, Scope, Shared, ERR, }; @@ -207,12 +208,12 @@ impl FileModuleResolver { let file_path = self.get_file_path(path.as_ref(), source_path); - shared_write_lock(&self.cache).contains_key(&file_path) + locked_write(&self.cache).contains_key(&file_path) } /// Empty the internal cache. #[inline] pub fn clear_cache(&mut self) -> &mut Self { - shared_write_lock(&self.cache).clear(); + locked_write(&self.cache).clear(); self } /// Remove the specified path from internal cache. @@ -227,7 +228,7 @@ impl FileModuleResolver { ) -> Option> { let file_path = self.get_file_path(path.as_ref(), source_path.as_ref().map(<_>::as_ref)); - shared_write_lock(&self.cache) + locked_write(&self.cache) .remove_entry(&file_path) .map(|(_, v)| v) } @@ -252,24 +253,25 @@ impl FileModuleResolver { file_path.set_extension(self.extension.as_str()); // Force extension file_path } -} -impl ModuleResolver for FileModuleResolver { - fn resolve( + /// Resolve a module based on a path. + fn impl_resolve( &self, engine: &Engine, - source_path: Option<&str>, + global: Option<&mut GlobalRuntimeState>, + source: Option<&str>, path: &str, pos: Position, - ) -> RhaiResultOf> { + ) -> Result, Box> { // Load relative paths from source if there is no base path specified - let source_path = - source_path.and_then(|p| Path::new(p).parent().map(|p| p.to_string_lossy())); + let source_path = global + .as_ref() + .and_then(|g| g.source()) + .or(source) + .and_then(|p| Path::new(p).parent().map(|p| p.to_string_lossy())); - // Construct the script file path let file_path = self.get_file_path(path, source_path.as_ref().map(|p| p.as_ref())); - // See if it is cached if self.is_cache_enabled() { #[cfg(not(feature = "sync"))] let c = self.cache.borrow(); @@ -281,7 +283,6 @@ impl ModuleResolver for FileModuleResolver { } } - // Load the script file and compile it let scope = Scope::new(); let mut ast = engine @@ -295,18 +296,43 @@ impl ModuleResolver for FileModuleResolver { ast.set_source(path); - // Make a module from the AST - let m: Shared = Module::eval_ast_as_new(scope, &ast, engine) - .map_err(|err| Box::new(ERR::ErrorInModule(path.to_string(), err, pos)))? - .into(); + let m: Shared = if let Some(global) = global { + Module::eval_ast_as_new_raw(engine, scope, global, &ast) + } else { + Module::eval_ast_as_new(scope, &ast, engine) + } + .map_err(|err| Box::new(ERR::ErrorInModule(path.to_string(), err, pos)))? + .into(); - // Put it into the cache if self.is_cache_enabled() { - shared_write_lock(&self.cache).insert(file_path, m.clone()); + locked_write(&self.cache).insert(file_path, m.clone()); } Ok(m) } +} + +impl ModuleResolver for FileModuleResolver { + fn resolve_raw( + &self, + engine: &Engine, + global: &mut GlobalRuntimeState, + path: &str, + pos: Position, + ) -> RhaiResultOf> { + self.impl_resolve(engine, Some(global), None, path, pos) + } + + #[inline(always)] + fn resolve( + &self, + engine: &Engine, + source: Option<&str>, + path: &str, + pos: Position, + ) -> RhaiResultOf> { + self.impl_resolve(engine, None, source, path, pos) + } /// Resolve an `AST` based on a path string. /// diff --git a/src/module/resolvers/mod.rs b/src/module/resolvers/mod.rs index cf14c2b8..6f2899cb 100644 --- a/src/module/resolvers/mod.rs +++ b/src/module/resolvers/mod.rs @@ -1,3 +1,4 @@ +use crate::eval::GlobalRuntimeState; use crate::func::native::SendSync; use crate::{Engine, Module, Position, RhaiResultOf, Shared, AST}; #[cfg(feature = "no_std")] @@ -21,11 +22,26 @@ pub trait ModuleResolver: SendSync { fn resolve( &self, engine: &Engine, - source_path: Option<&str>, + source: Option<&str>, path: &str, pos: Position, ) -> RhaiResultOf>; + /// Resolve a module based on a path string, given a [`GlobalRuntimeState`]. + /// + /// # WARNING - Low Level API + /// + /// This function is very low level. + fn resolve_raw( + &self, + engine: &Engine, + global: &mut GlobalRuntimeState, + path: &str, + pos: Position, + ) -> RhaiResultOf> { + self.resolve(engine, global.source(), path, pos) + } + /// Resolve an `AST` based on a path string. /// /// Returns [`None`] (default) if such resolution is not supported @@ -40,7 +56,7 @@ pub trait ModuleResolver: SendSync { fn resolve_ast( &self, engine: &Engine, - source_path: Option<&str>, + source: Option<&str>, path: &str, pos: Position, ) -> Option> { diff --git a/src/optimizer.rs b/src/optimizer.rs index 7bfe8bbf..7e2cb6e7 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -138,7 +138,7 @@ impl<'a> OptimizerState<'a> { self.engine .call_native_fn( - &mut GlobalRuntimeState::new(), + &mut GlobalRuntimeState::new(&self.engine), &mut EvalState::new(), lib, fn_name, @@ -164,9 +164,21 @@ fn has_native_fn_override( // First check the global namespace and packages, but skip modules that are standard because // they should never conflict with system functions. - engine.global_modules.iter().filter(|m| !m.standard).any(|m| m.contains_fn(hash)) - // Then check sub-modules - || engine.global_sub_modules.values().any(|m| m.contains_qualified_fn(hash)) + let result = engine + .global_modules + .iter() + .filter(|m| !m.standard) + .any(|m| m.contains_fn(hash)); + + #[cfg(not(feature = "no_module"))] + // Then check sub-modules + let result = result + || engine + .global_sub_modules + .values() + .any(|m| m.contains_qualified_fn(hash)); + + result } /// Optimize a block of [statements][Stmt]. @@ -237,7 +249,7 @@ fn optimize_stmt_block( }); // Optimize each statement in the block - statements.iter_mut().for_each(|stmt| { + for stmt in statements.iter_mut() { match stmt { Stmt::Var(value_expr, x, options, _) => { if options.contains(AST_OPTION_CONSTANT) { @@ -260,7 +272,7 @@ fn optimize_stmt_block( // Optimize the statement _ => optimize_stmt(stmt, state, preserve_result), } - }); + } // Remove all pure statements except the last one let mut index = 0; @@ -308,7 +320,7 @@ fn optimize_stmt_block( match statements[..] { // { return; } -> {} [Stmt::Return(options, None, _)] - if reduce_return && !options.contains(AST_OPTION_BREAK_OUT) => + if reduce_return && !options.contains(AST_OPTION_BREAK) => { state.set_dirty(); statements.clear(); @@ -320,7 +332,7 @@ fn optimize_stmt_block( // { ...; return; } -> { ... } [.., ref last_stmt, Stmt::Return(options, None, _)] if reduce_return - && !options.contains(AST_OPTION_BREAK_OUT) + && !options.contains(AST_OPTION_BREAK) && !last_stmt.returns_value() => { state.set_dirty(); @@ -328,7 +340,7 @@ fn optimize_stmt_block( } // { ...; return val; } -> { ...; val } [.., Stmt::Return(options, ref mut expr, pos)] - if reduce_return && !options.contains(AST_OPTION_BREAK_OUT) => + if reduce_return && !options.contains(AST_OPTION_BREAK) => { state.set_dirty(); *statements.last_mut().unwrap() = expr @@ -365,7 +377,7 @@ fn optimize_stmt_block( } // { ...; return; } -> { ... } [.., Stmt::Return(options, None, _)] - if reduce_return && !options.contains(AST_OPTION_BREAK_OUT) => + if reduce_return && !options.contains(AST_OPTION_BREAK) => { state.set_dirty(); statements.pop().unwrap(); @@ -373,7 +385,7 @@ fn optimize_stmt_block( // { ...; return pure_val; } -> { ... } [.., Stmt::Return(options, Some(ref expr), _)] if reduce_return - && !options.contains(AST_OPTION_BREAK_OUT) + && !options.contains(AST_OPTION_BREAK) && expr.is_pure() => { state.set_dirty(); @@ -412,26 +424,26 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b match stmt { // var = var op expr => var op= expr Stmt::Assignment(x, _) - if x.1.is_none() - && x.0.is_variable_access(true) - && matches!(&x.2, Expr::FnCall(x2, _) + if x.0.is_none() + && x.1.lhs.is_variable_access(true) + && matches!(&x.1.rhs, Expr::FnCall(x2, _) if Token::lookup_from_syntax(&x2.name).map(|t| t.has_op_assignment()).unwrap_or(false) && x2.args.len() == 2 - && x2.args[0].get_variable_name(true) == x.0.get_variable_name(true) + && x2.args[0].get_variable_name(true) == x.1.lhs.get_variable_name(true) ) => { - match x.2 { + match x.1.rhs { Expr::FnCall(ref mut x2, _) => { state.set_dirty(); - x.1 = Some(OpAssignment::new_from_base(&x2.name)); + x.0 = Some(OpAssignment::new_from_base(&x2.name)); let value = mem::take(&mut x2.args[1]); if let Expr::Stack(slot, pos) = value { - x.2 = + x.1.rhs = Expr::from_dynamic(mem::take(x2.constants.get_mut(slot).unwrap()), pos); } else { - x.2 = value; + x.1.rhs = value; } } ref expr => unreachable!("Expr::FnCall expected but gets {:?}", expr), @@ -439,13 +451,12 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b } // expr op= expr - Stmt::Assignment(x, _) => match x.0 { - Expr::Variable(_, _, _) => optimize_expr(&mut x.2, state, false), - _ => { - optimize_expr(&mut x.0, state, false); - optimize_expr(&mut x.2, state, false); + Stmt::Assignment(x, _) => { + if !x.1.lhs.is_variable_access(false) { + optimize_expr(&mut x.1.lhs, state, false); } - }, + optimize_expr(&mut x.1.rhs, state, false); + } // if expr {} Stmt::If(condition, x, _) if x.0.is_empty() && x.1.is_empty() => { @@ -502,31 +513,39 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b value.hash(hasher); let hash = hasher.finish(); - let table = &mut x.0; + let cases = &mut x.cases; // First check hashes - if let Some(block) = table.get_mut(&hash) { - if let Some(mut condition) = mem::take(&mut block.0) { + if let Some(block) = cases.get_mut(&hash) { + if let Some(mut condition) = mem::take(&mut block.condition) { // switch const { case if condition => stmt, _ => def } => if condition { stmt } else { def } optimize_expr(&mut condition, state, false); let def_stmt = - optimize_stmt_block(mem::take(&mut *x.1), state, true, true, false); + optimize_stmt_block(mem::take(&mut x.def_case), state, true, true, false); *stmt = Stmt::If( condition, Box::new(( - mem::take(&mut block.1), - Stmt::Block(def_stmt.into_boxed_slice(), x.1.position().or_else(*pos)) - .into(), + mem::take(&mut block.statements), + Stmt::Block( + def_stmt.into_boxed_slice(), + x.def_case.position().or_else(*pos), + ) + .into(), )), match_expr.position(), ); } else { // Promote the matched case - let statements = - optimize_stmt_block(mem::take(&mut *block.1), state, true, true, false); - *stmt = Stmt::Block(statements.into_boxed_slice(), block.1.position()); + let statements = optimize_stmt_block( + mem::take(&mut block.statements), + state, + true, + true, + false, + ); + *stmt = Stmt::Block(statements.into_boxed_slice(), block.statements.position()); } state.set_dirty(); @@ -534,34 +553,39 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b } // Then check ranges - let ranges = &mut x.2; + let ranges = &mut x.ranges; if value.is::() && !ranges.is_empty() { let value = value.as_int().expect("`INT`"); // Only one range or all ranges without conditions - if ranges.len() == 1 || ranges.iter().all(|(_, _, _, c, _)| c.is_none()) { - for (_, _, _, condition, stmt_block) in + if ranges.len() == 1 || ranges.iter().all(|(_, _, _, c)| !c.has_condition()) { + for (_, _, _, block) in ranges .iter_mut() - .filter(|&&mut (start, end, inclusive, _, _)| { + .filter(|&&mut (start, end, inclusive, _)| { (!inclusive && (start..end).contains(&value)) || (inclusive && (start..=end).contains(&value)) }) { - if let Some(mut condition) = mem::take(condition) { + if let Some(mut condition) = mem::take(&mut block.condition) { // switch const { range if condition => stmt, _ => def } => if condition { stmt } else { def } optimize_expr(&mut condition, state, false); - let def_stmt = - optimize_stmt_block(mem::take(&mut *x.1), state, true, true, false); + let def_stmt = optimize_stmt_block( + mem::take(&mut x.def_case), + state, + true, + true, + false, + ); *stmt = Stmt::If( condition, Box::new(( - mem::take(stmt_block), + mem::take(&mut block.statements), Stmt::Block( def_stmt.into_boxed_slice(), - x.1.position().or_else(*pos), + x.def_case.position().or_else(*pos), ) .into(), )), @@ -569,11 +593,13 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b ); } else { // Promote the matched case - let statements = mem::take(&mut **stmt_block); + let statements = mem::take(&mut *block.statements); let statements = optimize_stmt_block(statements, state, true, true, false); - *stmt = - Stmt::Block(statements.into_boxed_slice(), stmt_block.position()); + *stmt = Stmt::Block( + statements.into_boxed_slice(), + block.statements.position(), + ); } state.set_dirty(); @@ -581,14 +607,14 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b } } else { // Multiple ranges - clear the table and just keep the right ranges - if !table.is_empty() { + if !cases.is_empty() { state.set_dirty(); - table.clear(); + cases.clear(); } let old_ranges_len = ranges.len(); - ranges.retain(|&mut (start, end, inclusive, _, _)| { + ranges.retain(|&mut (start, end, inclusive, _)| { (!inclusive && (start..end).contains(&value)) || (inclusive && (start..=end).contains(&value)) }); @@ -597,16 +623,16 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b state.set_dirty(); } - for (_, _, _, condition, stmt_block) in ranges.iter_mut() { - let statements = mem::take(&mut **stmt_block); - **stmt_block = + for (_, _, _, block) in ranges.iter_mut() { + let statements = mem::take(&mut *block.statements); + *block.statements = optimize_stmt_block(statements, state, preserve_result, true, false); - if let Some(mut c) = mem::take(condition) { - optimize_expr(&mut c, state, false); - match c { + if let Some(mut condition) = mem::take(&mut block.condition) { + optimize_expr(&mut condition, state, false); + match condition { Expr::Unit(_) | Expr::BoolConstant(true, _) => state.set_dirty(), - _ => *condition = Some(c), + _ => block.condition = Some(condition), } } } @@ -616,35 +642,41 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b // Promote the default case state.set_dirty(); - let def_stmt = optimize_stmt_block(mem::take(&mut *x.1), state, true, true, false); - *stmt = Stmt::Block(def_stmt.into_boxed_slice(), x.1.position().or_else(*pos)); + let def_stmt = + optimize_stmt_block(mem::take(&mut x.def_case), state, true, true, false); + *stmt = Stmt::Block( + def_stmt.into_boxed_slice(), + x.def_case.position().or_else(*pos), + ); } // switch Stmt::Switch(match_expr, x, _) => { optimize_expr(match_expr, state, false); - x.0.values_mut().for_each(|block| { - let statements = mem::take(&mut *block.1); - *block.1 = optimize_stmt_block(statements, state, preserve_result, true, false); + for block in x.cases.values_mut() { + let statements = mem::take(&mut *block.statements); + *block.statements = + optimize_stmt_block(statements, state, preserve_result, true, false); - if let Some(mut condition) = mem::take(&mut block.0) { + if let Some(mut condition) = mem::take(&mut block.condition) { optimize_expr(&mut condition, state, false); match condition { Expr::Unit(_) | Expr::BoolConstant(true, _) => state.set_dirty(), - _ => block.0 = Some(condition), + _ => block.condition = Some(condition), } } - }); + } // Remove false cases - while let Some((&key, _)) = x.0.iter().find(|(_, block)| match block.0 { + while let Some((&key, _)) = x.cases.iter().find(|(_, block)| match block.condition { Some(Expr::BoolConstant(false, _)) => true, _ => false, }) { state.set_dirty(); - x.0.remove(&key); + x.cases.remove(&key); } - *x.1 = optimize_stmt_block(mem::take(&mut *x.1), state, preserve_result, true, false); + let def_block = mem::take(&mut *x.def_case); + *x.def_case = optimize_stmt_block(def_block, state, preserve_result, true, false); } // while false { block } -> Noop @@ -663,7 +695,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_OUT) => { + Stmt::BreakLoop(options, pos) if options.contains(AST_OPTION_BREAK) => { // Only a single break statement - turn into running the guard expression once state.set_dirty(); if !condition.is_unit() { @@ -727,19 +759,21 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b } } // try { pure try_block } catch ( var ) { catch_block } -> try_block - Stmt::TryCatch(x, _) if x.0.iter().all(Stmt::is_pure) => { + Stmt::TryCatch(x, _) if x.try_block.iter().all(Stmt::is_pure) => { // If try block is pure, there will never be any exceptions state.set_dirty(); *stmt = Stmt::Block( - optimize_stmt_block(mem::take(&mut *x.0), state, false, true, false) + optimize_stmt_block(mem::take(&mut *x.try_block), state, false, true, false) .into_boxed_slice(), - x.0.position(), + x.try_block.position(), ); } // try { try_block } catch ( var ) { catch_block } Stmt::TryCatch(x, _) => { - *x.0 = optimize_stmt_block(mem::take(&mut *x.0), state, false, true, false); - *x.2 = optimize_stmt_block(mem::take(&mut *x.2), state, false, true, false); + *x.try_block = + optimize_stmt_block(mem::take(&mut *x.try_block), state, false, true, false); + *x.catch_block = + optimize_stmt_block(mem::take(&mut *x.catch_block), state, false, true, false); } // func(...) Stmt::Expr(expr @ Expr::FnCall(_, _)) => { @@ -800,8 +834,8 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, chaining: bool) { #[cfg(not(feature = "no_object"))] Expr::Dot(x,_, _) if !_chaining => match (&mut x.lhs, &mut x.rhs) { // map.string - (Expr::Map(m, pos), Expr::Property(p)) if m.0.iter().all(|(_, x)| x.is_pure()) => { - let prop = p.2.0.as_str(); + (Expr::Map(m, pos), Expr::Property(p, _)) if m.0.iter().all(|(_, x)| x.is_pure()) => { + let prop = p.2.as_str(); // Map literal where everything is pure - promote the indexed item. // All other items can be thrown away. state.set_dirty(); @@ -1091,7 +1125,9 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, chaining: bool) { }, // constant-name - Expr::Variable(_, pos, x) if x.1.is_none() && state.find_constant(&x.2).is_some() => { + #[cfg(not(feature = "no_module"))] + Expr::Variable(_, _, x) if x.1.is_some() => (), + Expr::Variable(_, pos, x) if state.find_constant(&x.2).is_some() => { // Replace constant with value *expr = Expr::from_dynamic(state.find_constant(&x.2).unwrap().clone(), *pos); state.set_dirty(); @@ -1137,13 +1173,13 @@ fn optimize_top_level( ); // Add constants and variables from the scope - scope.iter().for_each(|(name, constant, value)| { + for (name, constant, value) in scope.iter() { if !constant { state.push_var(name, AccessMode::ReadWrite, None); } else { state.push_var(name, AccessMode::ReadOnly, Some(value)); } - }); + } statements = optimize_stmt_block(statements, &mut state, true, false, true); statements @@ -1169,9 +1205,8 @@ pub fn optimize_into_ast( // We only need the script library's signatures for optimization purposes let mut lib2 = crate::Module::new(); - functions - .iter() - .map(|fn_def| crate::ast::ScriptFnDef { + for fn_def in &functions { + lib2.set_script_fn(crate::ast::ScriptFnDef { name: fn_def.name.clone(), access: fn_def.access, body: crate::ast::StmtBlock::NONE, @@ -1182,33 +1217,25 @@ pub fn optimize_into_ast( #[cfg(not(feature = "no_function"))] #[cfg(feature = "metadata")] comments: None, - }) - .for_each(|fn_def| { - lib2.set_script_fn(fn_def); }); + } let lib2 = &[&lib2]; - functions - .into_iter() - .map(|fn_def| { - let mut fn_def = crate::func::native::shared_take_or_clone(fn_def); + for fn_def in functions { + let mut fn_def = crate::func::native::shared_take_or_clone(fn_def); - // Optimize the function body - let body = mem::take(&mut *fn_def.body); + // Optimize the function body + let body = mem::take(&mut *fn_def.body); - *fn_def.body = - optimize_top_level(body, engine, scope, lib2, optimization_level); + *fn_def.body = optimize_top_level(body, engine, scope, lib2, optimization_level); - fn_def - }) - .for_each(|fn_def| { - module.set_script_fn(fn_def); - }); - } else { - functions.into_iter().for_each(|fn_def| { module.set_script_fn(fn_def); - }); + } + } else { + for fn_def in functions { + module.set_script_fn(fn_def); + } } module diff --git a/src/packages/array_basic.rs b/src/packages/array_basic.rs index 5e7e2eaf..6c851dbb 100644 --- a/src/packages/array_basic.rs +++ b/src/packages/array_basic.rs @@ -1,5 +1,4 @@ #![cfg(not(feature = "no_index"))] -#![allow(non_snake_case)] use crate::engine::OP_EQUALS; use crate::eval::{calc_index, calc_offset_len}; diff --git a/src/packages/bit_field.rs b/src/packages/bit_field.rs index 4bd5ec08..ee4e37f2 100644 --- a/src/packages/bit_field.rs +++ b/src/packages/bit_field.rs @@ -1,5 +1,3 @@ -#![allow(non_snake_case)] - use crate::eval::calc_index; use crate::plugin::*; use crate::{ diff --git a/src/packages/blob_basic.rs b/src/packages/blob_basic.rs index 0e1997c0..007fd1c5 100644 --- a/src/packages/blob_basic.rs +++ b/src/packages/blob_basic.rs @@ -1,5 +1,4 @@ #![cfg(not(feature = "no_index"))] -#![allow(non_snake_case)] use crate::eval::{calc_index, calc_offset_len}; use crate::plugin::*; diff --git a/src/packages/debugging.rs b/src/packages/debugging.rs new file mode 100644 index 00000000..397f8b8e --- /dev/null +++ b/src/packages/debugging.rs @@ -0,0 +1,83 @@ +#![cfg(feature = "debugging")] + +use crate::def_package; +use crate::plugin::*; +#[cfg(feature = "no_std")] +use std::prelude::v1::*; + +#[cfg(not(feature = "no_function"))] +use crate::{Dynamic, NativeCallContext}; + +#[cfg(not(feature = "no_function"))] +#[cfg(not(feature = "no_index"))] +use crate::Array; + +#[cfg(not(feature = "no_function"))] +#[cfg(not(feature = "no_object"))] +use crate::Map; + +def_package! { + /// Package of basic debugging utilities. + crate::DebuggingPackage => |lib| { + lib.standard = true; + + combine_with_exported_module!(lib, "debugging", debugging_functions); + } +} + +#[export_module] +mod debugging_functions { + #[cfg(not(feature = "no_function"))] + #[cfg(not(feature = "no_index"))] + pub fn stack_trace(ctx: NativeCallContext) -> Array { + if let Some(global) = ctx.global_runtime_state() { + global + .debugger + .call_stack() + .iter() + .rev() + .map( + |frame @ crate::debugger::CallStackFrame { + fn_name: _fn_name, + args: _args, + source: _source, + pos: _pos, + }| { + let display = frame.to_string(); + + #[cfg(not(feature = "no_object"))] + { + let mut map = Map::new(); + map.insert("display".into(), display.into()); + map.insert("fn_name".into(), _fn_name.into()); + if !_args.is_empty() { + map.insert( + "args".into(), + Dynamic::from_array(_args.clone().to_vec()), + ); + } + if !_source.is_empty() { + map.insert("source".into(), _source.into()); + } + if !_pos.is_none() { + map.insert( + "line".into(), + (_pos.line().unwrap() as crate::INT).into(), + ); + map.insert( + "position".into(), + (_pos.position().unwrap_or(0) as crate::INT).into(), + ); + } + Dynamic::from_map(map) + } + #[cfg(feature = "no_object")] + display.into() + }, + ) + .collect() + } else { + Array::new() + } + } +} diff --git a/src/packages/lang_core.rs b/src/packages/lang_core.rs index d0319a98..cbe6db9c 100644 --- a/src/packages/lang_core.rs +++ b/src/packages/lang_core.rs @@ -92,13 +92,14 @@ fn collect_fn_metadata( // Create a metadata record for a function. fn make_metadata( dict: &BTreeSet, - namespace: Option, + #[cfg(not(feature = "no_module"))] namespace: Option, func: &ScriptFnDef, ) -> Map { const DICT: &str = "key exists"; let mut map = Map::new(); + #[cfg(not(feature = "no_module"))] if let Some(ns) = namespace { map.insert(dict.get("namespace").expect(DICT).clone(), ns.into()); } @@ -133,6 +134,7 @@ fn collect_fn_metadata( // Intern strings let dict: BTreeSet = [ + #[cfg(not(feature = "no_module"))] "namespace", "name", "access", @@ -150,21 +152,52 @@ fn collect_fn_metadata( ctx.iter_namespaces() .flat_map(Module::iter_script_fn) .filter(|(s, a, n, p, f)| filter(*s, *a, n, *p, f)) - .for_each(|(_, _, _, _, f)| list.push(make_metadata(&dict, None, f).into())); + .for_each(|(_, _, _, _, f)| { + list.push( + make_metadata( + &dict, + #[cfg(not(feature = "no_module"))] + None, + f, + ) + .into(), + ) + }); ctx.engine() .global_modules .iter() .flat_map(|m| m.iter_script_fn()) .filter(|(ns, a, n, p, f)| filter(*ns, *a, n, *p, f)) - .for_each(|(_, _, _, _, f)| list.push(make_metadata(&dict, None, f).into())); + .for_each(|(_, _, _, _, f)| { + list.push( + make_metadata( + &dict, + #[cfg(not(feature = "no_module"))] + None, + f, + ) + .into(), + ) + }); + #[cfg(not(feature = "no_module"))] ctx.engine() .global_sub_modules .values() .flat_map(|m| m.iter_script_fn()) .filter(|(ns, a, n, p, f)| filter(*ns, *a, n, *p, f)) - .for_each(|(_, _, _, _, f)| list.push(make_metadata(&dict, None, f).into())); + .for_each(|(_, _, _, _, f)| { + list.push( + make_metadata( + &dict, + #[cfg(not(feature = "no_module"))] + None, + f, + ) + .into(), + ) + }); #[cfg(not(feature = "no_module"))] { @@ -189,7 +222,7 @@ fn collect_fn_metadata( .for_each(|(_, _, _, _, f)| { list.push(make_metadata(dict, Some(namespace.clone()), f).into()) }); - module.iter_sub_modules().for_each(|(ns, m)| { + for (ns, m) in module.iter_sub_modules() { let ns = format!( "{}{}{}", namespace, @@ -197,11 +230,12 @@ fn collect_fn_metadata( ns ); scan_module(list, dict, ns.into(), m.as_ref(), filter) - }); + } } - ctx.iter_imports_raw() - .for_each(|(ns, m)| scan_module(&mut list, &dict, ns.clone(), m.as_ref(), filter)); + for (ns, m) in ctx.iter_imports_raw() { + scan_module(&mut list, &dict, ns.clone(), m.as_ref(), filter) + } } list diff --git a/src/packages/logic.rs b/src/packages/logic.rs index 4c9130d9..26821a55 100644 --- a/src/packages/logic.rs +++ b/src/packages/logic.rs @@ -1,5 +1,3 @@ -#![allow(non_snake_case)] - use crate::def_package; use crate::plugin::*; #[cfg(feature = "no_std")] diff --git a/src/packages/mod.rs b/src/packages/mod.rs index 0b8bc6f3..f56fafe9 100644 --- a/src/packages/mod.rs +++ b/src/packages/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod arithmetic; pub(crate) mod array_basic; mod bit_field; pub(crate) mod blob_basic; +mod debugging; mod fn_basic; mod iter_basic; mod lang_core; @@ -24,6 +25,8 @@ pub use array_basic::BasicArrayPackage; pub use bit_field::BitFieldPackage; #[cfg(not(feature = "no_index"))] pub use blob_basic::BasicBlobPackage; +#[cfg(feature = "debugging")] +pub use debugging::DebuggingPackage; pub use fn_basic::BasicFnPackage; pub use iter_basic::BasicIteratorPackage; pub use lang_core::LanguageCorePackage; diff --git a/src/packages/pkg_core.rs b/src/packages/pkg_core.rs index c006b0a1..94d59c7f 100644 --- a/src/packages/pkg_core.rs +++ b/src/packages/pkg_core.rs @@ -13,6 +13,7 @@ def_package! { /// * [`BasicStringPackage`][super::BasicStringPackage] /// * [`BasicIteratorPackage`][super::BasicIteratorPackage] /// * [`BasicFnPackage`][super::BasicFnPackage] + /// * [`DebuggingPackage`][super::DebuggingPackage] crate::CorePackage => |lib| { lib.standard = true; @@ -21,5 +22,7 @@ def_package! { super::BasicStringPackage::init(lib); super::BasicIteratorPackage::init(lib); super::BasicFnPackage::init(lib); + #[cfg(feature = "debugging")] + super::DebuggingPackage::init(lib); } } diff --git a/src/packages/string_basic.rs b/src/packages/string_basic.rs index 1e4dd59e..266ccec3 100644 --- a/src/packages/string_basic.rs +++ b/src/packages/string_basic.rs @@ -1,5 +1,3 @@ -#![allow(non_snake_case)] - use crate::plugin::*; use crate::{def_package, FnPtr, INT}; use std::fmt::{Binary, LowerHex, Octal}; diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index ff0373c6..f51b27d8 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -1,5 +1,3 @@ -#![allow(non_snake_case)] - use crate::plugin::*; use crate::{def_package, Dynamic, ExclusiveRange, InclusiveRange, RhaiResultOf, StaticVec, INT}; #[cfg(feature = "no_std")] diff --git a/src/parser.rs b/src/parser.rs index 9bfc84f3..21d12147 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3,12 +3,11 @@ use crate::api::custom_syntax::{markers::*, CustomSyntax}; use crate::api::options::LanguageOptions; use crate::ast::{ - BinaryExpr, CustomExpr, Expr, FnCallExpr, FnCallHashes, Ident, OpAssignment, ScriptFnDef, Stmt, - StmtBlock, AST_OPTION_FLAGS::*, + BinaryExpr, ConditionalStmtBlock, CustomExpr, Expr, FnCallExpr, FnCallHashes, Ident, + OpAssignment, ScriptFnDef, Stmt, SwitchCases, TryCatchBlock, AST_OPTION_FLAGS::*, }; use crate::engine::{Precedence, KEYWORD_THIS, OP_CONTAINS}; use crate::func::hashing::get_hasher; -use crate::module::Namespace; use crate::tokenizer::{ is_keyword_function, is_valid_function_name, is_valid_identifier, Token, TokenStream, TokenizerControl, @@ -16,9 +15,8 @@ use crate::tokenizer::{ use crate::types::dynamic::AccessMode; use crate::types::StringsInterner; use crate::{ - calc_fn_hash, calc_qualified_fn_hash, calc_qualified_var_hash, Dynamic, Engine, ExclusiveRange, - Identifier, ImmutableString, InclusiveRange, LexError, ParseError, Position, Scope, Shared, - StaticVec, AST, INT, PERR, + calc_fn_hash, Dynamic, Engine, ExclusiveRange, Identifier, ImmutableString, InclusiveRange, + LexError, ParseError, Position, Scope, Shared, StaticVec, AST, INT, PERR, }; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -63,7 +61,7 @@ pub struct ParseState<'e> { pub allow_capture: bool, /// Encapsulates a local stack with imported [module][crate::Module] names. #[cfg(not(feature = "no_module"))] - pub modules: StaticVec, + pub imports: StaticVec, /// Maximum levels of expression nesting. #[cfg(not(feature = "unchecked"))] pub max_expr_depth: Option, @@ -94,7 +92,7 @@ impl<'e> ParseState<'e> { stack: StaticVec::new_const(), entry_stack_len: 0, #[cfg(not(feature = "no_module"))] - modules: StaticVec::new_const(), + imports: StaticVec::new_const(), } } @@ -158,7 +156,7 @@ impl<'e> ParseState<'e> { #[inline] #[must_use] pub fn find_module(&self, name: &str) -> Option { - self.modules + self.imports .iter() .rev() .enumerate() @@ -266,18 +264,23 @@ impl Expr { #[must_use] fn into_property(self, state: &mut ParseState) -> Self { match self { - Self::Variable(_, pos, x) if x.1.is_none() => { + #[cfg(not(feature = "no_module"))] + Self::Variable(_, _, ref x) if x.1.is_some() => self, + Self::Variable(_, pos, x) => { let ident = x.2; let getter = state.get_identifier(crate::engine::FN_GET, &ident); let hash_get = calc_fn_hash(&getter, 1); let setter = state.get_identifier(crate::engine::FN_SET, &ident); let hash_set = calc_fn_hash(&setter, 2); - Self::Property(Box::new(( - (getter, hash_get), - (setter, hash_set), - (state.get_interned_string("", &ident), pos), - ))) + Self::Property( + Box::new(( + (getter, hash_get), + (setter, hash_set), + state.get_interned_string("", &ident), + )), + pos, + ) } _ => self, } @@ -449,7 +452,7 @@ fn parse_fn_call( lib: &mut FnLib, id: Identifier, capture_parent_scope: bool, - namespace: Option, + #[cfg(not(feature = "no_module"))] namespace: Option, settings: ParseSettings, ) -> ParseResult { #[cfg(not(feature = "unchecked"))] @@ -457,6 +460,7 @@ fn parse_fn_call( let (token, token_pos) = input.peek().expect(NEVER_ENDS); + #[cfg(not(feature = "no_module"))] let mut namespace = namespace; let mut args = StaticVec::new_const(); @@ -475,28 +479,29 @@ fn parse_fn_call( Token::RightParen => { eat_token(input, Token::RightParen); + #[cfg(not(feature = "no_module"))] let hash = if let Some(modules) = namespace.as_mut() { - #[cfg(not(feature = "no_module"))] - { - let index = state.find_module(&modules[0].name); + 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; + #[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); + if !relax && settings.strict_var && index.is_none() { + return Err( + PERR::ModuleUndefined(modules[0].name.to_string()).into_err(modules[0].pos) + ); } - calc_qualified_fn_hash(modules.iter().map(|m| m.name.as_str()), &id, 0) + 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() @@ -509,6 +514,7 @@ fn parse_fn_call( return Ok(FnCallExpr { name: state.get_identifier("", id), capture_parent_scope, + #[cfg(not(feature = "no_module"))] namespace, hashes, args, @@ -534,28 +540,32 @@ fn parse_fn_call( (Token::RightParen, _) => { eat_token(input, Token::RightParen); + #[cfg(not(feature = "no_module"))] let hash = if let Some(modules) = namespace.as_mut() { - #[cfg(not(feature = "no_module"))] - { - let index = state.find_module(&modules[0].name); + 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; + #[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); + if !relax && settings.strict_var && index.is_none() { + return Err(PERR::ModuleUndefined(modules[0].name.to_string()) + .into_err(modules[0].pos)); } - calc_qualified_fn_hash(modules.iter().map(|m| m.name.as_str()), &id, args.len()) + modules.set_index(index); + + crate::calc_qualified_fn_hash( + modules.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() @@ -568,6 +578,7 @@ fn parse_fn_call( return Ok(FnCallExpr { name: state.get_identifier("", id), capture_parent_scope, + #[cfg(not(feature = "no_module"))] namespace, hashes, args, @@ -985,8 +996,8 @@ fn parse_switch( } } - let mut table = BTreeMap::, StmtBlock)>>::new(); - let mut ranges = StaticVec::<(INT, INT, bool, Option, StmtBlock)>::new(); + let mut cases = BTreeMap::>::new(); + let mut ranges = StaticVec::<(INT, INT, bool, ConditionalStmtBlock)>::new(); let mut def_pos = Position::NONE; let mut def_stmt = None; @@ -1050,7 +1061,7 @@ fn parse_switch( value.hash(hasher); let hash = hasher.finish(); - if table.contains_key(&hash) { + if cases.contains_key(&hash) { return Err(PERR::DuplicatedSwitchCase.into_err(expr.position())); } (Some(hash), None) @@ -1092,18 +1103,20 @@ fn parse_switch( value.hash(hasher); let hash = hasher.finish(); - table - .entry(hash) - .or_insert_with(|| (condition.clone(), stmt.into()).into()); + cases.entry(hash).or_insert_with(|| { + let block: ConditionalStmtBlock = (condition, stmt).into(); + block.into() + }); } // Other range - _ => ranges.push((range.0, range.1, range.2, condition, stmt.into())), + _ => ranges.push((range.0, range.1, range.2, (condition, stmt).into())), } } None } (Some(hash), None) => { - table.insert(hash, (condition, stmt.into()).into()); + let block: ConditionalStmtBlock = (condition, stmt).into(); + cases.insert(hash, block.into()); None } (None, None) => Some(stmt.into()), @@ -1133,11 +1146,16 @@ fn parse_switch( } } - let def_stmt_block = def_stmt.unwrap_or_else(|| Stmt::Noop(Position::NONE).into()); + let def_case = def_stmt.unwrap_or_else(|| Stmt::Noop(Position::NONE).into()); Ok(Stmt::Switch( item, - (table, def_stmt_block, ranges).into(), + SwitchCases { + cases, + def_case, + ranges, + } + .into(), settings.pos, )) } @@ -1328,6 +1346,11 @@ fn parse_primary( // 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), @@ -1344,7 +1367,7 @@ fn parse_primary( Expr::Variable( None, settings.pos, - (None, None, state.get_identifier("", s)).into(), + (None, none, state.get_identifier("", s)).into(), ) } // Namespace qualification @@ -1358,7 +1381,7 @@ fn parse_primary( Expr::Variable( None, settings.pos, - (None, None, state.get_identifier("", s)).into(), + (None, none, state.get_identifier("", s)).into(), ) } // Normal variable access @@ -1379,7 +1402,7 @@ fn parse_primary( Expr::Variable( short_index, settings.pos, - (index, None, state.get_identifier("", s)).into(), + (index, none, state.get_identifier("", s)).into(), ) } } @@ -1387,6 +1410,11 @@ fn parse_primary( // 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), @@ -1397,14 +1425,14 @@ fn parse_primary( Token::LeftParen | Token::Bang if is_keyword_function(&s) => Expr::Variable( None, settings.pos, - (None, None, state.get_identifier("", s)).into(), + (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(), + (None, none, state.get_identifier("", s)).into(), ), // Cannot access to `this` as a variable not in a function scope _ if &*s == KEYWORD_THIS => { @@ -1451,6 +1479,7 @@ fn parse_postfix( 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()) @@ -1476,17 +1505,37 @@ fn parse_postfix( _ => (), } - let (_, namespace, name) = *x; + let (_, _ns, name) = *x; settings.pos = pos; - let ns = namespace.map(|(ns, _)| ns); - parse_fn_call(input, state, lib, name, true, ns, settings.level_up())? + #[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 (_, namespace, name) = *x; - let ns = namespace.map(|(ns, _)| ns); + 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, ns, settings.level_up())? + parse_fn_call( + input, + state, + lib, + name, + false, + #[cfg(not(feature = "no_module"))] + _ns, + settings.level_up(), + )? } // module access #[cfg(not(feature = "no_module"))] @@ -1498,7 +1547,7 @@ fn parse_postfix( if let Some((ref mut namespace, _)) = namespace { namespace.push(var_name_def); } else { - let mut ns = Namespace::new(); + let mut ns = crate::module::Namespace::new(); ns.push(var_name_def); namespace = Some((ns, 42)); } @@ -1543,6 +1592,7 @@ fn parse_postfix( } // 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 { @@ -1552,8 +1602,9 @@ fn parse_postfix( _ => None, }; + #[cfg(not(feature = "no_module"))] if let Some((_, Some((namespace, hash)), name)) = namespaced_variable { - *hash = calc_qualified_var_hash(namespace.iter().map(|v| v.name.as_str()), name); + *hash = crate::calc_qualified_var_hash(namespace.iter().map(|v| v.name.as_str()), name); #[cfg(not(feature = "no_module"))] { @@ -1692,20 +1743,20 @@ fn make_assignment_stmt( 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 => { + Expr::Property(_, _) if !term => { check_lvalue(&x.rhs, matches!(expr, Expr::Dot(_, _, _))) } - Expr::Property(_) => None, + 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"), + 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"), + Expr::Property(_, _) if parent_is_dot => None, + Expr::Property(_, _) => unreachable!("unexpected Expr::Property in indexing"), e if parent_is_dot => Some(e.position()), _ => None, } @@ -1719,9 +1770,10 @@ fn make_assignment_stmt( Err(PERR::AssignmentToConstant("".into()).into_err(lhs.position())) } // var (non-indexed) = rhs - Expr::Variable(None, _, ref x) if x.0.is_none() => { - Ok(Stmt::Assignment((lhs, op_info, rhs).into(), op_pos)) - } + 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(); @@ -1730,7 +1782,10 @@ fn make_assignment_stmt( |n| n.get() as usize, ); match state.stack[state.stack.len() - index].1 { - AccessMode::ReadWrite => Ok(Stmt::Assignment((lhs, op_info, rhs).into(), op_pos)), + 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)) @@ -1749,9 +1804,10 @@ fn make_assignment_stmt( None => { match x.lhs { // var[???] = rhs, var.??? = rhs - Expr::Variable(_, _, _) => { - Ok(Stmt::Assignment((lhs, op_info, rhs).into(), op_pos)) - } + Expr::Variable(_, _, _) => Ok(Stmt::Assignment( + (op_info, (lhs, rhs).into()).into(), + op_pos, + )), // expr[???] = rhs, expr.??? = rhs ref expr => { Err(PERR::AssignmentToInvalidLHS("".to_string()) @@ -1826,11 +1882,14 @@ fn make_dot_expr( Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), false, op_pos)) } // lhs.module::id - syntax error + #[cfg(not(feature = "no_module"))] (_, Expr::Variable(_, _, x)) => { Err(PERR::PropertyExpected.into_err(x.1.expect("`Some`").0[0].pos)) } + #[cfg(feature = "no_module")] + (_, Expr::Variable(_, _, _)) => unreachable!("qualified property name"), // lhs.prop - (lhs, prop @ Expr::Property(_)) => Ok(Expr::Dot( + (lhs, prop @ Expr::Property(_, _)) => Ok(Expr::Dot( BinaryExpr { lhs, rhs: prop }.into(), false, op_pos, @@ -1844,7 +1903,7 @@ fn make_dot_expr( }; match x.lhs { - Expr::Variable(_, _, _) | Expr::Property(_) => { + Expr::Variable(_, _, _) | Expr::Property(_, _) => { let new_lhs = BinaryExpr { lhs: x.lhs.into_property(state), rhs: x.rhs, @@ -2142,7 +2201,19 @@ fn parse_custom_syntax( 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, None, name).into())); + 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)?; @@ -2520,7 +2591,7 @@ fn parse_let( state.stack.push((name, var_type)); let export = if is_export { - AST_OPTION_PUBLIC + AST_OPTION_EXPORTED } else { AST_OPTION_NONE }; @@ -2564,7 +2635,7 @@ fn parse_import( // import expr as name ... let (name, pos) = parse_var_name(input)?; let name = state.get_identifier("", name); - state.modules.push(name.clone()); + state.imports.push(name.clone()); Ok(Stmt::Import( expr, @@ -2603,48 +2674,27 @@ fn parse_export( _ => (), } - let mut exports = Vec::<(Ident, Ident)>::with_capacity(4); + let (id, id_pos) = parse_var_name(input)?; - loop { - 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 (rename, rename_pos) = if match_token(input, Token::As).0 { - let (name, pos) = parse_var_name(input)?; - if exports.iter().any(|(_, alias)| alias.name == name.as_ref()) { - return Err(PERR::DuplicatedVariable(name.to_string()).into_err(pos)); - } - (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, + }, + ); - exports.push(( - Ident { - name: state.get_identifier("", id), - pos: id_pos, - }, - Ident { - name: state.get_identifier("", rename.as_ref().map_or("", <_>::as_ref)), - pos: rename_pos, - }, - )); - - match input.peek().expect(NEVER_ENDS) { - (Token::Comma, _) => { - eat_token(input, Token::Comma); - } - (Token::Identifier(_), pos) => { - return Err(PERR::MissingToken( - Token::Comma.into(), - "to separate the list of exports".into(), - ) - .into_err(*pos)) - } - _ => break, - } - } - - Ok(Stmt::Export(exports.into_boxed_slice(), settings.pos)) + Ok(Stmt::Export(export.into(), settings.pos)) } /// Parse a statement block. @@ -2677,7 +2727,7 @@ fn parse_block( state.entry_stack_len = state.stack.len(); #[cfg(not(feature = "no_module"))] - let prev_mods_len = state.modules.len(); + let orig_imports_len = state.imports.len(); loop { // Terminated? @@ -2744,7 +2794,7 @@ fn parse_block( state.entry_stack_len = prev_entry_stack_len; #[cfg(not(feature = "no_module"))] - state.modules.truncate(prev_mods_len); + state.imports.truncate(orig_imports_len); Ok(Stmt::Block(statements.into_boxed_slice(), settings.pos)) } @@ -2925,7 +2975,7 @@ fn parse_stmt( } Token::Break if settings.default_options.allow_loop && settings.is_breakable => { let pos = eat_token(input, Token::Break); - Ok(Stmt::BreakLoop(AST_OPTION_BREAK_OUT, pos)) + Ok(Stmt::BreakLoop(AST_OPTION_BREAK, pos)) } Token::Continue | Token::Break if settings.default_options.allow_loop => { Err(PERR::LoopBreak.into_err(token_pos)) @@ -2937,7 +2987,7 @@ fn parse_stmt( .map(|(token, pos)| { let flags = match token { Token::Return => AST_OPTION_NONE, - Token::Throw => AST_OPTION_BREAK_OUT, + Token::Throw => AST_OPTION_BREAK, token => unreachable!( "Token::Return or Token::Throw expected but gets {:?}", token @@ -2996,10 +3046,10 @@ fn parse_try_catch( let mut settings = settings; settings.pos = eat_token(input, Token::Try); - // try { body } - let body = parse_block(input, state, lib, settings.level_up())?; + // try { try_block } + let try_block = parse_block(input, state, lib, settings.level_up())?; - // try { body } catch + // try { try_block } catch let (matched, catch_pos) = match_token(input, Token::Catch); if !matched { @@ -3009,8 +3059,8 @@ fn parse_try_catch( ); } - // try { body } catch ( - let err_var = if match_token(input, Token::LeftParen).0 { + // 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); @@ -3029,16 +3079,21 @@ fn parse_try_catch( None }; - // try { body } catch ( var ) { catch_block } - let catch_body = parse_block(input, state, lib, settings.level_up())?; + // try { try_block } catch ( var ) { catch_block } + let catch_block = parse_block(input, state, lib, settings.level_up())?; - if err_var.is_some() { + if catch_var.is_some() { // Remove the error variable from the stack state.stack.pop().unwrap(); } Ok(Stmt::TryCatch( - (body.into(), err_var, catch_body.into()).into(), + TryCatchBlock { + try_block: try_block.into(), + catch_var, + catch_block: catch_block.into(), + } + .into(), settings.pos, )) } @@ -3162,12 +3217,21 @@ fn make_curry_from_externals( args.push(fn_expr); - args.extend( - externals - .iter() - .cloned() - .map(|x| Expr::Variable(None, Position::NONE, (None, None, x).into())), - ); + args.extend(externals.iter().cloned().map(|x| { + Expr::Variable( + None, + Position::NONE, + ( + None, + #[cfg(not(feature = "no_module"))] + None, + #[cfg(feature = "no_module")] + (), + x, + ) + .into(), + ) + })); let expr = FnCallExpr { name: state.get_identifier("", crate::engine::KEYWORD_FN_PTR_CURRY), @@ -3185,7 +3249,7 @@ fn make_curry_from_externals( let mut statements = StaticVec::with_capacity(externals.len() + 1); statements.extend(externals.into_iter().map(Stmt::Share)); statements.push(Stmt::Expr(expr)); - Expr::Stmt(StmtBlock::new(statements, pos).into()) + Expr::Stmt(crate::ast::StmtBlock::new(statements, pos).into()) } /// Parse an anonymous function definition. @@ -3451,9 +3515,9 @@ impl Engine { { let mut m = crate::Module::new(); - _lib.into_iter().for_each(|fn_def| { + for fn_def in _lib { m.set_script_fn(fn_def); - }); + } return Ok(AST::new(statements, m)); } diff --git a/src/serde/metadata.rs b/src/serde/metadata.rs index ded1e362..42b46a91 100644 --- a/src/serde/metadata.rs +++ b/src/serde/metadata.rs @@ -16,6 +16,7 @@ enum FnType { Native, } +#[cfg(not(feature = "no_module"))] #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] enum FnNamespace { @@ -23,6 +24,7 @@ enum FnNamespace { Internal, } +#[cfg(not(feature = "no_module"))] impl From for FnNamespace { fn from(value: crate::FnNamespace) -> Self { match value { @@ -62,6 +64,7 @@ struct FnParam<'a> { struct FnMetadata<'a> { pub base_hash: u64, pub full_hash: u64, + #[cfg(not(feature = "no_module"))] pub namespace: FnNamespace, pub access: FnAccess, pub name: String, @@ -110,6 +113,7 @@ impl<'a> From<&'a FuncInfo> for FnMetadata<'a> { Self { base_hash, full_hash, + #[cfg(not(feature = "no_module"))] namespace: info.metadata.namespace.into(), access: info.metadata.access.into(), name: info.metadata.name.to_string(), @@ -206,26 +210,35 @@ impl Engine { let _ast = ast; let mut global = ModuleMetadata::new(); - self.global_sub_modules.iter().for_each(|(name, m)| { + #[cfg(not(feature = "no_module"))] + for (name, m) in &self.global_sub_modules { global.modules.insert(name, m.as_ref().into()); - }); + } self.global_modules .iter() .filter(|m| include_packages || !m.standard) .flat_map(|m| m.iter_fn()) .for_each(|f| { + #[allow(unused_mut)] let mut meta: FnMetadata = f.into(); - meta.namespace = FnNamespace::Global; + #[cfg(not(feature = "no_module"))] + { + meta.namespace = FnNamespace::Global; + } global.functions.push(meta); }); #[cfg(not(feature = "no_function"))] - _ast.shared_lib().iter_fn().for_each(|f| { + for f in _ast.shared_lib().iter_fn() { + #[allow(unused_mut)] let mut meta: FnMetadata = f.into(); - meta.namespace = FnNamespace::Global; + #[cfg(not(feature = "no_module"))] + { + meta.namespace = FnNamespace::Global; + } global.functions.push(meta); - }); + } global.functions.sort(); diff --git a/src/tokenizer.rs b/src/tokenizer.rs index ba6fb045..d4202b18 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -260,7 +260,11 @@ impl fmt::Display for Position { impl fmt::Debug for Position { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { #[cfg(not(feature = "no_position"))] - write!(f, "{}:{}", self.line, self.pos)?; + if self.is_beginning_of_line() { + write!(f, "{}", self.line)?; + } else { + write!(f, "{}:{}", self.line, self.pos)?; + } #[cfg(feature = "no_position")] f.write_str("none")?; diff --git a/src/types/dynamic.rs b/src/types/dynamic.rs index d61cd612..0b8fdf80 100644 --- a/src/types/dynamic.rs +++ b/src/types/dynamic.rs @@ -1119,18 +1119,18 @@ impl Dynamic { #[cfg(not(feature = "no_index"))] Union::Array(ref mut a, _, ref mut access) => { *access = typ; - a.iter_mut().for_each(|v| { + for v in a.iter_mut() { v.set_access_mode(typ); - }); + } } #[cfg(not(feature = "no_index"))] Union::Blob(_, _, ref mut access) => *access = typ, #[cfg(not(feature = "no_object"))] Union::Map(ref mut m, _, ref mut access) => { *access = typ; - m.values_mut().for_each(|v| { + for v in m.values_mut() { v.set_access_mode(typ); - }); + } } #[cfg(not(feature = "no_std"))] Union::TimeStamp(_, _, ref mut access) => *access = typ, @@ -1708,14 +1708,14 @@ impl Dynamic { match self.0 { #[cfg(not(feature = "no_closure"))] Union::Shared(ref cell, _, _) => { - let value = crate::func::native::shared_write_lock(cell); + let guard = crate::func::native::locked_write(cell); - if (*value).type_id() != TypeId::of::() + if (*guard).type_id() != TypeId::of::() && TypeId::of::() != TypeId::of::() { return None; } else { - return Some(DynamicWriteLock(DynamicWriteLockInner::Guard(value))); + return Some(DynamicWriteLock(DynamicWriteLockInner::Guard(guard))); } } _ => (), diff --git a/src/types/scope.rs b/src/types/scope.rs index 56f21748..2f275b00 100644 --- a/src/types/scope.rs +++ b/src/types/scope.rs @@ -534,8 +534,12 @@ impl Scope<'_> { Self::new(), |mut entries, (index, (name, alias))| { if !entries.names.iter().any(|(key, _)| key == name) { + let orig_value = &self.values[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.values.push(self.values[len - 1 - index].clone()); + entries.values.push(value); } entries }, @@ -607,9 +611,9 @@ impl Scope<'_> { impl> Extend<(K, Dynamic)> for Scope<'_> { #[inline] fn extend>(&mut self, iter: T) { - iter.into_iter().for_each(|(name, value)| { + for (name, value) in iter { self.push_dynamic_value(name, AccessMode::ReadWrite, value); - }); + } } } @@ -625,7 +629,7 @@ impl> FromIterator<(K, Dynamic)> for Scope<'_> { impl> Extend<(K, bool, Dynamic)> for Scope<'_> { #[inline] fn extend>(&mut self, iter: T) { - iter.into_iter().for_each(|(name, is_constant, value)| { + for (name, is_constant, value) in iter { self.push_dynamic_value( name, if is_constant { @@ -635,7 +639,7 @@ impl> Extend<(K, bool, Dynamic)> for Scope<'_> { }, value, ); - }); + } } } diff --git a/tests/debugging.rs b/tests/debugging.rs new file mode 100644 index 00000000..a329a533 --- /dev/null +++ b/tests/debugging.rs @@ -0,0 +1,78 @@ +#![cfg(feature = "debugging")] +use rhai::{Dynamic, Engine, EvalAltResult, INT}; + +#[cfg(not(feature = "no_index"))] +use rhai::Array; + +#[cfg(not(feature = "no_object"))] +use rhai::Map; + +#[test] +fn test_debugging() -> Result<(), Box> { + let engine = Engine::new(); + + #[cfg(not(feature = "no_function"))] + #[cfg(not(feature = "no_index"))] + { + let r = engine.eval::( + " + fn foo(x) { + if x >= 5 { + stack_trace() + } else { + foo(x+1) + } + } + + foo(0) + ", + )?; + + assert_eq!(r.len(), 6); + + assert_eq!(engine.eval::("len(stack_trace())")?, 0); + } + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_object"))] +fn test_debugger_state() -> Result<(), Box> { + let mut engine = Engine::new(); + + engine.on_debugger( + || { + // Say, use an object map for the debugger state + let mut state = Map::new(); + // Initialize properties + state.insert("hello".into(), (42 as INT).into()); + state.insert("foo".into(), false.into()); + Dynamic::from_map(state) + }, + |context, _, _, _| { + // Get global runtime state + let global = context.global_runtime_state_mut(); + + // Get debugger + let debugger = &mut global.debugger; + + // Print debugger state - which is an object map + println!("Current state = {}", debugger.state()); + + // Modify state + let mut state = debugger.state_mut().write_lock::().unwrap(); + let hello = state.get("hello").unwrap().as_int().unwrap(); + state.insert("hello".into(), (hello + 1).into()); + state.insert("foo".into(), true.into()); + state.insert("something_new".into(), "hello, world!".into()); + + // Continue with debugging + Ok(rhai::debugger::DebuggerCommand::StepInto) + }, + ); + + engine.run("let x = 42;")?; + + Ok(()) +} diff --git a/tests/modules.rs b/tests/modules.rs index 6f8e56b7..9b3e62d4 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -317,11 +317,10 @@ fn test_module_from_ast() -> Result<(), Box> { foo = calc(foo); hello = `hello, ${foo} worlds!`; - export - x as abc, - x as xxx, - foo, - hello; + export x as abc; + export x as xxx; + export foo; + export hello; "#, )?;