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(())
+}