diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index df310705..98309937 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v2 - run: rustup toolchain update nightly && rustup default nightly - name: Run benchmark - run: cargo +nightly bench | tee output.txt + run: cargo +nightly bench --features decimal,metadata,serde,debugging | tee output.txt - name: Store benchmark result uses: rhysd/github-action-benchmark@v1 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c9de46..571ec17e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,25 +12,57 @@ 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. +* Globally-defined constants are now encapsulated correctly inside a loaded module and no longer spill across call boundaries. +* Type names display is fixed. Script-breaking changes ----------------------- -* For consistency, the `export` statement no longer exports multiple variables. +* For consistency with the `import` statement, 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. +* A new package, `DebuggingPackage`, is added which contains the `back_trace` function to get the current call stack anywhere in a script. +* `Engine::set_allow_shadowing` is added to allow/disallow variables _shadowing_, with new errors `EvalAltResult::ErrorVariableExists` and `ParseErrorType::VariableExists`. +* `Engine::on_def_var` allows registering a closure which can decide whether a variable definition is allow to continue, or should fail with an error. 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. +* Debug display of `AST` is improved. +* `NativeCallContext::call_level()` is added to give the current nesting level of function calls. +* A new feature, `bin-features`, pulls in all the required features for `bin` tools. +* `AST` position display is improved: + * `Expr::start_position` is added to give the beginning of the expression (not the operator's position). + * `StmtBlock` and `Stmt::Block` now keep the position of the closing `}` as well. + +REPL tool changes +----------------- + +The REPL bin tool, `rhai-rpl`, has been enhanced. + +### Build changes + +* The `rustyline` feature is now required in order to build `rhai-repl`. +* Therefore, `rhai-repl` is no longer automatically built when using a simple `cargo build` with default features. + +### Line editor + +* `rhai-repl` now uses [`rustyline`](https://crates.io/crates/rustyline) as a line editor with history. +* Shift-Enter can now be used to enter multiple lines without having to attach the `\` continuation character the end of each line. + +### New commands + +* `strict` to turn on/off _Strict Variables Mode_. +* `optimize` to turn on/off script optimization. +* `history` to print lines history. +* `!!`, `!`_num_ and `!`_text_ to recall a history line. +* `keys` to print all key bindings. Version 1.4.1 @@ -551,7 +583,7 @@ Breaking changes New features ------------ -* Line continuation (via `\`) and multi-line literal strings (wrapped with \`) support are added. +* Line continuation (via `\`) and multi-line literal strings (wrapped with `` ` ``) support are added. * Rhai scripts can now start with a shebang `#!` which is ignored. Enhancements diff --git a/Cargo.toml b/Cargo.toml index 2d687d30..c702f73f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,12 +55,16 @@ unicode-xid-ident = ["unicode-xid"] # allow Unicode Standard Annex #31 for ident metadata = ["serde", "serde_json", "rhai_codegen/metadata", "smartstring/serde"] # enable exporting functions metadata debugging = ["internals"] # enable debugging +# compiling for no-std no_std = ["no-std-compat", "num-traits/libm", "core-error", "libm", "ahash/compile-time-rng"] # compiling for WASM wasm-bindgen = ["instant/wasm-bindgen"] stdweb = ["instant/stdweb"] +# compiling bin tools +bin-features = ["decimal", "metadata", "serde", "debugging", "rustyline"] + [[bin]] name = "rhai-repl" required-features = ["rustyline"] diff --git a/src/api/call_fn.rs b/src/api/call_fn.rs index e501c68d..ca41552d 100644 --- a/src/api/call_fn.rs +++ b/src/api/call_fn.rs @@ -190,8 +190,8 @@ impl Engine { &mut this_ptr, fn_def, &mut args, - Position::NONE, rewind_scope, + Position::NONE, 0, ); diff --git a/src/api/events.rs b/src/api/events.rs index 6fb5227c..0a7b5eaf 100644 --- a/src/api/events.rs +++ b/src/api/events.rs @@ -15,12 +15,12 @@ impl Engine { /// > `Fn(name: &str, index: usize, context: &EvalContext) -> Result, Box>` /// /// where: + /// * `name`: name of the variable. /// * `index`: an offset from the bottom of the current [`Scope`][crate::Scope] that the /// variable is supposed to reside. Offsets start from 1, with 1 meaning the last variable in /// the current [`Scope`][crate::Scope]. Essentially the correct variable is at position /// `scope.len() - index`. If `index` is zero, then there is no pre-calculated offset position /// and a search through the current [`Scope`][crate::Scope] must be performed. - /// /// * `context`: the current [evaluation context][`EvalContext`]. /// /// ## Return value @@ -63,6 +63,67 @@ impl Engine { self.resolve_var = Some(Box::new(callback)); self } + /// Provide a callback that will be invoked before the definition of each variable . + /// + /// # Callback Function Signature + /// + /// The callback function signature takes the following form: + /// + /// > `Fn(name: &str, is_const: bool, block_level: usize, will_shadow: bool, context: &EvalContext) -> Result>` + /// + /// where: + /// * `name`: name of the variable to be defined. + /// * `is_const`: `true` if the statement is `const`, otherwise it is `let`. + /// * `block_level`: the current nesting level of statement blocks, with zero being the global level + /// * `will_shadow`: will the variable _shadow_ an existing variable? + /// * `context`: the current [evaluation context][`EvalContext`]. + /// + /// ## Return value + /// + /// * `Ok(true)`: continue with normal variable definition. + /// * `Ok(false)`: deny the variable definition with an [runtime error][EvalAltResult::ErrorRuntime]. + /// + /// ## Raising errors + /// + /// Return `Err(...)` if there is an error. + /// + /// # Example + /// + /// ```should_panic + /// # fn main() -> Result<(), Box> { + /// use rhai::Engine; + /// + /// let mut engine = Engine::new(); + /// + /// // Register a variable definition filter. + /// engine.on_def_var(|name, is_const, _, _, _| { + /// // Disallow defining MYSTIC_NUMBER as a constant + /// if name == "MYSTIC_NUMBER" && is_const { + /// Ok(false) + /// } else { + /// Ok(true) + /// } + /// }); + /// + /// // The following runs fine: + /// engine.eval::("let MYSTIC_NUMBER = 42;")?; + /// + /// // The following will cause an error: + /// engine.eval::("const MYSTIC_NUMBER = 42;")?; + /// + /// # Ok(()) + /// # } + /// ``` + #[inline(always)] + pub fn on_def_var( + &mut self, + callback: impl Fn(&str, bool, usize, bool, &EvalContext) -> RhaiResultOf + + SendSync + + 'static, + ) -> &mut Self { + self.def_var_filter = Some(Box::new(callback)); + self + } /// _(internals)_ Register a callback that will be invoked during parsing to remap certain tokens. /// Exported under the `internals` feature only. /// @@ -264,11 +325,12 @@ impl Engine { /// Exported under the `debugging` feature only. #[cfg(feature = "debugging")] #[inline(always)] - pub fn on_debugger( + pub fn register_debugger( &mut self, init: impl Fn() -> Dynamic + SendSync + 'static, callback: impl Fn( &mut EvalContext, + crate::eval::DebuggerEvent, crate::ast::ASTNode, Option<&str>, Position, diff --git a/src/api/mod.rs b/src/api/mod.rs index 39045da7..b7d9b8f2 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,7 @@ //! Module defining the public API of the Rhai engine. +pub mod type_names; + pub mod eval; pub mod run; diff --git a/src/api/options.rs b/src/api/options.rs index a6e1f59b..4fe6a4d7 100644 --- a/src/api/options.rs +++ b/src/api/options.rs @@ -17,9 +17,11 @@ pub struct LanguageOptions { #[cfg(not(feature = "no_function"))] pub allow_anonymous_fn: bool, /// Is looping allowed? - pub allow_loop: bool, + pub allow_looping: bool, /// Strict variables mode? pub strict_var: bool, + /// Is variables shadowing allowed? + pub allow_shadowing: bool, } impl LanguageOptions { @@ -32,8 +34,9 @@ impl LanguageOptions { allow_stmt_expr: true, #[cfg(not(feature = "no_function"))] allow_anonymous_fn: true, - allow_loop: true, + allow_looping: true, strict_var: false, + allow_shadowing: true, } } } @@ -94,12 +97,12 @@ impl Engine { /// Is looping allowed? #[inline(always)] pub fn allow_looping(&self) -> bool { - self.options.allow_loop + self.options.allow_looping } /// Set whether looping is allowed. #[inline(always)] pub fn set_allow_looping(&mut self, enable: bool) { - self.options.allow_loop = enable; + self.options.allow_looping = enable; } /// Is strict variables mode enabled? #[inline(always)] @@ -111,4 +114,14 @@ impl Engine { pub fn set_strict_variables(&mut self, enable: bool) { self.options.strict_var = enable; } + /// Is variables shadowing allowed? + #[inline(always)] + pub fn allow_shadowing(&self) -> bool { + self.options.allow_shadowing + } + /// Set whether variables shadowing is allowed. + #[inline(always)] + pub fn set_allow_shadowing(&mut self, enable: bool) { + self.options.allow_shadowing = enable; + } } diff --git a/src/api/type_names.rs b/src/api/type_names.rs new file mode 100644 index 00000000..faf1fa44 --- /dev/null +++ b/src/api/type_names.rs @@ -0,0 +1,131 @@ +use crate::{ + Engine, ExclusiveRange, FnPtr, ImmutableString, InclusiveRange, Position, RhaiError, ERR, +}; +use std::any::type_name; +#[cfg(feature = "no_std")] +use std::prelude::v1::*; + +/// Map the name of a standard type into a friendly form. +#[inline] +#[must_use] +fn map_std_type_name(name: &str, shorthands: bool) -> &str { + let name = name.trim(); + + if name == type_name::() { + return if shorthands { "string" } else { "String" }; + } + if name == type_name::() || name == "ImmutableString" { + return if shorthands { + "string" + } else { + "ImmutableString" + }; + } + if name == type_name::<&str>() { + return if shorthands { "string" } else { "&str" }; + } + #[cfg(feature = "decimal")] + if name == type_name::() { + return if shorthands { "decimal" } else { "Decimal" }; + } + if name == type_name::() || name == "FnPtr" { + return if shorthands { "Fn" } else { "FnPtr" }; + } + #[cfg(not(feature = "no_index"))] + if name == type_name::() || name == "Array" { + return if shorthands { "array" } else { "Array" }; + } + #[cfg(not(feature = "no_index"))] + if name == type_name::() || name == "Blob" { + return if shorthands { "blob" } else { "Blob" }; + } + #[cfg(not(feature = "no_object"))] + if name == type_name::() || name == "Map" { + return if shorthands { "map" } else { "Map" }; + } + #[cfg(not(feature = "no_std"))] + if name == type_name::() || name == "Instant" { + return if shorthands { "timestamp" } else { "Instant" }; + } + if name == type_name::() || name == "ExclusiveRange" { + return if shorthands { + "range" + } else if cfg!(feature = "only_i32") { + "Range" + } else { + "Range" + }; + } + if name == type_name::() || name == "InclusiveRange" { + return if shorthands { + "range=" + } else if cfg!(feature = "only_i32") { + "RangeInclusive" + } else { + "RangeInclusive" + }; + } + + if name.starts_with("rhai::") { + map_std_type_name(&name[6..], shorthands) + } else { + name + } +} + +impl Engine { + /// Pretty-print a type name. + /// + /// If a type is registered via [`register_type_with_name`][Engine::register_type_with_name], + /// the type name provided for the registration will be used. + /// + /// # Panics + /// + /// Panics if the type name is `&mut`. + #[inline] + #[must_use] + pub fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str { + self.type_names + .get(name) + .map(|s| s.as_str()) + .unwrap_or_else(|| map_std_type_name(name, true)) + } + + /// Format a type name. + /// + /// If a type is registered via [`register_type_with_name`][Engine::register_type_with_name], + /// the type name provided for the registration will be used. + #[cfg(feature = "metadata")] + #[inline] + #[must_use] + pub(crate) fn format_type_name<'a>(&'a self, name: &'a str) -> std::borrow::Cow<'a, str> { + if name.starts_with("&mut ") { + let x = &name[5..]; + let r = self.format_type_name(x); + return if x != r { + format!("&mut {}", r).into() + } else { + name.into() + }; + } + + self.type_names + .get(name) + .map(|s| s.as_str()) + .unwrap_or_else(|| match name { + "INT" => return type_name::(), + #[cfg(not(feature = "no_float"))] + "FLOAT" => return type_name::(), + _ => map_std_type_name(name, false), + }) + .into() + } + + /// Make a `Box<`[`EvalAltResult`][ERR::ErrorMismatchDataType]`>`. + #[inline(never)] + #[must_use] + pub(crate) fn make_type_mismatch_err(&self, typ: &str, pos: Position) -> RhaiError { + ERR::ErrorMismatchDataType(self.map_type_name(type_name::()).into(), typ.into(), pos) + .into() + } +} diff --git a/src/ast/ast.rs b/src/ast/ast.rs index 948df827..7077bad6 100644 --- a/src/ast/ast.rs +++ b/src/ast/ast.rs @@ -1,10 +1,11 @@ //! Module defining the AST (abstract syntax tree). -use super::{Expr, FnAccess, Stmt, StmtBlock, AST_OPTION_FLAGS}; +use super::{Expr, FnAccess, Stmt, StmtBlock, AST_OPTION_FLAGS::*}; use crate::{Dynamic, FnNamespace, Identifier, Position, StaticVec}; #[cfg(feature = "no_std")] use std::prelude::v1::*; use std::{ + fmt, hash::Hash, ops::{Add, AddAssign}, }; @@ -14,7 +15,7 @@ use std::{ /// # Thread Safety /// /// Currently, [`AST`] is neither `Send` nor `Sync`. Turn on the `sync` feature to make it `Send + Sync`. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct AST { /// Source of the [`AST`]. /// No source if string is empty. @@ -23,7 +24,7 @@ pub struct AST { body: StmtBlock, /// Script-defined functions. #[cfg(not(feature = "no_function"))] - functions: crate::Shared, + lib: crate::Shared, /// Embedded module resolver, if any. #[cfg(not(feature = "no_module"))] resolver: Option>, @@ -36,6 +37,31 @@ impl Default for AST { } } +impl fmt::Debug for AST { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut fp = f.debug_struct("AST"); + + if !self.source.is_empty() { + fp.field("source: ", &self.source); + } + #[cfg(not(feature = "no_module"))] + if let Some(ref resolver) = self.resolver { + fp.field("resolver: ", resolver); + } + + fp.field("body", &self.body.as_slice()); + + #[cfg(not(feature = "no_function"))] + if !self.lib.is_empty() { + for (_, _, _, _, ref fn_def) in self.lib.iter_script_fn() { + let sig = fn_def.to_string(); + fp.field(&sig, &fn_def.body.as_slice()); + } + } + fp.finish() + } +} + impl AST { /// Create a new [`AST`]. #[cfg(not(feature = "internals"))] @@ -47,9 +73,9 @@ impl AST { ) -> Self { Self { source: Identifier::new_const(), - body: StmtBlock::new(statements, Position::NONE), + body: StmtBlock::new(statements, Position::NONE, Position::NONE), #[cfg(not(feature = "no_function"))] - functions: functions.into(), + lib: functions.into(), #[cfg(not(feature = "no_module"))] resolver: None, } @@ -65,9 +91,9 @@ impl AST { ) -> Self { Self { source: Identifier::new_const(), - body: StmtBlock::new(statements, Position::NONE), + body: StmtBlock::new(statements, Position::NONE, Position::NONE), #[cfg(not(feature = "no_function"))] - functions: functions.into(), + lib: functions.into(), #[cfg(not(feature = "no_module"))] resolver: None, } @@ -115,7 +141,7 @@ impl AST { source: Identifier::new_const(), body: StmtBlock::NONE, #[cfg(not(feature = "no_function"))] - functions: crate::Module::new().into(), + lib: crate::Module::new().into(), #[cfg(not(feature = "no_module"))] resolver: None, } @@ -140,7 +166,7 @@ impl AST { pub fn set_source(&mut self, source: impl Into) -> &mut Self { let source = source.into(); #[cfg(not(feature = "no_function"))] - crate::Shared::get_mut(&mut self.functions) + crate::Shared::get_mut(&mut self.lib) .as_mut() .map(|m| m.set_id(source.clone())); self.source = source; @@ -181,7 +207,7 @@ impl AST { #[inline(always)] #[must_use] pub fn has_functions(&self) -> bool { - !self.functions.is_empty() + !self.lib.is_empty() } /// Get the internal shared [`Module`][crate::Module] containing all script-defined functions. #[cfg(not(feature = "internals"))] @@ -189,7 +215,7 @@ impl AST { #[inline(always)] #[must_use] pub(crate) fn shared_lib(&self) -> &crate::Shared { - &self.functions + &self.lib } /// _(internals)_ Get the internal shared [`Module`][crate::Module] containing all script-defined functions. /// Exported under the `internals` feature only. @@ -200,7 +226,7 @@ impl AST { #[inline(always)] #[must_use] pub fn shared_lib(&self) -> &crate::Shared { - &self.functions + &self.lib } /// Get the embedded [module resolver][`ModuleResolver`]. #[cfg(not(feature = "internals"))] @@ -260,12 +286,12 @@ impl AST { &self, filter: impl Fn(FnNamespace, FnAccess, bool, &str, usize) -> bool, ) -> Self { - let mut functions = crate::Module::new(); - functions.merge_filtered(&self.functions, &filter); + let mut lib = crate::Module::new(); + lib.merge_filtered(&self.lib, &filter); Self { source: self.source.clone(), body: StmtBlock::NONE, - functions: functions.into(), + lib: lib.into(), #[cfg(not(feature = "no_module"))] resolver: self.resolver.clone(), } @@ -279,7 +305,7 @@ impl AST { source: self.source.clone(), body: self.body.clone(), #[cfg(not(feature = "no_function"))] - functions: crate::Module::new().into(), + lib: crate::Module::new().into(), #[cfg(not(feature = "no_module"))] resolver: self.resolver.clone(), } @@ -472,24 +498,24 @@ impl AST { }; #[cfg(not(feature = "no_function"))] - let functions = { - let mut functions = self.functions.as_ref().clone(); - functions.merge_filtered(&other.functions, &_filter); - functions + let lib = { + let mut lib = self.lib.as_ref().clone(); + lib.merge_filtered(&other.lib, &_filter); + lib }; if !other.source.is_empty() { Self::new_with_source( merged, #[cfg(not(feature = "no_function"))] - functions, + lib, other.source.clone(), ) } else { Self::new( merged, #[cfg(not(feature = "no_function"))] - functions, + lib, ) } } @@ -562,9 +588,9 @@ impl AST { self.body.extend(other.body.into_iter()); #[cfg(not(feature = "no_function"))] - if !other.functions.is_empty() { - crate::func::native::shared_make_mut(&mut self.functions) - .merge_filtered(&other.functions, &_filter); + if !other.lib.is_empty() { + crate::func::native::shared_make_mut(&mut self.lib) + .merge_filtered(&other.lib, &_filter); } self } @@ -599,20 +625,32 @@ impl AST { &mut self, filter: impl Fn(FnNamespace, FnAccess, &str, usize) -> bool, ) -> &mut Self { - if !self.functions.is_empty() { - crate::func::native::shared_make_mut(&mut self.functions) - .retain_script_functions(filter); + if !self.lib.is_empty() { + crate::func::native::shared_make_mut(&mut self.lib).retain_script_functions(filter); } self } + /// _(internals)_ Iterate through all function definitions. + /// Exported under the `internals` feature only. + /// + /// Not available under `no_function`. + #[cfg(feature = "internals")] + #[cfg(not(feature = "no_function"))] + #[inline] + pub fn iter_fn_def(&self) -> impl Iterator { + self.lib + .iter_script_fn() + .map(|(_, _, _, _, fn_def)| fn_def.as_ref()) + } /// Iterate through all function definitions. /// /// Not available under `no_function`. + #[cfg(not(feature = "internals"))] #[cfg(not(feature = "no_function"))] #[allow(dead_code)] #[inline] pub(crate) fn iter_fn_def(&self) -> impl Iterator { - self.functions + self.lib .iter_script_fn() .map(|(_, _, _, _, fn_def)| fn_def.as_ref()) } @@ -622,7 +660,7 @@ impl AST { #[cfg(not(feature = "no_function"))] #[inline] pub fn iter_functions<'a>(&'a self) -> impl Iterator + 'a { - self.functions + self.lib .iter_script_fn() .map(|(_, _, _, _, fn_def)| fn_def.as_ref().into()) } @@ -632,7 +670,7 @@ impl AST { #[cfg(not(feature = "no_function"))] #[inline(always)] pub fn clear_functions(&mut self) -> &mut Self { - self.functions = crate::Module::new().into(); + self.lib = crate::Module::new().into(); self } /// Clear all statements in the [`AST`], leaving only function definitions. @@ -707,16 +745,11 @@ impl AST { ) -> impl Iterator { self.statements().iter().filter_map(move |stmt| match stmt { Stmt::Var(expr, name, options, _) - if options.contains(AST_OPTION_FLAGS::AST_OPTION_CONSTANT) && include_constants - || !options.contains(AST_OPTION_FLAGS::AST_OPTION_CONSTANT) - && include_variables => + if options.contains(AST_OPTION_CONSTANT) && include_constants + || !options.contains(AST_OPTION_CONSTANT) && include_variables => { if let Some(value) = expr.get_literal_value() { - Some(( - name.as_str(), - options.contains(AST_OPTION_FLAGS::AST_OPTION_CONSTANT), - value, - )) + Some((name.as_str(), options.contains(AST_OPTION_CONSTANT), value)) } else { None } @@ -871,6 +904,6 @@ impl AST { #[inline(always)] #[must_use] pub fn lib(&self) -> &crate::Module { - &self.functions + &self.lib } } diff --git a/src/ast/expr.rs b/src/ast/expr.rs index 72dea97c..dffacf42 100644 --- a/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -165,7 +165,7 @@ impl FnCallHashes { /// _(internals)_ A function call. /// Exported under the `internals` feature only. -#[derive(Debug, Clone, Default, Hash)] +#[derive(Clone, Default, Hash)] pub struct FnCallExpr { /// Namespace of the function, if any. /// @@ -191,6 +191,27 @@ pub struct FnCallExpr { pub constants: StaticVec, /// Does this function call capture the parent scope? pub capture_parent_scope: bool, + /// [Position] of the function name. + pub pos: Position, +} + +impl fmt::Debug for FnCallExpr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut ff = f.debug_struct("FnCallExpr"); + #[cfg(not(feature = "no_module"))] + self.namespace.as_ref().map(|ns| ff.field("namespace", ns)); + ff.field("name", &self.name) + .field("hash", &self.hashes) + .field("arg_exprs", &self.args); + if !self.constants.is_empty() { + ff.field("constant_args", &self.constants); + } + if self.capture_parent_scope { + ff.field("capture_parent_scope", &self.capture_parent_scope); + } + ff.field("pos", &self.pos); + ff.finish() + } } impl FnCallExpr { @@ -419,7 +440,7 @@ impl Default for Expr { impl fmt::Debug for Expr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut display_pos = self.position(); + let mut display_pos = self.start_position(); match self { Self::DynamicConstant(value, _) => write!(f, "{:?}", value), @@ -459,26 +480,12 @@ impl fmt::Debug for Expr { f.write_str(")") } Self::Property(x, _) => write!(f, "Property({})", x.2), - Self::Stack(x, _) => write!(f, "StackSlot({})", x), + Self::Stack(x, _) => write!(f, "ConstantArg#{}", x), Self::Stmt(x) => { f.write_str("ExprStmtBlock")?; f.debug_list().entries(x.iter()).finish() } - 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) - .field("args", &x.args); - if !x.constants.is_empty() { - ff.field("constants", &x.constants); - } - if x.capture_parent_scope { - ff.field("capture_parent_scope", &x.capture_parent_scope); - } - ff.finish() - } + Self::FnCall(x, _) => fmt::Debug::fmt(x, f), Self::Index(x, term, pos) => { display_pos = *pos; @@ -625,6 +632,7 @@ impl Expr { args: once(Self::Stack(0, pos)).collect(), constants: once(f.fn_name().into()).collect(), capture_parent_scope: false, + pos, } .into(), pos, @@ -681,15 +689,30 @@ impl Expr { | Self::Map(_, pos) | Self::Variable(_, pos, _) | Self::Stack(_, pos) - | Self::FnCall(_, pos) + | Self::And(_, pos) + | Self::Or(_, pos) | Self::Index(_, _, pos) + | Self::Dot(_, _, pos) | Self::Custom(_, pos) | Self::InterpolatedString(_, pos) | Self::Property(_, pos) => *pos, - Self::Stmt(x) => x.position(), + Self::FnCall(x, _) => x.pos, - Self::And(x, _) | Self::Or(x, _) | Self::Dot(x, _, _) => x.lhs.position(), + Self::Stmt(x) => x.position(), + } + } + /// Get the starting [position][Position] of the expression. + /// For a binary expression, this will be the left-most LHS instead of the operator. + #[inline] + #[must_use] + pub const fn start_position(&self) -> Position { + match self { + Self::And(x, _) | Self::Or(x, _) | Self::Index(x, _, _) | Self::Dot(x, _, _) => { + x.lhs.start_position() + } + Self::FnCall(_, pos) => *pos, + _ => self.position(), } } /// Override the [position][Position] of the expression. @@ -718,7 +741,7 @@ impl Expr { | Self::InterpolatedString(_, pos) | Self::Property(_, pos) => *pos = new_pos, - Self::Stmt(x) => x.set_position(new_pos), + Self::Stmt(x) => x.set_position(new_pos, Position::NONE), } self diff --git a/src/ast/mod.rs b/src/ast/mod.rs index aebf79fc..009b1199 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -11,6 +11,9 @@ pub use ast::{ASTNode, AST}; pub use expr::{BinaryExpr, CustomExpr, Expr, FnCallExpr, FnCallHashes}; pub use flags::{FnAccess, OptionFlags, AST_OPTION_FLAGS}; pub use ident::Ident; +#[cfg(not(feature = "no_module"))] +#[cfg(not(feature = "no_function"))] +pub use script_fn::EncapsulatedEnviron; #[cfg(not(feature = "no_function"))] pub use script_fn::{ScriptFnDef, ScriptFnMetadata}; pub use stmt::{ConditionalStmtBlock, OpAssignment, Stmt, StmtBlock, SwitchCases, TryCatchBlock}; diff --git a/src/ast/script_fn.rs b/src/ast/script_fn.rs index a9d924e7..0daf388f 100644 --- a/src/ast/script_fn.rs +++ b/src/ast/script_fn.rs @@ -2,24 +2,43 @@ #![cfg(not(feature = "no_function"))] use super::{FnAccess, StmtBlock}; -use crate::{Identifier, Module, Shared, StaticVec}; +use crate::{Identifier, StaticVec}; #[cfg(feature = "no_std")] use std::prelude::v1::*; use std::{fmt, hash::Hash}; +/// _(internals)_ Encapsulated AST environment. +/// Exported under the `internals` feature only. +/// +/// 1) other functions defined within the same AST +/// 2) the stack of imported [modules][crate::Module] +/// 3) global constants +/// +/// Not available under `no_module` or `no_function`. +#[cfg(not(feature = "no_module"))] +#[cfg(not(feature = "no_function"))] +#[derive(Debug, Clone)] +pub struct EncapsulatedEnviron { + /// Functions defined within the same [`AST`][crate::AST]. + pub lib: crate::Shared, + /// Imported [modules][crate::Module]. + pub imports: Box<[(Identifier, crate::Shared)]>, + /// Globally-defined constants. + pub constants: Option, +} + /// _(internals)_ A type containing information on a script-defined function. /// Exported under the `internals` feature only. #[derive(Debug, Clone)] pub struct ScriptFnDef { /// Function body. pub body: StmtBlock, - /// Encapsulated running environment, if any. - pub lib: Option>, - /// Encapsulated stack of imported modules, if any. + /// Encapsulated AST environment, if any. /// - /// Not available under `no_module`. + /// Not available under `no_module` or `no_function`. #[cfg(not(feature = "no_module"))] - pub global: Option)]>>, + #[cfg(not(feature = "no_function"))] + pub environ: Option, /// Function name. pub name: Identifier, /// Function access mode. diff --git a/src/ast/stmt.rs b/src/ast/stmt.rs index f9f6717c..ca82fb8f 100644 --- a/src/ast/stmt.rs +++ b/src/ast/stmt.rs @@ -134,7 +134,7 @@ pub struct TryCatchBlock { /// _(internals)_ A scoped block of statements. /// Exported under the `internals` feature only. #[derive(Clone, Hash, Default)] -pub struct StmtBlock(StaticVec, Position); +pub struct StmtBlock(StaticVec, (Position, Position)); impl StmtBlock { /// A [`StmtBlock`] that does not exist. @@ -142,16 +142,20 @@ impl StmtBlock { /// Create a new [`StmtBlock`]. #[must_use] - pub fn new(statements: impl IntoIterator, pos: Position) -> Self { + pub fn new( + statements: impl IntoIterator, + start_pos: Position, + end_pos: Position, + ) -> Self { let mut statements: StaticVec<_> = statements.into_iter().collect(); statements.shrink_to_fit(); - Self(statements, pos) + Self(statements, (start_pos, end_pos)) } /// Create an empty [`StmtBlock`]. #[inline(always)] #[must_use] pub const fn empty(pos: Position) -> Self { - Self(StaticVec::new_const(), pos) + Self(StaticVec::new_const(), (pos, pos)) } /// Is this statements block empty? #[inline(always)] @@ -183,16 +187,42 @@ impl StmtBlock { pub fn iter(&self) -> impl Iterator { self.0.iter() } - /// Get the position (location of the beginning `{`) of this statements block. + /// Get the start position (location of the beginning `{`) of this statements block. #[inline(always)] #[must_use] pub const fn position(&self) -> Position { + (self.1).0 + } + /// Get the end position (location of the ending `}`) of this statements block. + #[inline(always)] + #[must_use] + pub const fn end_position(&self) -> Position { + (self.1).1 + } + /// Get the positions (locations of the beginning `{` and ending `}`) of this statements block. + #[inline(always)] + #[must_use] + pub const fn positions(&self) -> (Position, Position) { self.1 } - /// Set the position (location of the beginning `{`) of this statements block. + /// Get the positions (locations of the beginning `{` and ending `}`) of this statements block + /// or a default. #[inline(always)] - pub fn set_position(&mut self, pos: Position) { - self.1 = pos; + #[must_use] + pub const fn positions_or_else( + &self, + def_start_pos: Position, + def_end_pos: Position, + ) -> (Position, Position) { + ( + (self.1).0.or_else(def_start_pos), + (self.1).1.or_else(def_end_pos), + ) + } + /// Set the positions of this statements block. + #[inline(always)] + pub fn set_position(&mut self, start_pos: Position, end_pos: Position) { + self.1 = (start_pos, end_pos); } } @@ -230,7 +260,12 @@ impl fmt::Debug for StmtBlock { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("Block")?; fmt::Debug::fmt(&self.0, f)?; - self.1.debug_print(f) + (self.1).0.debug_print(f)?; + #[cfg(not(feature = "no_position"))] + if !(self.1).1.is_none() { + write!(f, "-{:?}", (self.1).1)?; + } + Ok(()) } } @@ -239,10 +274,10 @@ impl From for StmtBlock { fn from(stmt: Stmt) -> Self { match stmt { Stmt::Block(mut block, pos) => Self(block.iter_mut().map(mem::take).collect(), pos), - Stmt::Noop(pos) => Self(StaticVec::new_const(), pos), + Stmt::Noop(pos) => Self(StaticVec::new_const(), (pos, pos)), _ => { let pos = stmt.position(); - Self(vec![stmt].into(), pos) + Self(vec![stmt].into(), (pos, Position::NONE)) } } } @@ -309,7 +344,7 @@ pub enum Stmt { /// function call forming one statement. FnCall(Box, Position), /// `{` stmt`;` ... `}` - Block(Box<[Stmt]>, Position), + Block(Box<[Stmt]>, (Position, Position)), /// `try` `{` stmt; ... `}` `catch` `(` var `)` `{` stmt; ... `}` TryCatch(Box, Position), /// [expression][Expr] @@ -377,7 +412,7 @@ impl Stmt { match self { Self::Noop(pos) | Self::BreakLoop(_, pos) - | Self::Block(_, pos) + | Self::Block(_, (pos, _)) | Self::Assignment(_, pos) | Self::FnCall(_, pos) | Self::If(_, _, pos) @@ -389,7 +424,7 @@ impl Stmt { | Self::Var(_, _, _, pos) | Self::TryCatch(_, pos) => *pos, - Self::Expr(x) => x.position(), + Self::Expr(x) => x.start_position(), #[cfg(not(feature = "no_module"))] Self::Import(_, _, pos) => *pos, @@ -405,7 +440,7 @@ impl Stmt { match self { Self::Noop(pos) | Self::BreakLoop(_, pos) - | Self::Block(_, pos) + | Self::Block(_, (pos, _)) | Self::Assignment(_, pos) | Self::FnCall(_, pos) | Self::If(_, _, pos) diff --git a/src/bin/README.md b/src/bin/README.md index e0648866..32ba31ae 100644 --- a/src/bin/README.md +++ b/src/bin/README.md @@ -9,32 +9,29 @@ Tools for running Rhai scripts. | [`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_ | +There is a feature called `bin-features` which automatically includes all the necessary features +required for building these tools. + How to Run ---------- ```sh -cargo run --bin sample_app_to_run -``` - -or with required features - -```sh -cargo run --bin sample_app_to_run --features feature1,feature2,feature3 +cargo run --features bin-features --bin sample_app_to_run ``` How to Install -------------- -To install these all tools (with [`decimal`] and [`metadata`] support), use the following command: +To install these all tools (with full features), use the following command: ```sh -cargo install --path . --bins --features decimal,metadata,debugging,rustyline +cargo install --path . --bins --features bin-features ``` or specifically: ```sh -cargo install --path . --bin rhai-run --features decimal,metadata,debugging,rustyline +cargo install --path . --bin rhai-run --features bin-features ``` diff --git a/src/bin/rhai-dbg.rs b/src/bin/rhai-dbg.rs index a506b640..5158eebb 100644 --- a/src/bin/rhai-dbg.rs +++ b/src/bin/rhai-dbg.rs @@ -1,4 +1,4 @@ -use rhai::debugger::DebuggerCommand; +use rhai::debugger::{BreakPoint, DebuggerCommand, DebuggerEvent}; use rhai::{Dynamic, Engine, EvalAltResult, ImmutableString, Position, Scope}; use std::{ @@ -36,6 +36,32 @@ fn print_source(lines: &[String], pos: Position, offset: usize) { } } +fn print_current_source( + context: &mut rhai::EvalContext, + source: Option<&str>, + pos: Position, + lines: &Vec, +) { + let current_source = &mut *context + .global_runtime_state_mut() + .debugger + .state_mut() + .write_lock::() + .unwrap(); + let src = source.unwrap_or(""); + 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); + } +} + /// Pretty-print error. fn print_error(input: &str, mut err: EvalAltResult) { let lines: Vec<_> = input.trim().split('\n').collect(); @@ -71,36 +97,38 @@ fn print_error(input: &str, mut err: EvalAltResult) { /// 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"); + println!("help, h => print this help"); + println!("quit, q, exit, kill => quit"); + println!("scope => print the scope"); + println!("print, p => print all variables de-duplicated"); + println!("print/p => 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"); + println!("imports => print all imported modules"); + println!("node => print the current AST node"); + println!("list, l => print the current source line"); + println!("backtrace, bt => print the current call-stack"); + println!("info break, i b => print all break-points"); + println!("enable/en => enable a break-point"); + println!("disable/dis => disable a break-point"); + println!("delete, d => delete all break-points"); + println!("delete/d => delete a break-point"); #[cfg(not(feature = "no_position"))] - println!("break => set a new break-point at the current position"); + println!("break, b => 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"); + println!("break/b => 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/b . => set a new break-point for a property access"); + println!("break/b => set a new break-point for a function call"); println!( - "break <#args> => set a new break-point for a function call with #args arguments" + "break/b <#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!("throw [message] => throw an exception (message optional)"); + println!("run, r => restart the script evaluation from beginning"); + println!("step, s => go to the next expression, diving into functions"); + println!("over => go to the next expression, skipping oer functions"); + println!("next, n, => go to the next statement, skipping over functions"); + println!("finish, f => continue until the end of the current function call"); + println!("continue, c => continue normal execution"); println!(); } @@ -220,36 +248,58 @@ fn main() { // Hook up debugger let lines: Vec<_> = script.trim().split('\n').map(|s| s.to_string()).collect(); - engine.on_debugger( + engine.register_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(); + move |context, event, node, source, pos| { + match event { + DebuggerEvent::Step => (), + DebuggerEvent::BreakPoint(n) => { + match context.global_runtime_state().debugger.break_points()[n] { + #[cfg(not(feature = "no_position"))] + BreakPoint::AtPosition { .. } => (), + BreakPoint::AtFunctionName { ref name, .. } + | BreakPoint::AtFunctionCall { ref name, .. } => { + println!("! Call to function {}.", name) + } + #[cfg(not(feature = "no_object"))] + BreakPoint::AtProperty { ref name, .. } => { + println!("! Property {} accessed.", name) + } + } } - - 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); + DebuggerEvent::FunctionExitWithValue(r) => { + println!( + "! Return from function call '{}' => {}", + context + .global_runtime_state() + .debugger + .call_stack() + .last() + .unwrap() + .fn_name, + r + ) + } + DebuggerEvent::FunctionExitWithError(err) => { + println!( + "! Return from function call '{}' with error: {}", + context + .global_runtime_state() + .debugger + .call_stack() + .last() + .unwrap() + .fn_name, + err + ) } } + // Print current source line + print_current_source(context, source, pos, &lines); + // Read stdin for commands let mut input = String::new(); @@ -267,8 +317,8 @@ fn main() { .collect::>() .as_slice() { - ["help", ..] => print_debug_help(), - ["exit", ..] | ["quit", ..] | ["kill", ..] => { + ["help" | "h", ..] => print_debug_help(), + ["exit" | "quit" | "q" | "kill", ..] => { println!("Script terminated. Bye!"); exit(0); } @@ -276,12 +326,14 @@ fn main() { println!("{:?} {}@{:?}", node, source.unwrap_or_default(), pos); println!(); } - ["continue", ..] => break Ok(DebuggerCommand::Continue), - [] | ["step", ..] => break Ok(DebuggerCommand::StepInto), + ["list" | "l", ..] => print_current_source(context, source, pos, &lines), + ["continue" | "c", ..] => break Ok(DebuggerCommand::Continue), + ["finish" | "f", ..] => break Ok(DebuggerCommand::FunctionExit), + [] | ["step" | "s", ..] => break Ok(DebuggerCommand::StepInto), ["over", ..] => break Ok(DebuggerCommand::StepOver), - ["next", ..] => break Ok(DebuggerCommand::Next), + ["next" | "n", ..] => break Ok(DebuggerCommand::Next), ["scope", ..] => print_scope(context.scope(), false), - ["print", var_name, ..] => { + ["print" | "p", var_name, ..] => { if let Some(value) = context.scope().get_value::(var_name) { if value.is::<()>() { println!("=> ()"); @@ -292,7 +344,7 @@ fn main() { eprintln!("Variable not found: {}", var_name); } } - ["print", ..] => print_scope(context.scope(), true), + ["print" | "p"] => print_scope(context.scope(), true), #[cfg(not(feature = "no_module"))] ["imports", ..] => { for (i, (name, module)) in context @@ -311,7 +363,7 @@ fn main() { println!(); } #[cfg(not(feature = "no_function"))] - ["backtrace", ..] => { + ["backtrace" | "bt", ..] => { for frame in context .global_runtime_state() .debugger @@ -322,15 +374,7 @@ fn main() { println!("{}", frame) } } - ["clear", ..] => { - context - .global_runtime_state_mut() - .debugger - .break_points_mut() - .clear(); - println!("All break-points cleared."); - } - ["breakpoints", ..] => Iterator::for_each( + ["info", "break", ..] | ["i", "b", ..] => Iterator::for_each( context .global_runtime_state() .debugger @@ -347,7 +391,7 @@ fn main() { _ => println!("[{}] {}", i + 1, bp), }, ), - ["enable", n, ..] => { + ["enable" | "en", n, ..] => { if let Ok(n) = n.parse::() { let range = 1..=context .global_runtime_state_mut() @@ -370,7 +414,7 @@ fn main() { eprintln!("Invalid break-point: '{}'", n); } } - ["disable", n, ..] => { + ["disable" | "dis", n, ..] => { if let Ok(n) = n.parse::() { let range = 1..=context .global_runtime_state_mut() @@ -393,7 +437,7 @@ fn main() { eprintln!("Invalid break-point: '{}'", n); } } - ["delete", n, ..] => { + ["delete" | "d", n, ..] => { if let Ok(n) = n.parse::() { let range = 1..=context .global_runtime_state_mut() @@ -414,7 +458,15 @@ fn main() { eprintln!("Invalid break-point: '{}'", n); } } - ["break", fn_name, args, ..] => { + ["delete" | "d", ..] => { + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .clear(); + println!("All break-points deleted."); + } + ["break" | "b", fn_name, args, ..] => { if let Ok(args) = args.parse::() { let bp = rhai::debugger::BreakPoint::AtFunctionCall { name: fn_name.trim().into(), @@ -433,7 +485,7 @@ fn main() { } // Property name #[cfg(not(feature = "no_object"))] - ["break", param] if param.starts_with('.') && param.len() > 1 => { + ["break" | "b", param] if param.starts_with('.') && param.len() > 1 => { let bp = rhai::debugger::BreakPoint::AtProperty { name: param[1..].into(), enabled: true, @@ -447,7 +499,7 @@ fn main() { } // Numeric parameter #[cfg(not(feature = "no_position"))] - ["break", param] if param.parse::().is_ok() => { + ["break" | "b", param] if param.parse::().is_ok() => { let n = param.parse::().unwrap(); let range = if source.is_none() { 1..=lines.len() @@ -472,7 +524,7 @@ fn main() { } } // Function name parameter - ["break", param] => { + ["break" | "b", param] => { let bp = rhai::debugger::BreakPoint::AtFunctionName { name: param.trim().into(), enabled: true, @@ -485,7 +537,7 @@ fn main() { .push(bp); } #[cfg(not(feature = "no_position"))] - ["break", ..] => { + ["break" | "b"] => { let bp = rhai::debugger::BreakPoint::AtPosition { source: source.unwrap_or("").into(), pos, @@ -505,7 +557,7 @@ fn main() { let msg = input.trim().splitn(2, ' ').skip(1).next().unwrap_or(""); break Err(EvalAltResult::ErrorRuntime(msg.trim().into(), pos).into()); } - ["run", ..] => { + ["run" | "r", ..] => { println!("Restarting script..."); break Err(EvalAltResult::ErrorTerminated(Dynamic::UNIT, pos).into()); } diff --git a/src/bin/rhai-repl.rs b/src/bin/rhai-repl.rs index 6e964182..c23eab2e 100644 --- a/src/bin/rhai-repl.rs +++ b/src/bin/rhai-repl.rs @@ -44,9 +44,13 @@ 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!("keys => print list of key bindings"); println!("history => print lines history"); + println!("!! => repeat the last history line"); + println!("!<#> => repeat a particular history line"); + println!("! => repeat the last history line starting with some text"); + println!("!? => repeat the last history line containing some text"); println!("scope => print all variables in the scope"); println!("strict => toggle on/off Strict Variables Mode"); #[cfg(not(feature = "no_optimize"))] @@ -295,6 +299,7 @@ fn main() { engine.set_module_resolver(resolver); } + // Register sample functions engine .register_fn("test", |x: INT, y: INT| format!("{} {}", x, y)) .register_fn("test", |x: &mut INT, y: INT, z: &str| { @@ -310,6 +315,10 @@ fn main() { // REPL loop let mut input = String::new(); + let mut replacement = None; + let mut replacement_index = 0; + let mut history_offset = 1; + let mut main_ast = AST::empty(); let mut ast_u = AST::empty(); let mut ast = AST::empty(); @@ -317,35 +326,53 @@ fn main() { print_help(); 'main_loop: loop { - input.clear(); - - loop { - let prompt = if input.is_empty() { - "rhai-repl> " + if let Some(replace) = replacement.take() { + input = replace; + if rl.add_history_entry(input.clone()) { + history_offset += 1; + } + if input.contains('\n') { + println!("[{}] ~~~~", replacement_index); + println!("{}", input); + println!("~~~~"); } else { - " > " - }; + println!("[{}] {}", replacement_index, input); + } + replacement_index = 0; + } else { + input.clear(); - 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()); + loop { + let prompt = if input.is_empty() { + "rhai-repl> " + } else { + " > " + }; + + 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() && !input.starts_with('!') && input.trim() != "history" + { + if rl.add_history_entry(input.clone()) { + history_offset += 1; + } + } + break; } - break; - } - Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break 'main_loop, + Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break 'main_loop, - Err(err) => { - eprintln!("Error: {:?}", err); - break 'main_loop; + Err(err) => { + eprintln!("Error: {:?}", err); + break 'main_loop; + } } } } @@ -370,10 +397,10 @@ fn main() { "history" => { for (i, h) in rl.history().iter().enumerate() { match &h.split('\n').collect::>()[..] { - [line] => println!("[{}] {}", i + 1, line), + [line] => println!("[{}] {}", history_offset + i, line), lines => { for (x, line) in lines.iter().enumerate() { - let number = format!("[{}]", i + 1); + let number = format!("[{}]", history_offset + i); if x == 0 { println!("{} {}", number, line.trim_end()); } else { @@ -446,6 +473,57 @@ fn main() { ); continue; } + "!!" => { + if let Some(line) = rl.history().last() { + replacement = Some(line.clone()); + replacement_index = history_offset + rl.history().len() - 1; + } else { + eprintln!("No lines history!"); + } + continue; + } + _ if script.starts_with("!?") => { + let text = script[2..].trim(); + if let Some((n, line)) = rl + .history() + .iter() + .rev() + .enumerate() + .find(|&(_, h)| h.contains(text)) + { + replacement = Some(line.clone()); + replacement_index = history_offset + (rl.history().len() - 1 - n); + } else { + eprintln!("History line not found: {}", text); + } + continue; + } + _ if script.starts_with('!') => { + if let Ok(num) = script[1..].parse::() { + if num >= history_offset { + if let Some(line) = rl.history().get(num - history_offset) { + replacement = Some(line.clone()); + replacement_index = num; + continue; + } + } + } else { + let prefix = script[1..].trim(); + if let Some((n, line)) = rl + .history() + .iter() + .rev() + .enumerate() + .find(|&(_, h)| h.trim_start().starts_with(prefix)) + { + replacement = Some(line.clone()); + replacement_index = history_offset + (rl.history().len() - 1 - n); + continue; + } + } + eprintln!("History line not found: {}", &script[1..]); + continue; + } _ => (), } diff --git a/src/engine.rs b/src/engine.rs index 14206d6f..86fb6859 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,18 +1,18 @@ //! Main module defining the script evaluation [`Engine`]. use crate::api::custom_syntax::CustomSyntax; -use crate::func::native::{OnDebugCallback, OnParseTokenCallback, OnPrintCallback, OnVarCallback}; +use crate::func::native::{ + OnDebugCallback, OnDefVarCallback, OnParseTokenCallback, OnPrintCallback, OnVarCallback, +}; use crate::packages::{Package, StandardPackage}; use crate::tokenizer::Token; -use crate::types::dynamic::{map_std_type_name, Union}; +use crate::types::dynamic::Union; use crate::{ - Dynamic, Identifier, ImmutableString, Module, Position, RhaiError, RhaiResult, Shared, - StaticVec, ERR, + Dynamic, Identifier, ImmutableString, Module, Position, RhaiResult, Shared, StaticVec, }; #[cfg(feature = "no_std")] use std::prelude::v1::*; use std::{ - any::type_name, collections::{BTreeMap, BTreeSet}, fmt, num::NonZeroU8, @@ -115,6 +115,8 @@ pub struct Engine { pub(crate) custom_keywords: BTreeMap>, /// Custom syntax. pub(crate) custom_syntax: BTreeMap>, + /// Callback closure for filtering variable definition. + pub(crate) def_var_filter: Option>, /// Callback closure for resolving variable access. pub(crate) resolve_var: Option>, /// Callback closure to remap tokens during parsing. @@ -162,6 +164,7 @@ impl fmt::Debug for Engine { .field("disabled_symbols", &self.disabled_symbols) .field("custom_keywords", &self.custom_keywords) .field("custom_syntax", &(!self.custom_syntax.is_empty())) + .field("def_var_filter", &self.def_var_filter.is_some()) .field("resolve_var", &self.resolve_var.is_some()) .field("token_mapper", &self.token_mapper.is_some()) .field("print", &self.print.is_some()) @@ -274,6 +277,7 @@ impl Engine { custom_keywords: BTreeMap::new(), custom_syntax: BTreeMap::new(), + def_var_filter: None, resolve_var: None, token_mapper: None, @@ -337,59 +341,4 @@ impl Engine { result } - - /// Pretty-print a type name. - /// - /// If a type is registered via [`register_type_with_name`][Engine::register_type_with_name], - /// the type name provided for the registration will be used. - /// - /// # Panics - /// - /// Panics if the type name is `&mut`. - #[inline] - #[must_use] - pub fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str { - self.type_names - .get(name) - .map(|s| s.as_str()) - .unwrap_or_else(|| map_std_type_name(name, true)) - } - - /// Format a type name. - /// - /// If a type is registered via [`register_type_with_name`][Engine::register_type_with_name], - /// the type name provided for the registration will be used. - #[cfg(feature = "metadata")] - #[inline] - #[must_use] - pub(crate) fn format_type_name<'a>(&'a self, name: &'a str) -> std::borrow::Cow<'a, str> { - if name.starts_with("&mut ") { - let x = &name[5..]; - let r = self.format_type_name(x); - return if x != r { - format!("&mut {}", r).into() - } else { - name.into() - }; - } - - self.type_names - .get(name) - .map(|s| s.as_str()) - .unwrap_or_else(|| match name { - "INT" => return type_name::(), - #[cfg(not(feature = "no_float"))] - "FLOAT" => return type_name::(), - _ => map_std_type_name(name, false), - }) - .into() - } - - /// Make a `Box<`[`EvalAltResult`][ERR::ErrorMismatchDataType]`>`. - #[inline] - #[must_use] - pub(crate) fn make_type_mismatch_err(&self, typ: &str, pos: Position) -> RhaiError { - ERR::ErrorMismatchDataType(self.map_type_name(type_name::()).into(), typ.into(), pos) - .into() - } } diff --git a/src/eval/chaining.rs b/src/eval/chaining.rs index 53cd1767..a9a34adf 100644 --- a/src/eval/chaining.rs +++ b/src/eval/chaining.rs @@ -146,7 +146,7 @@ impl Engine { match chain_type { #[cfg(not(feature = "no_index"))] ChainType::Indexing => { - let pos = rhs.position(); + let pos = rhs.start_position(); let root_pos = idx_val.position(); let idx_val = idx_val.into_index_value().expect("`ChainType::Index`"); @@ -159,7 +159,7 @@ impl Engine { 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 idx_pos = x.lhs.start_position(); let rhs_chain = rhs.into(); let (try_setter, result) = { @@ -189,8 +189,8 @@ impl Engine { let fn_name = crate::engine::FN_IDX_SET; if let Err(err) = self.exec_fn_call( - global, state, lib, fn_name, hash_set, args, is_ref_mut, true, - root_pos, None, level, + None, global, state, lib, fn_name, hash_set, args, is_ref_mut, + true, root_pos, level, ) { // Just ignore if there is no index setter if !matches!(*err, ERR::ErrorFunctionNotFound(_, _)) { @@ -216,6 +216,7 @@ impl Engine { Ok(ref mut obj_ptr) => { self.eval_op_assignment( global, state, lib, op_info, op_pos, obj_ptr, root, new_val, + level, ) .map_err(|err| err.fill_position(new_pos))?; #[cfg(not(feature = "unchecked"))] @@ -239,8 +240,8 @@ impl Engine { let fn_name = crate::engine::FN_IDX_SET; self.exec_fn_call( - global, state, lib, fn_name, hash_set, args, is_ref_mut, true, - root_pos, None, level, + None, global, state, lib, fn_name, hash_set, args, is_ref_mut, + true, root_pos, level, )?; } @@ -302,6 +303,7 @@ impl Engine { )?; self.eval_op_assignment( global, state, lib, op_info, op_pos, val_target, root, new_val, + level, ) .map_err(|err| err.fill_position(new_pos))?; } @@ -333,8 +335,8 @@ impl Engine { let args = &mut [target.as_mut()]; let (mut orig_val, _) = self .exec_fn_call( - global, state, lib, getter, hash, args, is_ref_mut, true, *pos, - None, level, + None, global, state, lib, getter, hash, args, is_ref_mut, true, + *pos, level, ) .or_else(|err| match *err { // Try an indexer if property does not exist @@ -364,6 +366,7 @@ impl Engine { &mut (&mut orig_val).into(), root, new_val, + level, ) .map_err(|err| err.fill_position(new_pos))?; @@ -373,7 +376,7 @@ impl Engine { let hash = crate::ast::FnCallHashes::from_native(*hash_set); let args = &mut [target.as_mut(), &mut new_val]; self.exec_fn_call( - global, state, lib, setter, hash, args, is_ref_mut, true, *pos, None, + None, global, state, lib, setter, hash, args, is_ref_mut, true, *pos, level, ) .or_else(|err| match *err { @@ -386,8 +389,8 @@ impl Engine { let pos = Position::NONE; self.exec_fn_call( - global, state, lib, fn_name, hash_set, args, is_ref_mut, true, - pos, None, level, + None, global, state, lib, fn_name, hash_set, args, is_ref_mut, + true, pos, level, ) .map_err( |idx_err| match *idx_err { @@ -408,7 +411,7 @@ impl Engine { let hash = crate::ast::FnCallHashes::from_native(*hash_get); let args = &mut [target.as_mut()]; self.exec_fn_call( - global, state, lib, getter, hash, args, is_ref_mut, true, *pos, None, + None, global, state, lib, getter, hash, args, is_ref_mut, true, *pos, level, ) .map_or_else( @@ -508,8 +511,8 @@ impl Engine { // Assume getters are always pure let (mut val, _) = self .exec_fn_call( - global, state, lib, getter, hash_get, args, is_ref_mut, - true, pos, None, level, + None, global, state, lib, getter, hash_get, args, + is_ref_mut, true, pos, level, ) .or_else(|err| match *err { // Try an indexer if property does not exist @@ -556,8 +559,8 @@ impl Engine { let mut arg_values = [target.as_mut(), val]; let args = &mut arg_values; self.exec_fn_call( - global, state, lib, setter, hash_set, args, is_ref_mut, - true, pos, None, level, + None, global, state, lib, setter, hash_set, args, + is_ref_mut, true, pos, level, ) .or_else( |err| match *err { @@ -571,8 +574,8 @@ impl Engine { global.hash_idx_set(), ); self.exec_fn_call( - global, state, lib, fn_name, hash_set, args, - is_ref_mut, true, pos, None, level, + None, global, state, lib, fn_name, hash_set, + args, is_ref_mut, true, pos, level, ) .or_else(|idx_err| match *idx_err { ERR::ErrorIndexingType(_, _) => { @@ -626,7 +629,7 @@ impl Engine { } } // Syntax error - _ => Err(ERR::ErrorDotExpr("".into(), rhs.position()).into()), + _ => Err(ERR::ErrorDotExpr("".into(), rhs.start_position()).into()), } } } @@ -670,7 +673,7 @@ impl Engine { self.inc_operations(&mut global.num_operations, *var_pos)?; let (mut target, _) = - self.search_namespace(scope, global, state, lib, this_ptr, lhs)?; + self.search_namespace(scope, global, state, lib, this_ptr, lhs, level)?; let obj_ptr = &mut target; let root = (x.2.as_str(), *var_pos); @@ -688,7 +691,7 @@ impl Engine { expr => { let value = self.eval_expr(scope, global, state, lib, this_ptr, expr, level)?; let obj_ptr = &mut value.into(); - let root = ("", expr.position()); + let root = ("", expr.start_position()); self.eval_dot_index_chain_helper( global, state, lib, this_ptr, obj_ptr, root, expr, rhs, term, idx_values, chain_type, level, new_val, @@ -732,7 +735,7 @@ impl Engine { (crate::FnArgsVec::with_capacity(args.len()), Position::NONE), |(mut values, mut pos), expr| -> RhaiResultOf<_> { let (value, arg_pos) = self.get_arg_value( - scope, global, state, lib, this_ptr, level, expr, constants, + scope, global, state, lib, this_ptr, expr, constants, level, )?; if values.is_empty() { pos = arg_pos; @@ -778,7 +781,7 @@ impl Engine { (crate::FnArgsVec::with_capacity(args.len()), Position::NONE), |(mut values, mut pos), expr| -> RhaiResultOf<_> { let (value, arg_pos) = self.get_arg_value( - scope, global, state, lib, this_ptr, level, expr, constants, + scope, global, state, lib, this_ptr, expr, constants, level, )?; if values.is_empty() { pos = arg_pos @@ -801,7 +804,10 @@ impl Engine { _ if _parent_chain_type == ChainType::Indexing => self .eval_expr(scope, global, state, lib, this_ptr, lhs, level) .map(|v| { - super::ChainArgument::from_index_value(v.flatten(), lhs.position()) + super::ChainArgument::from_index_value( + v.flatten(), + lhs.start_position(), + ) })?, expr => unreachable!("unknown chained expression: {:?}", expr), }; @@ -825,7 +831,7 @@ impl Engine { _ if _parent_chain_type == ChainType::Indexing => idx_values.push( self.eval_expr(scope, global, state, lib, this_ptr, expr, level) .map(|v| { - super::ChainArgument::from_index_value(v.flatten(), expr.position()) + super::ChainArgument::from_index_value(v.flatten(), expr.start_position()) })?, ), _ => unreachable!("unknown chained expression: {:?}", expr), @@ -1044,7 +1050,7 @@ impl Engine { let pos = Position::NONE; self.exec_fn_call( - global, state, lib, fn_name, hash_get, args, true, true, pos, None, level, + None, global, state, lib, fn_name, hash_get, args, true, true, pos, level, ) .map(|(v, _)| v.into()) } diff --git a/src/eval/debugger.rs b/src/eval/debugger.rs index b6c1e487..d585439b 100644 --- a/src/eval/debugger.rs +++ b/src/eval/debugger.rs @@ -3,10 +3,10 @@ use super::{EvalContext, EvalState, GlobalRuntimeState}; use crate::ast::{ASTNode, Expr, Stmt}; -use crate::{Dynamic, Engine, Identifier, Module, Position, RhaiResultOf, Scope}; -use std::fmt; +use crate::{Dynamic, Engine, EvalAltResult, Identifier, Module, Position, RhaiResultOf, Scope}; #[cfg(feature = "no_std")] use std::prelude::v1::*; +use std::{fmt, mem}; /// Callback function to initialize the debugger. #[cfg(not(feature = "sync"))] @@ -17,11 +17,22 @@ 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; +pub type OnDebuggerCallback = dyn Fn( + &mut EvalContext, + DebuggerEvent, + ASTNode, + Option<&str>, + Position, +) -> RhaiResultOf; /// Callback function for debugging. #[cfg(feature = "sync")] -pub type OnDebuggerCallback = dyn Fn(&mut EvalContext, ASTNode, Option<&str>, Position) -> RhaiResultOf +pub type OnDebuggerCallback = dyn Fn( + &mut EvalContext, + DebuggerEvent, + ASTNode, + Option<&str>, + Position, + ) -> RhaiResultOf + Send + Sync; @@ -36,6 +47,50 @@ pub enum DebuggerCommand { StepOver, // Run to the next statement, skipping over functions. Next, + // Run to the end of the current function call. + FunctionExit, +} + +impl Default for DebuggerCommand { + #[inline(always)] + fn default() -> Self { + Self::Continue + } +} + +/// The debugger status. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum DebuggerStatus { + // Stop at the next statement or expression. + Next(bool, bool), + // Run to the end of the current level of function call. + FunctionExit(usize), +} + +impl Default for DebuggerStatus { + #[inline(always)] + fn default() -> Self { + Self::CONTINUE + } +} + +impl DebuggerStatus { + pub const CONTINUE: Self = Self::Next(false, false); + pub const STEP: Self = Self::Next(true, true); + pub const NEXT: Self = Self::Next(true, false); +} + +/// A event that triggers the debugger. +#[derive(Debug, Clone, Copy)] +pub enum DebuggerEvent<'a> { + // Break on next step. + Step, + // Break on break-point. + BreakPoint(usize), + // Return from a function with a value. + FunctionExitWithValue(&'a Dynamic), + // Return from a function with a value. + FunctionExitWithError(&'a EvalAltResult), } /// A break-point for debugging. @@ -158,7 +213,6 @@ impl BreakPoint { } /// A function call. -#[cfg(not(feature = "no_function"))] #[derive(Debug, Clone, Hash)] pub struct CallStackFrame { /// Function name. @@ -171,7 +225,6 @@ pub struct CallStackFrame { 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); @@ -198,13 +251,12 @@ impl fmt::Display for CallStackFrame { #[derive(Debug, Clone, Hash)] pub struct Debugger { /// The current status command. - status: DebuggerCommand, + pub(crate) status: DebuggerStatus, /// 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, } @@ -215,9 +267,9 @@ impl Debugger { pub fn new(engine: &Engine) -> Self { Self { status: if engine.debugger.is_some() { - DebuggerCommand::StepInto + DebuggerStatus::STEP } else { - DebuggerCommand::Continue + DebuggerStatus::CONTINUE }, state: if let Some((ref init, _)) = engine.debugger { init() @@ -225,7 +277,6 @@ impl Debugger { Dynamic::UNIT }, break_points: Vec::new(), - #[cfg(not(feature = "no_function"))] call_stack: Vec::new(), } } @@ -242,26 +293,17 @@ impl Debugger { &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, @@ -277,34 +319,37 @@ impl Debugger { 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; + /// Change the current status to [`CONTINUE`][DebuggerStatus::CONTINUE] and return the previous status. + pub(crate) fn clear_status_if( + &mut self, + filter: impl Fn(&DebuggerStatus) -> bool, + ) -> Option { + if filter(&self.status) { + Some(mem::replace(&mut self.status, DebuggerStatus::CONTINUE)) + } else { + None } } - /// Does a particular [`AST` Node][ASTNode] trigger a break-point? + /// Override the status of this [`Debugger`] if it is [`Some`] the current status is + /// [`CONTINUE`][DebuggerStatus::CONTINUE]. + #[inline(always)] + pub(crate) fn reset_status(&mut self, status: Option) { + if self.status == DebuggerStatus::CONTINUE { + if let Some(cmd) = status { + self.status = cmd; + } + } + } + /// Returns the first break-point triggered by a particular [`AST` Node][ASTNode]. #[must_use] - pub fn is_break_point(&self, src: &str, node: ASTNode) -> bool { + pub fn is_break_point(&self, src: &str, node: ASTNode) -> Option { let _src = src; self.break_points() .iter() - .filter(|&bp| bp.is_enabled()) - .any(|bp| match bp { + .enumerate() + .filter(|&(_, bp)| bp.is_enabled()) + .find(|&(_, bp)| match bp { #[cfg(not(feature = "no_position"))] BreakPoint::AtPosition { pos, .. } if pos.is_none() => false, #[cfg(not(feature = "no_position"))] @@ -316,13 +361,15 @@ impl Debugger { node.position() == *pos && _src == source } BreakPoint::AtFunctionName { name, .. } => match node { - ASTNode::Expr(Expr::FnCall(x, _)) | ASTNode::Stmt(Stmt::FnCall(x, _)) => { - x.name == *name - } + ASTNode::Expr(Expr::FnCall(x, _)) + | ASTNode::Stmt(Stmt::FnCall(x, _)) + | ASTNode::Stmt(Stmt::Expr(Expr::FnCall(x, _))) => x.name == *name, _ => false, }, BreakPoint::AtFunctionCall { name, args, .. } => match node { - ASTNode::Expr(Expr::FnCall(x, _)) | ASTNode::Stmt(Stmt::FnCall(x, _)) => { + ASTNode::Expr(Expr::FnCall(x, _)) + | ASTNode::Stmt(Stmt::FnCall(x, _)) + | ASTNode::Stmt(Stmt::Expr(Expr::FnCall(x, _))) => { x.args.len() == *args && x.name == *name } _ => false, @@ -333,6 +380,7 @@ impl Debugger { _ => false, }, }) + .map(|(i, _)| i) } /// Get a slice of all [`BreakPoint`]'s. #[inline(always)] @@ -349,7 +397,7 @@ impl Debugger { } impl Engine { - /// Run the debugger callback. + /// Run the debugger callback if there is a debugging interface registered. #[inline(always)] pub(crate) fn run_debugger<'a>( &self, @@ -361,26 +409,23 @@ impl Engine { 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; + if self.debugger.is_some() { + if let Some(cmd) = + self.run_debugger_with_reset_raw(scope, global, state, lib, this_ptr, node, level)? + { + global.debugger.status = cmd; + } } Ok(()) } - /// Run the debugger callback. + /// Run the debugger callback if there is a debugging interface registered. /// - /// Returns `true` if the debugger needs to be reactivated at the end of the block, statement or + /// Returns `Some` 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] + #[inline(always)] #[must_use] pub(crate) fn run_debugger_with_reset<'a>( &self, @@ -391,61 +436,125 @@ impl Engine { this_ptr: &mut Option<&mut Dynamic>, node: impl Into>, level: usize, - ) -> RhaiResultOf> { - if let Some((_, ref on_debugger)) = self.debugger { - let node = node.into(); + ) -> RhaiResultOf> { + if self.debugger.is_some() { + self.run_debugger_with_reset_raw(scope, global, state, lib, this_ptr, node, level) + } else { + Ok(None) + } + } + /// Run the debugger callback. + /// + /// Returns `Some` if the debugger needs to be reactivated at the end of the block, statement or + /// function call. + /// + /// It is up to the [`Engine`] to reactivate the debugger. + #[inline] + #[must_use] + pub(crate) fn run_debugger_with_reset_raw<'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> { + let node = node.into(); - // Skip transitive nodes - match node { - ASTNode::Expr(Expr::Stmt(_)) | ASTNode::Stmt(Stmt::Expr(_)) => return Ok(None), - _ => (), - } + // 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, - }; + let stop = match global.debugger.status { + DebuggerStatus::Next(false, false) => false, + DebuggerStatus::Next(true, false) => matches!(node, ASTNode::Stmt(_)), + DebuggerStatus::Next(false, true) => matches!(node, ASTNode::Expr(_)), + DebuggerStatus::Next(true, true) => true, + DebuggerStatus::FunctionExit(_) => false, + }; - if !stop && !global.debugger.is_break_point(&global.source, node) { + let event = if stop { + DebuggerEvent::Step + } else { + if let Some(bp) = global.debugger.is_break_point(&global.source, node) { + DebuggerEvent::BreakPoint(bp) + } else { return Ok(None); } + }; - let source = global.source.clone(); - let source = if source.is_empty() { - None - } else { - Some(source.as_str()) - }; + self.run_debugger_raw(scope, global, state, lib, this_ptr, node, event, level) + } + /// Run the debugger callback unconditionally. + /// + /// Returns `Some` if the debugger needs to be reactivated at the end of the block, statement or + /// function call. + /// + /// It is up to the [`Engine`] to reactivate the debugger. + #[inline] + #[must_use] + pub(crate) fn run_debugger_raw<'a>( + &self, + scope: &mut Scope, + global: &mut GlobalRuntimeState, + state: &mut EvalState, + lib: &[&Module], + this_ptr: &mut Option<&mut Dynamic>, + node: ASTNode<'a>, + event: DebuggerEvent, + level: usize, + ) -> Result, Box> { + 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 mut context = crate::EvalContext { + engine: self, + scope, + global, + state, + lib, + this_ptr, + level, + }; - let command = on_debugger(&mut context, node, source, node.position())?; + if let Some((_, ref on_debugger)) = self.debugger { + let command = on_debugger(&mut context, event, node, source, node.position())?; match command { DebuggerCommand::Continue => { - global.debugger.status = DebuggerCommand::Continue; + global.debugger.status = DebuggerStatus::CONTINUE; Ok(None) } DebuggerCommand::Next => { - global.debugger.status = DebuggerCommand::Continue; - Ok(Some(DebuggerCommand::Next)) - } - DebuggerCommand::StepInto => { - global.debugger.status = DebuggerCommand::StepInto; - Ok(None) + global.debugger.status = DebuggerStatus::CONTINUE; + Ok(Some(DebuggerStatus::NEXT)) } DebuggerCommand::StepOver => { - global.debugger.status = DebuggerCommand::Continue; - Ok(Some(DebuggerCommand::StepOver)) + global.debugger.status = DebuggerStatus::CONTINUE; + Ok(Some(DebuggerStatus::STEP)) + } + DebuggerCommand::StepInto => { + global.debugger.status = DebuggerStatus::STEP; + Ok(None) + } + DebuggerCommand::FunctionExit => { + // Bump a level if it is a function call + let level = match node { + ASTNode::Expr(Expr::FnCall(_, _)) + | ASTNode::Stmt(Stmt::FnCall(_, _)) + | ASTNode::Stmt(Stmt::Expr(Expr::FnCall(_, _))) => context.call_level() + 1, + _ => context.call_level(), + }; + global.debugger.status = DebuggerStatus::FunctionExit(level); + Ok(None) } } } else { diff --git a/src/eval/expr.rs b/src/eval/expr.rs index 6b8a1287..b7e89972 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -50,17 +50,22 @@ impl Engine { lib: &[&Module], this_ptr: &'s mut Option<&mut Dynamic>, expr: &Expr, + level: usize, ) -> RhaiResultOf<(Target<'s>, Position)> { match expr { Expr::Variable(Some(_), _, _) => { - self.search_scope_only(scope, global, state, lib, this_ptr, expr) + self.search_scope_only(scope, global, state, lib, this_ptr, expr, level) } 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), + (_, None, _) => { + self.search_scope_only(scope, global, state, lib, this_ptr, expr, level) + } #[cfg(feature = "no_module")] - (_, (), _) => self.search_scope_only(scope, global, state, lib, this_ptr, expr), + (_, (), _) => { + self.search_scope_only(scope, global, state, lib, this_ptr, expr, level) + } // Qualified variable access #[cfg(not(feature = "no_module"))] @@ -136,6 +141,7 @@ impl Engine { lib: &[&Module], this_ptr: &'s mut Option<&mut Dynamic>, expr: &Expr, + level: usize, ) -> RhaiResultOf<(Target<'s>, Position)> { // Make sure that the pointer indirection is taken only when absolutely necessary. @@ -148,7 +154,7 @@ impl Engine { Err(ERR::ErrorUnboundThis(*pos).into()) } } - _ if state.always_search_scope => (0, expr.position()), + _ if state.always_search_scope => (0, expr.start_position()), Expr::Variable(Some(i), pos, _) => (i.get() as usize, *pos), Expr::Variable(None, pos, v) => (v.0.map(NonZeroUsize::get).unwrap_or(0), *pos), _ => unreachable!("Expr::Variable expected but gets {:?}", expr), @@ -163,7 +169,7 @@ impl Engine { state, lib, this_ptr, - level: 0, + level, }; match resolve_var( expr.get_variable_name(true).expect("`Expr::Variable`"), @@ -236,8 +242,8 @@ impl Engine { ); self.make_function_call( - scope, global, state, lib, this_ptr, name, first_arg, args, constants, *hashes, pos, - *capture, level, + scope, global, state, lib, this_ptr, name, first_arg, args, constants, *hashes, + *capture, pos, level, ) } @@ -297,7 +303,7 @@ impl Engine { .cloned() .ok_or_else(|| ERR::ErrorUnboundThis(*var_pos).into()) } else { - self.search_namespace(scope, global, state, lib, this_ptr, expr) + self.search_namespace(scope, global, state, lib, this_ptr, expr, level) .map(|(val, _)| val.take_or_clone()) }; } @@ -345,12 +351,13 @@ impl Engine { &mut (&mut concat).into(), ("", Position::NONE), item, + level, ) { - result = Err(err.fill_position(expr.position())); + result = Err(err.fill_position(expr.start_position())); break; } - pos = expr.position(); + pos = expr.start_position(); } result.map(|_| concat) @@ -503,7 +510,7 @@ impl Engine { let result = (custom_def.func)(&mut context, &expressions); - self.check_return_value(result, expr.position()) + self.check_return_value(result, expr.start_position()) } Expr::Stmt(x) if x.is_empty() => Ok(Dynamic::UNIT), diff --git a/src/eval/global_state.rs b/src/eval/global_state.rs index fa7979b7..22f2574f 100644 --- a/src/eval/global_state.rs +++ b/src/eval/global_state.rs @@ -5,6 +5,12 @@ use crate::{Engine, Identifier}; use std::prelude::v1::*; use std::{fmt, marker::PhantomData}; +/// Collection of globally-defined constants. +#[cfg(not(feature = "no_module"))] +#[cfg(not(feature = "no_function"))] +pub type GlobalConstants = + crate::Shared>>; + /// _(internals)_ Global runtime states. /// Exported under the `internals` feature only. // @@ -44,9 +50,7 @@ pub struct GlobalRuntimeState<'a> { /// Interior mutability is needed because it is shared in order to aid in cloning. #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_function"))] - pub(crate) constants: Option< - crate::Shared>>, - >, + pub(crate) constants: Option, /// Debugging interface. #[cfg(feature = "debugging")] pub debugger: super::Debugger, diff --git a/src/eval/mod.rs b/src/eval/mod.rs index b76619e4..0b87a2dd 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -11,11 +11,14 @@ 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 debugger::{ + BreakPoint, CallStackFrame, Debugger, DebuggerCommand, DebuggerEvent, DebuggerStatus, + OnDebuggerCallback, OnDebuggingInit, +}; pub use eval_context::EvalContext; pub use eval_state::EvalState; +#[cfg(not(feature = "no_module"))] +#[cfg(not(feature = "no_function"))] +pub use global_state::GlobalConstants; pub use global_state::GlobalRuntimeState; pub use target::{calc_index, calc_offset_len, Target}; diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index a5dd2da4..1c4a06dc 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -1,6 +1,6 @@ //! Module defining functions for evaluating a statement. -use super::{EvalState, GlobalRuntimeState, Target}; +use super::{EvalContext, EvalState, GlobalRuntimeState, Target}; use crate::ast::{ BinaryExpr, Expr, Ident, OpAssignment, Stmt, SwitchCases, TryCatchBlock, AST_OPTION_FLAGS::*, }; @@ -120,6 +120,7 @@ impl Engine { target: &mut Target, root: (&str, Position), new_val: Dynamic, + level: usize, ) -> RhaiResultOf<()> { if target.is_read_only() { // Assignment to constant variable @@ -152,9 +153,10 @@ impl Engine { let hash = hash_op_assign; let args = &mut [lhs_ptr_inner, &mut new_val]; + let level = level + 1; match self.call_native_fn( - global, state, lib, op_assign, hash, args, true, true, op_pos, + global, state, lib, op_assign, hash, args, true, true, op_pos, level, ) { Ok(_) => { #[cfg(not(feature = "unchecked"))] @@ -164,7 +166,7 @@ impl Engine { { // Expand to `var = var op rhs` let (value, _) = self.call_native_fn( - global, state, lib, op, hash_op, args, true, false, op_pos, + global, state, lib, op, hash_op, args, true, false, op_pos, level, )?; #[cfg(not(feature = "unchecked"))] @@ -238,7 +240,7 @@ impl Engine { if let Ok(rhs_val) = rhs_result { let search_result = - self.search_namespace(scope, global, state, lib, this_ptr, lhs); + self.search_namespace(scope, global, state, lib, this_ptr, lhs, level); if let Ok(search_val) = search_result { let (mut lhs_ptr, pos) = search_val; @@ -263,8 +265,9 @@ impl Engine { &mut lhs_ptr, (var_name, pos), rhs_val, + level, ) - .map_err(|err| err.fill_position(rhs.position())) + .map_err(|err| err.fill_position(rhs.start_position())) .map(|_| Dynamic::UNIT) } else { search_result.map(|_| Dynamic::UNIT) @@ -280,7 +283,7 @@ impl Engine { .map(Dynamic::flatten); if let Ok(rhs_val) = rhs_result { - let _new_val = Some(((rhs_val, rhs.position()), (*op_info, *op_pos))); + let _new_val = Some(((rhs_val, rhs.start_position()), (*op_info, *op_pos))); // Must be either `var[index] op= val` or `var.prop op= val` match lhs { @@ -683,7 +686,7 @@ impl Engine { loop_result } else { - Err(ERR::ErrorFor(expr.position()).into()) + Err(ERR::ErrorFor(expr.start_position()).into()) } } else { iter_result @@ -799,9 +802,14 @@ impl Engine { // Empty return Stmt::Return(_, None, pos) => Err(ERR::Return(Dynamic::UNIT, *pos).into()), + // Let/const statement - shadowing disallowed + Stmt::Var(_, x, _, pos) if !self.allow_shadowing() && scope.contains(&x.name) => { + Err(ERR::ErrorVariableExists(x.name.to_string(), *pos).into()) + } // Let/const statement - Stmt::Var(expr, x, options, _) => { + Stmt::Var(expr, x, options, pos) => { let var_name = &x.name; + let entry_type = if options.contains(AST_OPTION_CONSTANT) { AccessMode::ReadOnly } else { @@ -809,48 +817,79 @@ impl Engine { }; let export = options.contains(AST_OPTION_EXPORTED); - let value_result = self - .eval_expr(scope, global, state, lib, this_ptr, expr, level) - .map(Dynamic::flatten); - - 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) - } else { - None - } - } else if export { - unreachable!("exported variable not on global level"); - } else { - None + let result = if let Some(ref filter) = self.def_var_filter { + let shadowing = scope.contains(var_name); + let scope_level = state.scope_level; + let is_const = entry_type == AccessMode::ReadOnly; + let context = EvalContext { + engine: self, + scope, + global, + state, + lib, + this_ptr, + level: level, }; - 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()); + match filter(var_name, is_const, scope_level, shadowing, &context) { + Ok(true) => None, + Ok(false) => Some(Err(ERR::ErrorRuntime( + format!("Variable cannot be defined: {}", var_name).into(), + *pos, + ) + .into())), + err @ Err(_) => Some(err), } - - Ok(Dynamic::UNIT) } else { - value_result + None + }; + + if let Some(result) = result { + result.map(|_| Dynamic::UNIT) + } else { + let value_result = self + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .map(Dynamic::flatten); + + 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) + } 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()); + } + + Ok(Dynamic::UNIT) + } else { + value_result + } } } @@ -866,9 +905,10 @@ impl Engine { let path_result = self .eval_expr(scope, global, state, lib, this_ptr, &expr, level) .and_then(|v| { + let typ = v.type_name(); v.try_cast::().ok_or_else(|| { self.make_type_mismatch_err::( - "", + typ, expr.position(), ) }) @@ -877,7 +917,7 @@ impl Engine { if let Ok(path) = path_result { use crate::ModuleResolver; - let path_pos = expr.position(); + let path_pos = expr.start_position(); let resolver = global.embedded_module_resolver.clone(); diff --git a/src/func/call.rs b/src/func/call.rs index f7c45e27..e688178a 100644 --- a/src/func/call.rs +++ b/src/func/call.rs @@ -328,6 +328,8 @@ impl Engine { result.as_ref().map(Box::as_ref) } + /// # Main Entry-Point + /// /// Call a native Rust function registered with the [`Engine`]. /// /// # WARNING @@ -347,6 +349,7 @@ impl Engine { is_ref_mut: bool, is_op_assign: bool, pos: Position, + level: usize, ) -> RhaiResultOf<(Dynamic, bool)> { #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, pos)?; @@ -365,43 +368,92 @@ impl Engine { is_op_assign, ); - if let Some(FnResolutionCacheEntry { func, source }) = func { - assert!(func.is_native()); + if func.is_some() { + let is_method = func.map(|f| f.func.is_method()).unwrap_or(false); - // Calling pure function but the first argument is a reference? - let mut backup: Option = None; - if is_ref_mut && func.is_pure() && !args.is_empty() { - // Clone the first argument - backup = Some(ArgBackup::new()); - backup - .as_mut() - .expect("`Some`") - .change_first_arg_to_copy(args); - } + // Push a new call stack frame + #[cfg(feature = "debugging")] + let orig_call_stack_len = global.debugger.call_stack().len(); - // Run external function - let source = match (source.as_str(), parent_source.as_str()) { - ("", "") => None, - ("", s) | (s, _) => Some(s), - }; + let mut _result = if let Some(FnResolutionCacheEntry { func, source }) = func { + assert!(func.is_native()); - let context = (self, name, source, &*global, lib, pos).into(); + // Calling pure function but the first argument is a reference? + let mut backup: Option = None; + if is_ref_mut && func.is_pure() && !args.is_empty() { + // Clone the first argument + backup = Some(ArgBackup::new()); + backup + .as_mut() + .expect("`Some`") + .change_first_arg_to_copy(args); + } - let result = if func.is_plugin_fn() { - func.get_plugin_fn() - .expect("plugin function") - .call(context, args) + let source = match (source.as_str(), parent_source.as_str()) { + ("", "") => None, + ("", s) | (s, _) => Some(s), + }; + + #[cfg(feature = "debugging")] + if self.debugger.is_some() { + global.debugger.push_call_stack_frame( + name, + args.iter().map(|v| (*v).clone()).collect(), + source.unwrap_or(""), + pos, + ); + } + + // Run external function + let context = (self, name, source, &*global, lib, pos, level).into(); + + let result = if func.is_plugin_fn() { + func.get_plugin_fn() + .expect("plugin function") + .call(context, args) + } else { + func.get_native_fn().expect("native function")(context, args) + }; + + // Restore the original reference + if let Some(bk) = backup { + bk.restore_first_arg(args) + } + + result } else { - func.get_native_fn().expect("native function")(context, args) + unreachable!("`Some`"); }; - // Restore the original reference - if let Some(bk) = backup { - bk.restore_first_arg(args) + #[cfg(feature = "debugging")] + { + let trigger = match global.debugger.status { + crate::eval::DebuggerStatus::FunctionExit(n) => n >= level, + crate::eval::DebuggerStatus::Next(_, true) => true, + _ => false, + }; + if trigger { + let scope = &mut &mut Scope::new(); + let node = crate::ast::Stmt::Noop(pos); + let node = (&node).into(); + let event = match _result { + Ok(ref r) => crate::eval::DebuggerEvent::FunctionExitWithValue(r), + Err(ref err) => crate::eval::DebuggerEvent::FunctionExitWithError(err), + }; + match self + .run_debugger_raw(scope, global, state, lib, &mut None, node, event, level) + { + Ok(_) => (), + Err(err) => _result = Err(err), + } + } + + // Pop the call stack + global.debugger.rewind_call_stack(orig_call_stack_len); } // Check the return value (including data sizes) - let result = self.check_return_value(result, pos)?; + let result = self.check_return_value(_result, pos)?; // Check the data size of any `&mut` object, which may be changed. #[cfg(not(feature = "unchecked"))] @@ -443,7 +495,7 @@ impl Engine { (Dynamic::UNIT, false) } } - _ => (result, func.is_method()), + _ => (result, is_method), }); } @@ -530,6 +582,8 @@ impl Engine { } } + /// # Main Entry-Point + /// /// Perform an actual function call, native Rust or scripted, taking care of special functions. /// /// # WARNING @@ -540,6 +594,7 @@ impl Engine { /// **DO NOT** reuse the argument values unless for the first `&mut` argument - all others are silently replaced by `()`! pub(crate) fn exec_fn_call( &self, + scope: Option<&mut Scope>, global: &mut GlobalRuntimeState, state: &mut EvalState, lib: &[&Module], @@ -549,7 +604,6 @@ impl Engine { is_ref_mut: bool, is_method_call: bool, pos: Position, - scope: Option<&mut Scope>, level: usize, ) -> RhaiResultOf<(Dynamic, bool)> { fn no_method_err(name: &str, pos: Position) -> RhaiResultOf<(Dynamic, bool)> { @@ -562,7 +616,6 @@ impl Engine { ensure_no_data_race(fn_name, args, is_ref_mut)?; let _scope = scope; - let _level = level; let _is_method_call = is_method_call; // These may be redirected from method style calls. @@ -612,6 +665,8 @@ impl Engine { _ => (), } + let level = level + 1; + // Script-defined function call? #[cfg(not(feature = "no_function"))] if let Some(FnResolutionCacheEntry { func, mut source }) = self @@ -651,8 +706,6 @@ impl Engine { // Method call of script function - map first argument to `this` let (first_arg, rest_args) = args.split_first_mut().unwrap(); - let level = _level + 1; - let result = self.call_script_fn( scope, global, @@ -661,8 +714,8 @@ impl Engine { &mut Some(*first_arg), func, rest_args, - pos, true, + pos, level, ); @@ -679,10 +732,8 @@ impl Engine { .change_first_arg_to_copy(args); } - let level = _level + 1; - let result = self.call_script_fn( - scope, global, state, lib, &mut None, func, args, pos, true, level, + scope, global, state, lib, &mut None, func, args, true, pos, level, ); // Restore the original reference @@ -702,7 +753,7 @@ impl Engine { // Native function call let hash = hashes.native; self.call_native_fn( - global, state, lib, fn_name, hash, args, is_ref_mut, false, pos, + global, state, lib, fn_name, hash, args, is_ref_mut, false, pos, level, ) } @@ -764,7 +815,7 @@ impl Engine { // Map it to name(args) in function-call style self.exec_fn_call( - global, state, lib, fn_name, new_hash, &mut args, false, false, pos, None, + None, global, state, lib, fn_name, new_hash, &mut args, false, false, pos, level, ) } @@ -804,7 +855,7 @@ impl Engine { // Map it to name(args) in function-call style self.exec_fn_call( - global, state, lib, fn_name, new_hash, &mut args, is_ref_mut, true, pos, None, + None, global, state, lib, fn_name, new_hash, &mut args, is_ref_mut, true, pos, level, ) } @@ -876,7 +927,7 @@ impl Engine { args.extend(call_args.iter_mut()); self.exec_fn_call( - global, state, lib, fn_name, hash, &mut args, is_ref_mut, true, pos, None, + None, global, state, lib, fn_name, hash, &mut args, is_ref_mut, true, pos, level, ) } @@ -901,9 +952,9 @@ impl Engine { state: &mut EvalState, lib: &[&Module], this_ptr: &mut Option<&mut Dynamic>, - level: usize, arg_expr: &Expr, constants: &[Dynamic], + level: usize, ) -> RhaiResultOf<(Dynamic, Position)> { Ok(( if let Expr::Stack(slot, _) = arg_expr { @@ -915,9 +966,21 @@ impl Engine { 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)? + // Do not match function exit for arguments + #[cfg(feature = "debugging")] + let reset_debugger = global.debugger.clear_status_if(|status| { + matches!(status, crate::eval::DebuggerStatus::FunctionExit(_)) + }); + + let result = self.eval_expr(scope, global, state, lib, this_ptr, arg_expr, level); + + // Restore function exit status + #[cfg(feature = "debugging")] + global.debugger.reset_status(reset_debugger); + + result? }, - arg_expr.position(), + arg_expr.start_position(), )) } @@ -934,8 +997,8 @@ impl Engine { args_expr: &[Expr], constants: &[Dynamic], hashes: FnCallHashes, - pos: Position, capture_scope: bool, + pos: Position, level: usize, ) -> RhaiResult { let mut first_arg = first_arg; @@ -951,7 +1014,7 @@ impl Engine { KEYWORD_FN_PTR_CALL if total_args >= 1 => { let arg = first_arg.unwrap(); let (arg_value, arg_pos) = - self.get_arg_value(scope, global, state, lib, this_ptr, level, arg, constants)?; + self.get_arg_value(scope, global, state, lib, this_ptr, arg, constants, level)?; if !arg_value.is::() { return Err(self.make_type_mismatch_err::( @@ -986,7 +1049,7 @@ impl Engine { KEYWORD_FN_PTR if total_args == 1 => { let arg = first_arg.unwrap(); let (arg_value, arg_pos) = - self.get_arg_value(scope, global, state, lib, this_ptr, level, arg, constants)?; + self.get_arg_value(scope, global, state, lib, this_ptr, arg, constants, level)?; // Fn - only in function call style return arg_value @@ -1001,7 +1064,7 @@ impl Engine { KEYWORD_FN_PTR_CURRY if total_args > 1 => { let first = first_arg.unwrap(); let (arg_value, arg_pos) = self - .get_arg_value(scope, global, state, lib, this_ptr, level, first, constants)?; + .get_arg_value(scope, global, state, lib, this_ptr, first, constants, level)?; if !arg_value.is::() { return Err(self.make_type_mismatch_err::( @@ -1018,7 +1081,7 @@ impl Engine { .iter() .try_fold(fn_curry, |mut curried, expr| -> RhaiResultOf<_> { let (value, _) = self.get_arg_value( - scope, global, state, lib, this_ptr, level, expr, constants, + scope, global, state, lib, this_ptr, expr, constants, level, )?; curried.push(value); Ok(curried) @@ -1032,7 +1095,7 @@ impl Engine { crate::engine::KEYWORD_IS_SHARED if total_args == 1 => { let arg = first_arg.unwrap(); let (arg_value, _) = - self.get_arg_value(scope, global, state, lib, this_ptr, level, arg, constants)?; + self.get_arg_value(scope, global, state, lib, this_ptr, arg, constants, level)?; return Ok(arg_value.is_shared().into()); } @@ -1041,14 +1104,14 @@ impl Engine { crate::engine::KEYWORD_IS_DEF_FN if total_args == 2 => { let first = first_arg.unwrap(); let (arg_value, arg_pos) = self - .get_arg_value(scope, global, state, lib, this_ptr, level, first, constants)?; + .get_arg_value(scope, global, state, lib, this_ptr, first, constants, level)?; let fn_name = arg_value .into_immutable_string() .map_err(|typ| self.make_type_mismatch_err::(typ, arg_pos))?; let (arg_value, arg_pos) = self.get_arg_value( - scope, global, state, lib, this_ptr, level, &a_expr[0], constants, + scope, global, state, lib, this_ptr, &a_expr[0], constants, level, )?; let num_params = arg_value @@ -1068,7 +1131,7 @@ impl Engine { KEYWORD_IS_DEF_VAR if total_args == 1 => { let arg = first_arg.unwrap(); let (arg_value, arg_pos) = - self.get_arg_value(scope, global, state, lib, this_ptr, level, arg, constants)?; + self.get_arg_value(scope, global, state, lib, this_ptr, arg, constants, level)?; let var_name = arg_value .into_immutable_string() .map_err(|typ| self.make_type_mismatch_err::(typ, arg_pos))?; @@ -1081,7 +1144,7 @@ impl Engine { let orig_scope_len = scope.len(); let arg = first_arg.unwrap(); let (arg_value, pos) = - self.get_arg_value(scope, global, state, lib, this_ptr, level, arg, constants)?; + self.get_arg_value(scope, global, state, lib, this_ptr, arg, constants, level)?; let script = &arg_value .into_immutable_string() .map_err(|typ| self.make_type_mismatch_err::(typ, pos))?; @@ -1130,7 +1193,7 @@ impl Engine { .map(|&v| v) .chain(a_expr.iter()) .try_for_each(|expr| { - self.get_arg_value(scope, global, state, lib, this_ptr, level, expr, constants) + self.get_arg_value(scope, global, state, lib, this_ptr, expr, constants, level) .map(|(value, _)| arg_values.push(value.flatten())) })?; args.extend(curry.iter_mut()); @@ -1141,7 +1204,7 @@ impl Engine { return self .exec_fn_call( - global, state, lib, name, hashes, &mut args, is_ref_mut, false, pos, scope, + scope, global, state, lib, name, hashes, &mut args, is_ref_mut, false, pos, level, ) .map(|(v, _)| v); @@ -1162,12 +1225,12 @@ impl Engine { // func(x, ...) -> x.func(...) a_expr.iter().try_for_each(|expr| { - self.get_arg_value(scope, global, state, lib, this_ptr, level, expr, constants) + self.get_arg_value(scope, global, state, lib, this_ptr, expr, constants, level) .map(|(value, _)| arg_values.push(value.flatten())) })?; let (mut target, _pos) = - self.search_namespace(scope, global, state, lib, this_ptr, first_expr)?; + self.search_namespace(scope, global, state, lib, this_ptr, first_expr, level)?; if target.as_ref().is_read_only() { target = target.into_owned(); @@ -1198,7 +1261,7 @@ impl Engine { .chain(a_expr.iter()) .try_for_each(|expr| { self.get_arg_value( - scope, global, state, lib, this_ptr, level, expr, constants, + scope, global, state, lib, this_ptr, expr, constants, level, ) .map(|(value, _)| arg_values.push(value.flatten())) })?; @@ -1208,7 +1271,7 @@ impl Engine { } self.exec_fn_call( - global, state, lib, name, hashes, &mut args, is_ref_mut, false, pos, None, level, + None, global, state, lib, name, hashes, &mut args, is_ref_mut, false, pos, level, ) .map(|(v, _)| v) } @@ -1248,13 +1311,14 @@ impl Engine { arg_values.push(Dynamic::UNIT); args_expr.iter().skip(1).try_for_each(|expr| { - self.get_arg_value(scope, global, state, lib, this_ptr, level, expr, constants) + self.get_arg_value(scope, global, state, lib, this_ptr, expr, constants, level) .map(|(value, _)| arg_values.push(value.flatten())) })?; // Get target reference to first argument + let first_arg = &args_expr[0]; let (target, _pos) = - self.search_scope_only(scope, global, state, lib, this_ptr, &args_expr[0])?; + self.search_scope_only(scope, global, state, lib, this_ptr, first_arg, level)?; #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, _pos)?; @@ -1278,7 +1342,7 @@ impl Engine { } else { // func(..., ...) or func(mod::x, ...) args_expr.iter().try_for_each(|expr| { - self.get_arg_value(scope, global, state, lib, this_ptr, level, expr, constants) + self.get_arg_value(scope, global, state, lib, this_ptr, expr, constants, level) .map(|(value, _)| arg_values.push(value.flatten())) })?; args.extend(arg_values.iter_mut()); @@ -1312,34 +1376,27 @@ impl Engine { } } + let level = level + 1; + match func { #[cfg(not(feature = "no_function"))] Some(f) if f.is_script() => { let fn_def = f.get_script_fn_def().expect("script-defined function"); + let new_scope = &mut Scope::new(); + let mut source = module.id_raw().clone(); + mem::swap(&mut global.source, &mut source); - if fn_def.body.is_empty() { - Ok(Dynamic::UNIT) - } else { - let new_scope = &mut Scope::new(); + let result = self.call_script_fn( + new_scope, global, state, lib, &mut None, fn_def, &mut args, true, pos, level, + ); - let mut source = module.id_raw().clone(); - mem::swap(&mut global.source, &mut source); + global.source = source; - let level = level + 1; - - let result = self.call_script_fn( - new_scope, global, state, lib, &mut None, fn_def, &mut args, pos, true, - level, - ); - - global.source = source; - - result - } + result } Some(f) if f.is_plugin_fn() => { - let context = (self, fn_name, module.id(), &*global, lib, pos).into(); + let context = (self, fn_name, module.id(), &*global, lib, pos, level).into(); let result = f .get_plugin_fn() .expect("plugin function") @@ -1350,7 +1407,7 @@ impl Engine { Some(f) if f.is_native() => { let func = f.get_native_fn().expect("native function"); - let context = (self, fn_name, module.id(), &*global, lib, pos).into(); + let context = (self, fn_name, module.id(), &*global, lib, pos, level).into(); let result = func(context, &mut args); self.check_return_value(result, pos) } diff --git a/src/func/native.rs b/src/func/native.rs index 7938f4cd..380098fd 100644 --- a/src/func/native.rs +++ b/src/func/native.rs @@ -70,6 +70,8 @@ pub struct NativeCallContext<'a> { lib: &'a [&'a Module], /// [Position] of the function call. pos: Position, + /// The current nesting level of function calls. + level: usize, } impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef + 'a + ?Sized> @@ -80,6 +82,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef + 'a + ?Sized> &'a GlobalRuntimeState<'a>, &'a M, Position, + usize, )> for NativeCallContext<'a> { #[inline(always)] @@ -91,6 +94,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef + 'a + ?Sized> &'a GlobalRuntimeState, &'a M, Position, + usize, ), ) -> Self { Self { @@ -100,6 +104,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef + 'a + ?Sized> global: Some(value.3), lib: value.4.as_ref(), pos: value.5, + level: value.6, } } } @@ -116,6 +121,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef + 'a + ?Sized> global: None, lib: value.2.as_ref(), pos: Position::NONE, + level: 0, } } } @@ -141,6 +147,7 @@ impl<'a> NativeCallContext<'a> { global: None, lib, pos: Position::NONE, + level: 0, } } /// _(internals)_ Create a new [`NativeCallContext`]. @@ -158,6 +165,7 @@ impl<'a> NativeCallContext<'a> { global: &'a GlobalRuntimeState, lib: &'a [&Module], pos: Position, + level: usize, ) -> Self { Self { engine, @@ -166,6 +174,7 @@ impl<'a> NativeCallContext<'a> { global: Some(global), lib, pos, + level, } } /// The current [`Engine`]. @@ -186,6 +195,12 @@ impl<'a> NativeCallContext<'a> { pub const fn position(&self) -> Position { self.pos } + /// Current nesting level of function calls. + #[inline(always)] + #[must_use] + pub const fn call_level(&self) -> usize { + self.level + } /// The current source. #[inline(always)] #[must_use] @@ -306,6 +321,7 @@ impl<'a> NativeCallContext<'a> { self.engine() .exec_fn_call( + None, &mut global, &mut state, self.lib, @@ -315,8 +331,7 @@ impl<'a> NativeCallContext<'a> { is_ref_mut, is_method_call, Position::NONE, - None, - 0, + self.level + 1, ) .map(|(r, _)| r) } @@ -429,3 +444,11 @@ pub type OnVarCallback = dyn Fn(&str, usize, &EvalContext) -> RhaiResultOf RhaiResultOf> + Send + Sync; + +/// Callback function for variable definition. +#[cfg(not(feature = "sync"))] +pub type OnDefVarCallback = dyn Fn(&str, bool, usize, bool, &EvalContext) -> RhaiResultOf; +/// Callback function for variable definition. +#[cfg(feature = "sync")] +pub type OnDefVarCallback = + dyn Fn(&str, bool, usize, bool, &EvalContext) -> RhaiResultOf + Send + Sync; diff --git a/src/func/script.rs b/src/func/script.rs index c087053e..7b4236a8 100644 --- a/src/func/script.rs +++ b/src/func/script.rs @@ -4,12 +4,14 @@ use super::call::FnCallArgs; use crate::ast::ScriptFnDef; use crate::eval::{EvalState, GlobalRuntimeState}; -use crate::{Dynamic, Engine, Module, Position, RhaiError, RhaiResult, Scope, StaticVec, ERR}; +use crate::{Dynamic, Engine, Module, Position, RhaiError, RhaiResult, Scope, ERR}; use std::mem; #[cfg(feature = "no_std")] use std::prelude::v1::*; impl Engine { + /// # Main Entry-Point + /// /// Call a script-defined function. /// /// If `rewind_scope` is `false`, arguments are removed from the scope but new variables are not. @@ -29,8 +31,8 @@ impl Engine { this_ptr: &mut Option<&mut Dynamic>, fn_def: &ScriptFnDef, args: &mut FnCallArgs, - pos: Position, rewind_scope: bool, + pos: Position, level: usize, ) -> RhaiResult { #[inline(never)] @@ -41,13 +43,19 @@ impl Engine { err: RhaiError, pos: Position, ) -> RhaiResult { + let _fn_def = fn_def; + + #[cfg(not(feature = "no_module"))] + let source = _fn_def + .environ + .as_ref() + .and_then(|environ| environ.lib.id().map(str::to_string)); + #[cfg(feature = "no_module")] + let source = None; + Err(ERR::ErrorInFunctionCall( name, - fn_def - .lib - .as_ref() - .and_then(|m| m.id().map(str::to_string)) - .unwrap_or_else(|| global.source.to_string()), + source.unwrap_or_else(|| global.source.to_string()), err, pos, ) @@ -59,22 +67,26 @@ impl Engine { #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, pos)?; - if fn_def.body.is_empty() { - return Ok(Dynamic::UNIT); - } - // Check for stack overflow #[cfg(not(feature = "unchecked"))] if level > self.max_call_levels() { return Err(ERR::ErrorStackOverflow(pos).into()); } + #[cfg(feature = "debugging")] + if self.debugger.is_none() && fn_def.body.is_empty() { + return Ok(Dynamic::UNIT); + } + #[cfg(not(feature = "debugging"))] + if fn_def.body.is_empty() { + return Ok(Dynamic::UNIT); + } + let orig_scope_len = scope.len(); #[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 @@ -85,44 +97,58 @@ impl Engine { // 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, - ); + if self.debugger.is_some() { + 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(); - let lib = if let Some(ref fn_lib) = fn_def.lib { - if fn_lib.is_empty() { - lib - } else { - state.push_fn_resolution_cache(); - lib_merged.push(fn_lib.as_ref()); - lib_merged.extend(lib.iter().cloned()); - &lib_merged - } - } else { - lib - }; + #[cfg(not(feature = "no_module"))] + let mut lib_merged = crate::StaticVec::with_capacity(lib.len() + 1); #[cfg(not(feature = "no_module"))] - if let Some(ref modules) = fn_def.global { - for (n, m) in modules.iter().cloned() { + let (lib, constants) = if let Some(crate::ast::EncapsulatedEnviron { + lib: ref fn_lib, + ref imports, + ref constants, + }) = fn_def.environ + { + for (n, m) in imports.iter().cloned() { global.push_import(n, m) } + ( + if fn_lib.is_empty() { + lib + } else { + state.push_fn_resolution_cache(); + lib_merged.push(fn_lib.as_ref()); + lib_merged.extend(lib.iter().cloned()); + &lib_merged + }, + Some(mem::replace(&mut global.constants, constants.clone())), + ) + } else { + (lib, None) + }; + + #[cfg(feature = "debugging")] + { + let node = crate::ast::Stmt::Noop(fn_def.body.position()); + self.run_debugger(scope, global, state, lib, this_ptr, &node, level)?; } // Evaluate the function - let result = self + let mut _result = self .eval_stmt_block( scope, global, @@ -155,6 +181,31 @@ impl Engine { _ => make_error(fn_def.name.to_string(), fn_def, global, err, pos), }); + #[cfg(feature = "debugging")] + { + let trigger = match global.debugger.status { + crate::eval::DebuggerStatus::FunctionExit(n) => n >= level, + crate::eval::DebuggerStatus::Next(_, true) => true, + _ => false, + }; + if trigger { + let node = crate::ast::Stmt::Noop(fn_def.body.end_position().or_else(pos)); + let node = (&node).into(); + let event = match _result { + Ok(ref r) => crate::eval::DebuggerEvent::FunctionExitWithValue(r), + Err(ref err) => crate::eval::DebuggerEvent::FunctionExitWithError(err), + }; + match self.run_debugger_raw(scope, global, state, lib, this_ptr, node, event, level) + { + Ok(_) => (), + Err(err) => _result = Err(err), + } + } + + // Pop the call stack + global.debugger.rewind_call_stack(orig_call_stack_len); + } + // Remove all local variables and imported modules if rewind_scope { scope.rewind(orig_scope_len); @@ -165,15 +216,16 @@ impl Engine { #[cfg(not(feature = "no_module"))] global.truncate_imports(orig_imports_len); + // Restore constants + #[cfg(not(feature = "no_module"))] + if let Some(constants) = constants { + global.constants = constants; + } + // 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 + _result } // Does a script-defined function exist? diff --git a/src/lib.rs b/src/lib.rs index 3e514dd8..ea94ed65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -154,6 +154,8 @@ pub use eval::EvalContext; pub use func::{NativeCallContext, RegisterNativeFunction}; pub use module::{FnNamespace, Module}; pub use tokenizer::Position; +#[cfg(not(feature = "no_std"))] +pub use types::Instant; pub use types::{ Dynamic, EvalAltResult, FnPtr, ImmutableString, LexError, ParseError, ParseErrorType, Scope, }; @@ -164,7 +166,7 @@ pub use types::{ pub mod debugger { #[cfg(not(feature = "no_function"))] pub use super::eval::CallStackFrame; - pub use super::eval::{BreakPoint, Debugger, DebuggerCommand}; + pub use super::eval::{BreakPoint, Debugger, DebuggerCommand, DebuggerEvent}; } /// An identifier in Rhai. [`SmartString`](https://crates.io/crates/smartstring) is used because most @@ -256,9 +258,14 @@ pub use parser::ParseState; pub use ast::{ ASTNode, BinaryExpr, ConditionalStmtBlock, CustomExpr, Expr, FnCallExpr, FnCallHashes, Ident, OpAssignment, OptionFlags, ScriptFnDef, Stmt, StmtBlock, SwitchCases, TryCatchBlock, - AST_OPTION_FLAGS::*, + AST_OPTION_FLAGS, }; +#[cfg(feature = "internals")] +#[cfg(not(feature = "no_module"))] +#[cfg(not(feature = "no_function"))] +pub use ast::EncapsulatedEnviron; + #[cfg(feature = "internals")] #[cfg(not(feature = "no_float"))] pub use ast::FloatWrapper; diff --git a/src/module/mod.rs b/src/module/mod.rs index e842fc1d..d9b16a17 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -1696,33 +1696,21 @@ impl Module { let mut module = Module::new(); // Extra modules left become sub-modules - #[cfg(not(feature = "no_function"))] - let mut func_global = None; + let mut imports = StaticVec::new_const(); 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())); - + imports.push((k.clone(), m.clone())); module.set_sub_module(k.clone(), m.clone()); }); } // Restore global state #[cfg(not(feature = "no_function"))] - { - global.constants = orig_constants; - } + let constants = std::mem::replace(&mut global.constants, orig_constants); global.truncate_imports(orig_imports_len); global.source = orig_source; @@ -1747,12 +1735,9 @@ impl Module { } } - #[cfg(not(feature = "no_function"))] - let func_global = func_global.map(|v| v.into_boxed_slice()); - // Non-private functions defined become module functions #[cfg(not(feature = "no_function"))] - if ast.has_functions() { + { ast.shared_lib() .iter_fn() .filter(|&f| match f.metadata.access { @@ -1761,15 +1746,20 @@ impl Module { }) .filter(|&f| f.func.is_script()) .for_each(|f| { - // Encapsulate AST environment let mut func = f .func .get_script_fn_def() .expect("script-defined function") .as_ref() .clone(); - func.lib = Some(ast.shared_lib().clone()); - func.global = func_global.clone(); + + // Encapsulate AST environment + func.environ = Some(crate::ast::EncapsulatedEnviron { + lib: ast.shared_lib().clone(), + imports: imports.clone().into_boxed_slice(), + constants: constants.clone(), + }); + module.set_script_fn(func); }); } diff --git a/src/optimizer.rs b/src/optimizer.rs index 7e2cb6e7..df9bc26d 100644 --- a/src/optimizer.rs +++ b/src/optimizer.rs @@ -147,6 +147,7 @@ impl<'a> OptimizerState<'a> { false, false, Position::NONE, + 0, ) .ok() .map(|(v, _)| v) @@ -462,13 +463,16 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b Stmt::If(condition, x, _) if x.0.is_empty() && x.1.is_empty() => { state.set_dirty(); - let pos = condition.position(); + let pos = condition.start_position(); let mut expr = mem::take(condition); optimize_expr(&mut expr, state, false); *stmt = if preserve_result { // -> { expr, Noop } - Stmt::Block([Stmt::Expr(expr), Stmt::Noop(pos)].into(), pos) + Stmt::Block( + [Stmt::Expr(expr), Stmt::Noop(pos)].into(), + (pos, Position::NONE), + ) } else { // -> expr Stmt::Expr(expr) @@ -486,7 +490,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b match optimize_stmt_block(mem::take(&mut *x.1), state, preserve_result, true, false) { statements if statements.is_empty() => Stmt::Noop(x.1.position()), - statements => Stmt::Block(statements.into_boxed_slice(), x.1.position()), + statements => Stmt::Block(statements.into_boxed_slice(), x.1.positions()), } } // if true { if_block } else { else_block } -> if_block @@ -496,7 +500,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b match optimize_stmt_block(mem::take(&mut *x.0), state, preserve_result, true, false) { statements if statements.is_empty() => Stmt::Noop(x.0.position()), - statements => Stmt::Block(statements.into_boxed_slice(), x.0.position()), + statements => Stmt::Block(statements.into_boxed_slice(), x.0.positions()), } } // if expr { if_block } else { else_block } @@ -530,11 +534,11 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b mem::take(&mut block.statements), Stmt::Block( def_stmt.into_boxed_slice(), - x.def_case.position().or_else(*pos), + x.def_case.positions_or_else(*pos, Position::NONE), ) .into(), )), - match_expr.position(), + match_expr.start_position(), ); } else { // Promote the matched case @@ -545,7 +549,8 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b true, false, ); - *stmt = Stmt::Block(statements.into_boxed_slice(), block.statements.position()); + *stmt = + Stmt::Block(statements.into_boxed_slice(), block.statements.positions()); } state.set_dirty(); @@ -585,11 +590,11 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b mem::take(&mut block.statements), Stmt::Block( def_stmt.into_boxed_slice(), - x.def_case.position().or_else(*pos), + x.def_case.positions_or_else(*pos, Position::NONE), ) .into(), )), - match_expr.position(), + match_expr.start_position(), ); } else { // Promote the matched case @@ -598,7 +603,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b optimize_stmt_block(statements, state, true, true, false); *stmt = Stmt::Block( statements.into_boxed_slice(), - block.statements.position(), + block.statements.positions(), ); } @@ -646,7 +651,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b 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), + x.def_case.positions_or_else(*pos, Position::NONE), ); } // switch @@ -703,7 +708,8 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b if preserve_result { statements.push(Stmt::Noop(pos)) } - *stmt = Stmt::Block(statements.into_boxed_slice(), pos); + *stmt = + Stmt::Block(statements.into_boxed_slice(), (pos, Position::NONE)); } else { *stmt = Stmt::Noop(pos); }; @@ -720,7 +726,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b *stmt = Stmt::Block( optimize_stmt_block(mem::take(&mut **body), state, false, true, false) .into_boxed_slice(), - body.position(), + body.positions(), ); } // do { block } while|until expr @@ -748,7 +754,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b match block.as_mut_slice() { [] => { state.set_dirty(); - *stmt = Stmt::Noop(*pos); + *stmt = Stmt::Noop(pos.0); } // Only one statement which is not block-dependent - promote [s] if !s.is_block_dependent() => { @@ -765,7 +771,7 @@ fn optimize_stmt(stmt: &mut Stmt, state: &mut OptimizerState, preserve_result: b *stmt = Stmt::Block( optimize_stmt_block(mem::take(&mut *x.try_block), state, false, true, false) .into_boxed_slice(), - x.try_block.position(), + x.try_block.positions(), ); } // try { try_block } catch ( var ) { catch_block } @@ -1072,7 +1078,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, chaining: bool) { if let Some(value) = arg.get_literal_value() { state.set_dirty(); constants.push(value); - *arg = Expr::Stack(constants.len()-1, arg.position()); + *arg = Expr::Stack(constants.len()-1, arg.start_position()); } }); } @@ -1120,7 +1126,7 @@ fn optimize_expr(expr: &mut Expr, state: &mut OptimizerState, chaining: bool) { if let Some(value) = arg.get_literal_value() { state.set_dirty(); x.constants.push(value); - *arg = Expr::Stack(x.constants.len()-1, arg.position()); + *arg = Expr::Stack(x.constants.len()-1, arg.start_position()); } }, @@ -1211,9 +1217,8 @@ pub fn optimize_into_ast( access: fn_def.access, body: crate::ast::StmtBlock::NONE, params: fn_def.params.clone(), - lib: None, #[cfg(not(feature = "no_module"))] - global: None, + environ: None, #[cfg(not(feature = "no_function"))] #[cfg(feature = "metadata")] comments: None, diff --git a/src/packages/debugging.rs b/src/packages/debugging.rs index 397f8b8e..ab24bc3a 100644 --- a/src/packages/debugging.rs +++ b/src/packages/debugging.rs @@ -27,15 +27,23 @@ def_package! { #[export_module] mod debugging_functions { + /// Get an array of object maps containing the function calls stack. + /// + /// If there is no debugging interface registered, an empty array is returned. + /// + /// An array of strings is returned under `no_object`. #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_index"))] - pub fn stack_trace(ctx: NativeCallContext) -> Array { + pub fn back_trace(ctx: NativeCallContext) -> Array { if let Some(global) = ctx.global_runtime_state() { global .debugger .call_stack() .iter() .rev() + .filter(|crate::debugger::CallStackFrame { fn_name, args, .. }| { + fn_name != "back_trace" || !args.is_empty() + }) .map( |frame @ crate::debugger::CallStackFrame { fn_name: _fn_name, diff --git a/src/parser.rs b/src/parser.rs index 21d12147..ae8de862 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -52,11 +52,11 @@ pub struct ParseState<'e> { pub entry_stack_len: usize, /// Tracks a list of external variables (variables that are not explicitly declared in the scope). #[cfg(not(feature = "no_closure"))] - pub external_vars: BTreeMap, + pub external_vars: Vec, /// An indicator that disables variable capturing into externals one single time /// up until the nearest consumed Identifier token. /// If set to false the next call to [`access_var`][ParseState::access_var] will not capture the variable. - /// All consequent calls to [`access_var`][ParseState::access_var] will not be affected + /// All consequent calls to [`access_var`][ParseState::access_var] will not be affected. #[cfg(not(feature = "no_closure"))] pub allow_capture: bool, /// Encapsulates a local stack with imported [module][crate::Module] names. @@ -85,7 +85,7 @@ impl<'e> ParseState<'e> { #[cfg(not(feature = "no_function"))] max_function_expr_depth: NonZeroUsize::new(engine.max_function_expr_depth()), #[cfg(not(feature = "no_closure"))] - external_vars: BTreeMap::new(), + external_vars: Vec::new(), #[cfg(not(feature = "no_closure"))] allow_capture: true, interned_strings: StringsInterner::new(), @@ -128,8 +128,11 @@ impl<'e> ParseState<'e> { #[cfg(not(feature = "no_closure"))] if self.allow_capture { - if index.is_none() && !self.external_vars.contains_key(name) { - self.external_vars.insert(name.into(), _pos); + if index.is_none() && !self.external_vars.iter().any(|v| v.name == name) { + self.external_vars.push(crate::ast::Ident { + name: name.into(), + pos: _pos, + }); } } else { self.allow_capture = true @@ -303,7 +306,7 @@ impl Expr { Err( PERR::MismatchedType("a boolean expression".to_string(), type_name.to_string()) - .into_err(self.position()), + .into_err(self.start_position()), ) } /// Raise an error if the expression can never yield an iterable value. @@ -323,7 +326,7 @@ impl Expr { Err( PERR::MismatchedType("an iterable value".to_string(), type_name.to_string()) - .into_err(self.position()), + .into_err(self.start_position()), ) } } @@ -518,6 +521,7 @@ fn parse_fn_call( namespace, hashes, args, + pos: settings.pos, ..Default::default() } .into_fn_call_expr(settings.pos)); @@ -582,6 +586,7 @@ fn parse_fn_call( namespace, hashes, args, + pos: settings.pos, ..Default::default() } .into_fn_call_expr(settings.pos)); @@ -649,7 +654,7 @@ fn parse_index_chain( return Err(PERR::MalformedIndexExpr( "Only arrays, object maps and strings can be indexed".into(), ) - .into_err(lhs.position())) + .into_err(lhs.start_position())) } Expr::CharConstant(_, _) @@ -660,7 +665,7 @@ fn parse_index_chain( return Err(PERR::MalformedIndexExpr( "Only arrays, object maps and strings can be indexed".into(), ) - .into_err(lhs.position())) + .into_err(lhs.start_position())) } _ => (), @@ -674,7 +679,7 @@ fn parse_index_chain( return Err(PERR::MalformedIndexExpr( "Array or string expects numeric index, not a string".into(), ) - .into_err(idx_expr.position())) + .into_err(idx_expr.start_position())) } #[cfg(not(feature = "no_float"))] @@ -682,7 +687,7 @@ fn parse_index_chain( return Err(PERR::MalformedIndexExpr( "Only arrays, object maps and strings can be indexed".into(), ) - .into_err(lhs.position())) + .into_err(lhs.start_position())) } Expr::CharConstant(_, _) @@ -693,7 +698,7 @@ fn parse_index_chain( return Err(PERR::MalformedIndexExpr( "Only arrays, object maps and strings can be indexed".into(), ) - .into_err(lhs.position())) + .into_err(lhs.start_position())) } _ => (), @@ -705,35 +710,35 @@ fn parse_index_chain( return Err(PERR::MalformedIndexExpr( "Array access expects integer index, not a float".into(), ) - .into_err(x.position())) + .into_err(x.start_position())) } // lhs[char] x @ Expr::CharConstant(_, _) => { return Err(PERR::MalformedIndexExpr( "Array access expects integer index, not a character".into(), ) - .into_err(x.position())) + .into_err(x.start_position())) } // lhs[()] x @ Expr::Unit(_) => { return Err(PERR::MalformedIndexExpr( "Array access expects integer index, not ()".into(), ) - .into_err(x.position())) + .into_err(x.start_position())) } // lhs[??? && ???], lhs[??? || ???] x @ Expr::And(_, _) | x @ Expr::Or(_, _) => { return Err(PERR::MalformedIndexExpr( "Array access expects integer index, not a boolean".into(), ) - .into_err(x.position())) + .into_err(x.start_position())) } // lhs[true], lhs[false] x @ Expr::BoolConstant(_, _) => { return Err(PERR::MalformedIndexExpr( "Array access expects integer index, not a boolean".into(), ) - .into_err(x.position())) + .into_err(x.start_position())) } // All other expressions _ => (), @@ -1045,7 +1050,7 @@ fn parse_switch( let (hash, range) = if let Some(expr) = expr { let value = expr.get_literal_value().ok_or_else(|| { - PERR::ExprExpected("a literal".to_string()).into_err(expr.position()) + PERR::ExprExpected("a literal".to_string()).into_err(expr.start_position()) })?; let guard = value.read_lock::(); @@ -1055,14 +1060,14 @@ fn parse_switch( } else if let Some(range) = value.read_lock::() { (None, Some((*range.start(), *range.end(), true))) } else if value.is::() && !ranges.is_empty() { - return Err(PERR::WrongSwitchIntegerCase.into_err(expr.position())); + return Err(PERR::WrongSwitchIntegerCase.into_err(expr.start_position())); } else { let hasher = &mut get_hasher(); value.hash(hasher); let hash = hasher.finish(); if cases.contains_key(&hash) { - return Err(PERR::DuplicatedSwitchCase.into_err(expr.position())); + return Err(PERR::DuplicatedSwitchCase.into_err(expr.start_position())); } (Some(hash), None) } @@ -1258,12 +1263,14 @@ fn parse_primary( #[cfg(not(feature = "no_closure"))] new_state.external_vars.iter().try_for_each( - |(captured_var, &pos)| -> ParseResult<_> { - let index = state.access_var(captured_var, pos); + |crate::ast::Ident { name, pos }| -> ParseResult<_> { + let index = state.access_var(name, *pos); - if !settings.is_closure && settings.strict_var && index.is_none() { + if settings.strict_var && !settings.is_closure && index.is_none() { // If the parent scope is not inside another capturing closure - Err(PERR::VariableUndefined(captured_var.to_string()).into_err(pos)) + // then we can conclude that the captured variable doesn't exist. + // Under Strict Variables mode, this is not allowed. + Err(PERR::VariableUndefined(name.to_string()).into_err(*pos)) } else { Ok(()) } @@ -1677,6 +1684,7 @@ fn parse_unary( name: state.get_identifier("", "-"), hashes: FnCallHashes::from_native(calc_fn_hash("-", 1)), args, + pos, ..Default::default() } .into_fn_call_expr(pos)) @@ -1703,6 +1711,7 @@ fn parse_unary( name: state.get_identifier("", "+"), hashes: FnCallHashes::from_native(calc_fn_hash("+", 1)), args, + pos, ..Default::default() } .into_fn_call_expr(pos)) @@ -1720,6 +1729,7 @@ fn parse_unary( name: state.get_identifier("", "!"), hashes: FnCallHashes::from_native(calc_fn_hash("!", 1)), args, + pos, ..Default::default() } .into_fn_call_expr(pos)) @@ -1748,7 +1758,7 @@ fn make_assignment_stmt( } 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()), + ref e => Some(e.start_position()), }, Expr::Index(x, term, _) | Expr::Dot(x, term, _) => match x.lhs { Expr::Property(_, _) => unreachable!("unexpected Expr::Property in indexing"), @@ -1757,7 +1767,7 @@ fn make_assignment_stmt( }, Expr::Property(_, _) if parent_is_dot => None, Expr::Property(_, _) => unreachable!("unexpected Expr::Property in indexing"), - e if parent_is_dot => Some(e.position()), + e if parent_is_dot => Some(e.start_position()), _ => None, } } @@ -1767,7 +1777,7 @@ fn make_assignment_stmt( match lhs { // const_expr = rhs ref expr if expr.is_constant() => { - Err(PERR::AssignmentToConstant("".into()).into_err(lhs.position())) + Err(PERR::AssignmentToConstant("".into()).into_err(lhs.start_position())) } // var (non-indexed) = rhs Expr::Variable(None, _, ref x) if x.0.is_none() => Ok(Stmt::Assignment( @@ -1809,10 +1819,8 @@ fn make_assignment_stmt( op_pos, )), // expr[???] = rhs, expr.??? = rhs - ref expr => { - Err(PERR::AssignmentToInvalidLHS("".to_string()) - .into_err(expr.position())) - } + ref expr => Err(PERR::AssignmentToInvalidLHS("".to_string()) + .into_err(expr.start_position())), } } Some(err_pos) => { @@ -1827,7 +1835,7 @@ fn make_assignment_stmt( ) .into_err(op_pos)), // expr = rhs - _ => Err(PERR::AssignmentToInvalidLHS("".to_string()).into_err(lhs.position())), + _ => Err(PERR::AssignmentToInvalidLHS("".to_string()).into_err(lhs.start_position())), } } @@ -1978,7 +1986,7 @@ fn make_dot_expr( Ok(Expr::Dot(BinaryExpr { lhs, rhs }.into(), false, op_pos)) } // lhs.rhs - (_, rhs) => Err(PERR::PropertyExpected.into_err(rhs.position())), + (_, rhs) => Err(PERR::PropertyExpected.into_err(rhs.start_position())), } } @@ -2060,6 +2068,7 @@ fn parse_binary_op( let op_base = FnCallExpr { name: state.get_identifier("", op), hashes: FnCallHashes::from_native(hash), + pos, ..Default::default() }; @@ -2077,7 +2086,10 @@ fn parse_binary_op( | Token::LessThan | Token::LessThanEqualsTo | Token::GreaterThan - | Token::GreaterThanEqualsTo => FnCallExpr { args, ..op_base }.into_fn_call_expr(pos), + | Token::GreaterThanEqualsTo => { + let pos = args[0].start_position(); + FnCallExpr { args, ..op_base }.into_fn_call_expr(pos) + } Token::Or => { let rhs = args.pop().unwrap(); @@ -2106,6 +2118,7 @@ fn parse_binary_op( Token::In => { // Swap the arguments let current_lhs = args.remove(0); + let pos = current_lhs.start_position(); args.push(current_lhs); args.shrink_to_fit(); @@ -2127,6 +2140,7 @@ fn parse_binary_op( .map_or(false, Option::is_some) => { let hash = calc_fn_hash(&s, 2); + let pos = args[0].start_position(); FnCallExpr { hashes: if is_valid_function_name(&s) { @@ -2140,7 +2154,10 @@ fn parse_binary_op( .into_fn_call_expr(pos) } - _ => FnCallExpr { args, ..op_base }.into_fn_call_expr(pos), + _ => { + let pos = args[0].start_position(); + FnCallExpr { args, ..op_base }.into_fn_call_expr(pos) + } }; } } @@ -2574,6 +2591,12 @@ fn parse_let( // let name ... let (name, pos) = parse_var_name(input)?; + if !settings.default_options.allow_shadowing + && state.stack.iter().any(|(v, _)| v == name.as_ref()) + { + return Err(PERR::VariableExists(name.to_string()).into_err(pos)); + } + let name = state.get_identifier("", name); let var_def = Ident { name: name.clone(), @@ -2729,13 +2752,10 @@ fn parse_block( #[cfg(not(feature = "no_module"))] let orig_imports_len = state.imports.len(); - loop { + let end_pos = loop { // Terminated? match input.peek().expect(NEVER_ENDS) { - (Token::RightBrace, _) => { - eat_token(input, Token::RightBrace); - break; - } + (Token::RightBrace, _) => break eat_token(input, Token::RightBrace), (Token::EOF, pos) => { return Err(PERR::MissingToken( Token::RightBrace.into(), @@ -2762,10 +2782,7 @@ fn parse_block( match input.peek().expect(NEVER_ENDS) { // { ... stmt } - (Token::RightBrace, _) => { - eat_token(input, Token::RightBrace); - break; - } + (Token::RightBrace, _) => break eat_token(input, Token::RightBrace), // { ... stmt; (Token::SemiColon, _) if need_semicolon => { eat_token(input, Token::SemiColon); @@ -2788,7 +2805,7 @@ fn parse_block( .into_err(*pos)); } } - } + }; state.stack.truncate(state.entry_stack_len); state.entry_stack_len = prev_entry_stack_len; @@ -2796,7 +2813,10 @@ fn parse_block( #[cfg(not(feature = "no_module"))] state.imports.truncate(orig_imports_len); - Ok(Stmt::Block(statements.into_boxed_slice(), settings.pos)) + Ok(Stmt::Block( + statements.into_boxed_slice(), + (settings.pos, end_pos), + )) } /// Parse an expression as a statement. @@ -2959,25 +2979,25 @@ fn parse_stmt( Token::If => parse_if(input, state, lib, settings.level_up()), Token::Switch => parse_switch(input, state, lib, settings.level_up()), - Token::While | Token::Loop if settings.default_options.allow_loop => { + Token::While | Token::Loop if settings.default_options.allow_looping => { parse_while_loop(input, state, lib, settings.level_up()) } - Token::Do if settings.default_options.allow_loop => { + Token::Do if settings.default_options.allow_looping => { parse_do(input, state, lib, settings.level_up()) } - Token::For if settings.default_options.allow_loop => { + Token::For if settings.default_options.allow_looping => { parse_for(input, state, lib, settings.level_up()) } - Token::Continue if settings.default_options.allow_loop && settings.is_breakable => { + Token::Continue if settings.default_options.allow_looping && settings.is_breakable => { let pos = eat_token(input, Token::Continue); Ok(Stmt::BreakLoop(AST_OPTION_NONE, pos)) } - Token::Break if settings.default_options.allow_loop && settings.is_breakable => { + Token::Break if settings.default_options.allow_looping && settings.is_breakable => { let pos = eat_token(input, Token::Break); Ok(Stmt::BreakLoop(AST_OPTION_BREAK, pos)) } - Token::Continue | Token::Break if settings.default_options.allow_loop => { + Token::Continue | Token::Break if settings.default_options.allow_looping => { Err(PERR::LoopBreak.into_err(token_pos)) } @@ -3185,9 +3205,8 @@ fn parse_fn( access, params, body, - lib: None, #[cfg(not(feature = "no_module"))] - global: None, + environ: None, #[cfg(not(feature = "no_function"))] #[cfg(feature = "metadata")] comments: if comments.is_empty() { @@ -3240,6 +3259,7 @@ fn make_curry_from_externals( num_externals + 1, )), args, + pos, ..Default::default() } .into_fn_call_expr(pos); @@ -3249,7 +3269,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(crate::ast::StmtBlock::new(statements, pos).into()) + Expr::Stmt(crate::ast::StmtBlock::new(statements, pos, Position::NONE).into()) } /// Parse an anonymous function definition. @@ -3312,20 +3332,21 @@ fn parse_anon_fn( // External variables may need to be processed in a consistent order, // so extract them into a list. #[cfg(not(feature = "no_closure"))] - let externals: StaticVec = state - .external_vars - .iter() - .map(|(name, _)| name.clone()) - .collect(); + let (mut params, externals) = { + let externals: StaticVec = state + .external_vars + .iter() + .map(|crate::ast::Ident { name, .. }| name.clone()) + .collect(); - #[cfg(not(feature = "no_closure"))] - let mut params = StaticVec::with_capacity(params_list.len() + externals.len()); + let mut params = StaticVec::with_capacity(params_list.len() + externals.len()); + params.extend(externals.iter().cloned()); + + (params, externals) + }; #[cfg(feature = "no_closure")] let mut params = StaticVec::with_capacity(params_list.len()); - #[cfg(not(feature = "no_closure"))] - params.extend(externals.iter().cloned()); - params.append(&mut params_list); // Create unique function name by hashing the script body plus the parameters. @@ -3341,9 +3362,8 @@ fn parse_anon_fn( access: crate::FnAccess::Public, params, body: body.into(), - lib: None, #[cfg(not(feature = "no_module"))] - global: None, + environ: None, #[cfg(not(feature = "no_function"))] #[cfg(feature = "metadata")] comments: None, diff --git a/src/tests.rs b/src/tests.rs index f4d32675..e96d8dd5 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -37,9 +37,9 @@ fn check_struct_sizes() { assert_eq!( size_of::(), if cfg!(feature = "no_position") { - 64 - } else { 72 + } else { + 80 } ); } diff --git a/src/types/dynamic.rs b/src/types/dynamic.rs index 0b8fdf80..48d0d124 100644 --- a/src/types/dynamic.rs +++ b/src/types/dynamic.rs @@ -16,11 +16,11 @@ use std::{ #[cfg(not(feature = "no_std"))] #[cfg(not(target_family = "wasm"))] -use std::time::Instant; +pub use std::time::Instant; #[cfg(not(feature = "no_std"))] #[cfg(target_family = "wasm")] -use instant::Instant; +pub use instant::Instant; /// The message: data type was checked const CHECKED: &str = "data type was checked"; @@ -543,74 +543,6 @@ impl Hash for Dynamic { } } -/// Map the name of a standard type into a friendly form. -#[inline] -#[must_use] -pub(crate) fn map_std_type_name(name: &str, shorthands: bool) -> &str { - let name = name.trim(); - - if name.starts_with("rhai::") { - return map_std_type_name(&name[6..], shorthands); - } - - if name == type_name::() { - return if shorthands { "string" } else { "String" }; - } - if name == type_name::() { - return if shorthands { - "string" - } else { - "ImmutableString" - }; - } - if name == type_name::<&str>() { - return if shorthands { "string" } else { "&str" }; - } - #[cfg(feature = "decimal")] - if name == type_name::() { - return if shorthands { "decimal" } else { "Decimal" }; - } - if name == type_name::() { - return if shorthands { "Fn" } else { "FnPtr" }; - } - #[cfg(not(feature = "no_index"))] - if name == type_name::() { - return if shorthands { "array" } else { "Array" }; - } - #[cfg(not(feature = "no_index"))] - if name == type_name::() { - return if shorthands { "blob" } else { "Blob" }; - } - #[cfg(not(feature = "no_object"))] - if name == type_name::() { - return if shorthands { "map" } else { "Map" }; - } - #[cfg(not(feature = "no_std"))] - if name == type_name::() { - return if shorthands { "timestamp" } else { "Instant" }; - } - if name == type_name::() || name == "ExclusiveRange" { - return if shorthands { - "range" - } else if cfg!(feature = "only_i32") { - "Range" - } else { - "Range" - }; - } - if name == type_name::() || name == "InclusiveRange" { - return if shorthands { - "range=" - } else if cfg!(feature = "only_i32") { - "RangeInclusive" - } else { - "RangeInclusive" - }; - } - - name -} - impl fmt::Display for Dynamic { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { diff --git a/src/types/error.rs b/src/types/error.rs index da507a9b..60fc5d41 100644 --- a/src/types/error.rs +++ b/src/types/error.rs @@ -30,6 +30,8 @@ pub enum EvalAltResult { /// Syntax error. ErrorParsing(ParseErrorType, Position), + /// Shadowing of an existing variable disallowed. Wrapped value is the variable name. + ErrorVariableExists(String, Position), /// Usage of an unknown variable. Wrapped value is the variable name. ErrorVariableNotFound(String, Position), /// Call to an unknown function. Wrapped value is the function signature. @@ -139,8 +141,9 @@ impl fmt::Display for EvalAltResult { } Self::ErrorInModule(s, err, _) => write!(f, "Error in module {}: {}", s, err)?, - Self::ErrorFunctionNotFound(s, _) => write!(f, "Function not found: {}", s)?, + Self::ErrorVariableExists(s, _) => write!(f, "Variable is already defined: {}", s)?, Self::ErrorVariableNotFound(s, _) => write!(f, "Variable not found: {}", s)?, + Self::ErrorFunctionNotFound(s, _) => write!(f, "Function not found: {}", s)?, Self::ErrorModuleNotFound(s, _) => write!(f, "Module not found: {}", s)?, Self::ErrorDataRace(s, _) => { write!(f, "Data race detected when accessing variable: {}", s)? @@ -149,7 +152,7 @@ impl fmt::Display for EvalAltResult { "" => f.write_str("Malformed dot expression"), s => f.write_str(s), }?, - Self::ErrorIndexingType(s, _) => write!(f, "Indexer not registered for {}", s)?, + Self::ErrorIndexingType(s, _) => write!(f, "Indexer not registered: {}", s)?, Self::ErrorUnboundThis(_) => f.write_str("'this' is not bound")?, Self::ErrorFor(_) => f.write_str("For loop expects a type that is iterable")?, Self::ErrorTooManyOperations(_) => f.write_str("Too many operations")?, @@ -166,7 +169,7 @@ impl fmt::Display for EvalAltResult { } Self::ErrorRuntime(d, _) => write!(f, "Runtime error: {}", d)?, - Self::ErrorAssignmentToConstant(s, _) => write!(f, "Cannot modify constant {}", s)?, + Self::ErrorAssignmentToConstant(s, _) => write!(f, "Cannot modify constant: {}", s)?, Self::ErrorMismatchOutputType(s, r, _) => match (r.as_str(), s.as_str()) { ("", s) => write!(f, "Output type is incorrect, expecting {}", s), (r, "") => write!(f, "Output type is incorrect: {}", r), @@ -274,6 +277,7 @@ impl EvalAltResult { | Self::ErrorBitFieldBounds(_, _, _) | Self::ErrorIndexingType(_, _) | Self::ErrorFor(_) + | Self::ErrorVariableExists(_, _) | Self::ErrorVariableNotFound(_, _) | Self::ErrorModuleNotFound(_, _) | Self::ErrorDataRace(_, _) @@ -364,7 +368,8 @@ impl EvalAltResult { Self::ErrorIndexingType(t, _) => { map.insert("type".into(), t.into()); } - Self::ErrorVariableNotFound(v, _) + Self::ErrorVariableExists(v, _) + | Self::ErrorVariableNotFound(v, _) | Self::ErrorDataRace(v, _) | Self::ErrorAssignmentToConstant(v, _) => { map.insert("variable".into(), v.into()); @@ -415,6 +420,7 @@ impl EvalAltResult { | Self::ErrorBitFieldBounds(_, _, pos) | Self::ErrorIndexingType(_, pos) | Self::ErrorFor(pos) + | Self::ErrorVariableExists(_, pos) | Self::ErrorVariableNotFound(_, pos) | Self::ErrorModuleNotFound(_, pos) | Self::ErrorDataRace(_, pos) @@ -463,6 +469,7 @@ impl EvalAltResult { | Self::ErrorBitFieldBounds(_, _, pos) | Self::ErrorIndexingType(_, pos) | Self::ErrorFor(pos) + | Self::ErrorVariableExists(_, pos) | Self::ErrorVariableNotFound(_, pos) | Self::ErrorModuleNotFound(_, pos) | Self::ErrorDataRace(_, pos) diff --git a/src/types/mod.rs b/src/types/mod.rs index 1012f8b1..66da654c 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -9,6 +9,8 @@ pub mod parse_error; pub mod scope; pub use dynamic::Dynamic; +#[cfg(not(feature = "no_std"))] +pub use dynamic::Instant; pub use error::EvalAltResult; pub use fn_ptr::FnPtr; pub use immutable_string::ImmutableString; diff --git a/src/types/parse_error.rs b/src/types/parse_error.rs index 50ab93da..d416043b 100644 --- a/src/types/parse_error.rs +++ b/src/types/parse_error.rs @@ -167,6 +167,10 @@ pub enum ParseErrorType { /// Assignment to an inappropriate LHS (left-hand-side) expression. /// Wrapped value is the error message (if any). AssignmentToInvalidLHS(String), + /// A variable is already defined. + /// + /// Only appears when variables shadowing is disabled. + VariableExists(String), /// A variable is not found. /// /// Only appears when strict variables mode is enabled. @@ -241,6 +245,7 @@ impl fmt::Display for ParseErrorType { Self::DuplicatedSwitchCase => f.write_str("Duplicated switch case"), Self::DuplicatedVariable(s) => write!(f, "Duplicated variable name: {}", s), + Self::VariableExists(s) => write!(f, "Variable already defined: {}", s), Self::VariableUndefined(s) => write!(f, "Undefined variable: {}", s), Self::ModuleUndefined(s) => write!(f, "Undefined module: {}", s), diff --git a/tests/debugging.rs b/tests/debugging.rs index a329a533..aa797624 100644 --- a/tests/debugging.rs +++ b/tests/debugging.rs @@ -9,7 +9,12 @@ use rhai::Map; #[test] fn test_debugging() -> Result<(), Box> { - let engine = Engine::new(); + let mut engine = Engine::new(); + + engine.register_debugger( + || Dynamic::UNIT, + |_, _, _, _, _| Ok(rhai::debugger::DebuggerCommand::Continue), + ); #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_index"))] @@ -18,7 +23,7 @@ fn test_debugging() -> Result<(), Box> { " fn foo(x) { if x >= 5 { - stack_trace() + back_trace() } else { foo(x+1) } @@ -30,7 +35,7 @@ fn test_debugging() -> Result<(), Box> { assert_eq!(r.len(), 6); - assert_eq!(engine.eval::("len(stack_trace())")?, 0); + assert_eq!(engine.eval::("len(back_trace())")?, 0); } Ok(()) @@ -41,7 +46,7 @@ fn test_debugging() -> Result<(), Box> { fn test_debugger_state() -> Result<(), Box> { let mut engine = Engine::new(); - engine.on_debugger( + engine.register_debugger( || { // Say, use an object map for the debugger state let mut state = Map::new(); @@ -50,7 +55,7 @@ fn test_debugger_state() -> Result<(), Box> { state.insert("foo".into(), false.into()); Dynamic::from_map(state) }, - |context, _, _, _| { + |context, _, _, _, _| { // Get global runtime state let global = context.global_runtime_state_mut(); diff --git a/tests/modules.rs b/tests/modules.rs index 9b3e62d4..db0038e7 100644 --- a/tests/modules.rs +++ b/tests/modules.rs @@ -514,3 +514,47 @@ fn test_module_file() -> Result<(), Box> { Module::eval_ast_as_new(Scope::new(), &ast, &engine)?; Ok(()) } + +#[cfg(not(feature = "no_function"))] +#[test] +fn test_module_environ() -> Result<(), Box> { + let mut engine = Engine::new(); + + let ast = engine.compile( + r#" + const SECRET = 42; + + fn foo(x) { + print(global::SECRET); + global::SECRET + x + } + "#, + )?; + + let mut m = Module::eval_ast_as_new(Scope::new(), &ast, &engine)?; + + m.set_id("test"); + m.build_index(); + + engine.register_static_module("test", m.into()); + + assert_eq!( + engine.eval::( + r#" + const SECRET = "hello"; + + fn foo(x) { + print(global::SECRET); + global::SECRET + x + } + + let t = test::foo(0); + + foo(t) + "# + )?, + "hello42" + ); + + Ok(()) +} diff --git a/tests/optimizer.rs b/tests/optimizer.rs index b6d6a289..af9754f4 100644 --- a/tests/optimizer.rs +++ b/tests/optimizer.rs @@ -78,33 +78,24 @@ fn test_optimizer_parse() -> Result<(), Box> { let ast = engine.compile("{ const DECISION = false; if DECISION { 42 } else { 123 } }")?; - assert_eq!( - format!("{:?}", ast), - "AST { source: \"\", body: Block[Expr(123 @ 1:53)], functions: Module, resolver: None }" - ); + assert_eq!(format!("{:?}", ast), "AST { body: [Expr(123 @ 1:53)] }"); let ast = engine.compile("const DECISION = false; if DECISION { 42 } else { 123 }")?; assert_eq!( format!("{:?}", ast), - r#"AST { source: "", body: Block[Var(false @ 1:18, "DECISION" @ 1:7, (Constant), 1:1), Expr(123 @ 1:51)], functions: Module, resolver: None }"# + r#"AST { body: [Var(false @ 1:18, "DECISION" @ 1:7, (Constant), 1:1), Expr(123 @ 1:51)] }"# ); let ast = engine.compile("if 1 == 2 { 42 }")?; - assert_eq!( - format!("{:?}", ast), - "AST { source: \"\", body: Block[], functions: Module, resolver: None }" - ); + assert_eq!(format!("{:?}", ast), "AST { body: [] }"); engine.set_optimization_level(OptimizationLevel::Full); let ast = engine.compile("abs(-42)")?; - assert_eq!( - format!("{:?}", ast), - "AST { source: \"\", body: Block[Expr(42 @ 1:1)], functions: Module, resolver: None }" - ); + assert_eq!(format!("{:?}", ast), "AST { body: [Expr(42 @ 1:1)] }"); Ok(()) } diff --git a/tests/options.rs b/tests/options.rs index 336a2856..d7e45780 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -1,4 +1,4 @@ -use rhai::{Engine, EvalAltResult}; +use rhai::{Engine, EvalAltResult, Scope, INT}; #[test] fn test_options_allow() -> Result<(), Box> { @@ -31,6 +31,20 @@ fn test_options_allow() -> Result<(), Box> { assert!(engine.compile("while x > y { foo(z); }").is_err()); + engine.compile("let x = 42; let x = 123;")?; + + engine.set_allow_shadowing(false); + + assert!(engine.compile("let x = 42; let x = 123;").is_err()); + assert!(engine.compile("const x = 42; let x = 123;").is_err()); + assert!(engine.compile("let x = 42; const x = 123;").is_err()); + assert!(engine.compile("const x = 42; const x = 123;").is_err()); + + let mut scope = Scope::new(); + scope.push("x", 42 as INT); + + assert!(engine.run_with_scope(&mut scope, "let x = 42;").is_err()); + Ok(()) } diff --git a/tests/var_scope.rs b/tests/var_scope.rs index 8efbc5f4..b8d4824d 100644 --- a/tests/var_scope.rs +++ b/tests/var_scope.rs @@ -120,3 +120,26 @@ fn test_var_resolver() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_var_def_filter() -> Result<(), Box> { + let mut engine = Engine::new(); + + engine.on_def_var(|name, _, scope_level, _, _| match (name, scope_level) { + ("x", 0 | 1) => Ok(false), + _ => Ok(true), + }); + + assert_eq!( + engine.eval::("let y = 42; let y = 123; let z = y + 1; z")?, + 124 + ); + + assert!(engine.run("let x = 42;").is_err()); + assert!(engine.run("const x = 42;").is_err()); + assert!(engine.run("let y = 42; { let x = y + 1; }").is_err()); + assert!(engine.run("let y = 42; { let x = y + 1; }").is_err()); + engine.run("let y = 42; { let z = y + 1; { let x = z + 1; } }")?; + + Ok(()) +}