Merge pull request #518 from schungx/master

Add rustyline to rhai-repl and variable definition filter.
This commit is contained in:
Stephen Chung 2022-02-04 23:21:19 +08:00 committed by GitHub
commit c7888ba1f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1577 additions and 790 deletions

View File

@ -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:

View File

@ -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 <code>\`</code>) 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

View File

@ -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"]

View File

@ -190,8 +190,8 @@ impl Engine {
&mut this_ptr,
fn_def,
&mut args,
Position::NONE,
rewind_scope,
Position::NONE,
0,
);

View File

@ -15,12 +15,12 @@ impl Engine {
/// > `Fn(name: &str, index: usize, context: &EvalContext) -> Result<Option<Dynamic>, Box<EvalAltResult>>`
///
/// 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<bool, Box<EvalAltResult>>`
///
/// 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<rhai::EvalAltResult>> {
/// 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::<i64>("let MYSTIC_NUMBER = 42;")?;
///
/// // The following will cause an error:
/// engine.eval::<i64>("const MYSTIC_NUMBER = 42;")?;
///
/// # Ok(())
/// # }
/// ```
#[inline(always)]
pub fn on_def_var(
&mut self,
callback: impl Fn(&str, bool, usize, bool, &EvalContext) -> RhaiResultOf<bool>
+ 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,

View File

@ -1,5 +1,7 @@
//! Module defining the public API of the Rhai engine.
pub mod type_names;
pub mod eval;
pub mod run;

View File

@ -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;
}
}

131
src/api/type_names.rs Normal file
View File

@ -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::<String>() {
return if shorthands { "string" } else { "String" };
}
if name == type_name::<ImmutableString>() || 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::<rust_decimal::Decimal>() {
return if shorthands { "decimal" } else { "Decimal" };
}
if name == type_name::<FnPtr>() || name == "FnPtr" {
return if shorthands { "Fn" } else { "FnPtr" };
}
#[cfg(not(feature = "no_index"))]
if name == type_name::<crate::Array>() || name == "Array" {
return if shorthands { "array" } else { "Array" };
}
#[cfg(not(feature = "no_index"))]
if name == type_name::<crate::Blob>() || name == "Blob" {
return if shorthands { "blob" } else { "Blob" };
}
#[cfg(not(feature = "no_object"))]
if name == type_name::<crate::Map>() || name == "Map" {
return if shorthands { "map" } else { "Map" };
}
#[cfg(not(feature = "no_std"))]
if name == type_name::<crate::Instant>() || name == "Instant" {
return if shorthands { "timestamp" } else { "Instant" };
}
if name == type_name::<ExclusiveRange>() || name == "ExclusiveRange" {
return if shorthands {
"range"
} else if cfg!(feature = "only_i32") {
"Range<i32>"
} else {
"Range<i64>"
};
}
if name == type_name::<InclusiveRange>() || name == "InclusiveRange" {
return if shorthands {
"range="
} else if cfg!(feature = "only_i32") {
"RangeInclusive<i32>"
} else {
"RangeInclusive<i64>"
};
}
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::<crate::INT>(),
#[cfg(not(feature = "no_float"))]
"FLOAT" => return type_name::<crate::FLOAT>(),
_ => map_std_type_name(name, false),
})
.into()
}
/// Make a `Box<`[`EvalAltResult<ErrorMismatchDataType>`][ERR::ErrorMismatchDataType]`>`.
#[inline(never)]
#[must_use]
pub(crate) fn make_type_mismatch_err<T>(&self, typ: &str, pos: Position) -> RhaiError {
ERR::ErrorMismatchDataType(self.map_type_name(type_name::<T>()).into(), typ.into(), pos)
.into()
}
}

View File

@ -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<crate::Module>,
lib: crate::Shared<crate::Module>,
/// Embedded module resolver, if any.
#[cfg(not(feature = "no_module"))]
resolver: Option<crate::Shared<crate::module::resolvers::StaticModuleResolver>>,
@ -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<Identifier>) -> &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<crate::Module> {
&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<crate::Module> {
&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<Item = &super::ScriptFnDef> {
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<Item = &super::ScriptFnDef> {
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<Item = super::ScriptFnMetadata> + '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<Item = (&str, bool, Dynamic)> {
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
}
}

View File

@ -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<Dynamic>,
/// 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

View File

@ -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};

View File

@ -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<crate::Module>,
/// Imported [modules][crate::Module].
pub imports: Box<[(Identifier, crate::Shared<crate::Module>)]>,
/// Globally-defined constants.
pub constants: Option<crate::eval::GlobalConstants>,
}
/// _(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<Shared<Module>>,
/// 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<Box<[(Identifier, Shared<Module>)]>>,
#[cfg(not(feature = "no_function"))]
pub environ: Option<EncapsulatedEnviron>,
/// Function name.
pub name: Identifier,
/// Function access mode.

View File

@ -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<Stmt>, Position);
pub struct StmtBlock(StaticVec<Stmt>, (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<Item = Stmt>, pos: Position) -> Self {
pub fn new(
statements: impl IntoIterator<Item = Stmt>,
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<Item = &Stmt> {
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<Stmt> 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<FnCallExpr>, Position),
/// `{` stmt`;` ... `}`
Block(Box<[Stmt]>, Position),
Block(Box<[Stmt]>, (Position, Position)),
/// `try` `{` stmt; ... `}` `catch` `(` var `)` `{` stmt; ... `}`
TryCatch(Box<TryCatchBlock>, 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)

View File

@ -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
```

View File

@ -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<String>,
) {
let current_source = &mut *context
.global_runtime_state_mut()
.debugger
.state_mut()
.write_lock::<ImmutableString>()
.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!("help, h => print this help");
println!("quit, q, exit, kill => quit");
println!("scope => print the scope");
println!("print => print all variables de-duplicated");
println!("print <variable> => print the current value of a variable");
println!("print, p => print all variables de-duplicated");
println!("print/p <variable> => 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 <bp#> => enable a break-point");
println!("disable <bp#> => disable a break-point");
println!("delete <bp#> => delete a break-point");
println!("clear => delete all break-points");
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 <bp#> => enable a break-point");
println!("disable/dis <bp#> => disable a break-point");
println!("delete, d => delete all break-points");
println!("delete/d <bp#> => 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 <line#> => set a new break-point at a line number");
println!("break/b <line#> => set a new break-point at a line number");
#[cfg(not(feature = "no_object"))]
println!("break .<prop> => set a new break-point for a property access");
println!("break <func> => set a new break-point for a function call");
println!("break/b .<prop> => set a new break-point for a property access");
println!("break/b <func> => set a new break-point for a function call");
println!(
"break <func> <#args> => set a new break-point for a function call with #args arguments"
"break/b <func> <#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!("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 => go to the next statement, skipping over functions");
println!("continue => continue normal execution");
println!("next, n, <Enter> => 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()
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)
}
}
}
DebuggerEvent::FunctionExitWithValue(r) => {
println!(
"! Return from function call '{}' => {}",
context
.global_runtime_state()
.debugger
.state_mut()
.write_lock::<ImmutableString>()
.unwrap();
let src = source.unwrap_or("");
// Check source
if src != current_source {
println!(">>> Source => {}", source.unwrap_or("main script"));
*current_source = src.into();
.call_stack()
.last()
.unwrap()
.fn_name,
r
)
}
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::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::<Vec<_>>()
.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::<Dynamic>(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::<usize>() {
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::<usize>() {
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::<usize>() {
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::<usize>() {
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::<usize>().is_ok() => {
["break" | "b", param] if param.parse::<usize>().is_ok() => {
let n = param.parse::<usize>().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());
}

View File

@ -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!("!<text> => repeat the last history line starting with some text");
println!("!?<text> => 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,6 +326,20 @@ fn main() {
print_help();
'main_loop: loop {
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();
loop {
@ -335,8 +358,11 @@ fn main() {
}
Ok(line) => {
input += line.trim_end();
if !input.is_empty() {
rl.add_history_entry(input.clone());
if !input.is_empty() && !input.starts_with('!') && input.trim() != "history"
{
if rl.add_history_entry(input.clone()) {
history_offset += 1;
}
}
break;
}
@ -349,6 +375,7 @@ fn main() {
}
}
}
}
let script = input.trim();
@ -370,10 +397,10 @@ fn main() {
"history" => {
for (i, h) in rl.history().iter().enumerate() {
match &h.split('\n').collect::<Vec<_>>()[..] {
[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::<usize>() {
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;
}
_ => (),
}

View File

@ -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<Identifier, Option<Precedence>>,
/// Custom syntax.
pub(crate) custom_syntax: BTreeMap<Identifier, Box<CustomSyntax>>,
/// Callback closure for filtering variable definition.
pub(crate) def_var_filter: Option<Box<OnDefVarCallback>>,
/// Callback closure for resolving variable access.
pub(crate) resolve_var: Option<Box<OnVarCallback>>,
/// 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::<crate::INT>(),
#[cfg(not(feature = "no_float"))]
"FLOAT" => return type_name::<crate::FLOAT>(),
_ => map_std_type_name(name, false),
})
.into()
}
/// Make a `Box<`[`EvalAltResult<ErrorMismatchDataType>`][ERR::ErrorMismatchDataType]`>`.
#[inline]
#[must_use]
pub(crate) fn make_type_mismatch_err<T>(&self, typ: &str, pos: Position) -> RhaiError {
ERR::ErrorMismatchDataType(self.map_type_name(type_name::<T>()).into(), typ.into(), pos)
.into()
}
}

View File

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

View File

@ -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<DebuggerCommand>;
pub type OnDebuggerCallback = dyn Fn(
&mut EvalContext,
DebuggerEvent,
ASTNode,
Option<&str>,
Position,
) -> RhaiResultOf<DebuggerCommand>;
/// Callback function for debugging.
#[cfg(feature = "sync")]
pub type OnDebuggerCallback = dyn Fn(&mut EvalContext, ASTNode, Option<&str>, Position) -> RhaiResultOf<DebuggerCommand>
pub type OnDebuggerCallback = dyn Fn(
&mut EvalContext,
DebuggerEvent,
ASTNode,
Option<&str>,
Position,
) -> RhaiResultOf<DebuggerCommand>
+ 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<BreakPoint>,
/// The current function call stack.
#[cfg(not(feature = "no_function"))]
call_stack: Vec<CallStackFrame>,
}
@ -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
/// 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<DebuggerStatus> {
if filter(&self.status) {
Some(mem::replace(&mut self.status, DebuggerStatus::CONTINUE))
} else {
None
}
/// 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`].
/// Override the status of this [`Debugger`] if it is [`Some`] the current status is
/// [`CONTINUE`][DebuggerStatus::CONTINUE].
#[inline(always)]
pub fn reset_status(&mut self, status: Option<DebuggerCommand>) {
pub(crate) fn reset_status(&mut self, status: Option<DebuggerStatus>) {
if self.status == DebuggerStatus::CONTINUE {
if let Some(cmd) = status {
self.status = cmd;
}
}
/// Does a particular [`AST` Node][ASTNode] trigger a break-point?
}
/// 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<usize> {
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<ASTNode<'a>>,
level: usize,
) -> RhaiResultOf<()> {
if self.debugger.is_some() {
if let Some(cmd) =
self.run_debugger_with_reset(scope, global, state, lib, this_ptr, node, level)?
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,8 +436,31 @@ impl Engine {
this_ptr: &mut Option<&mut Dynamic>,
node: impl Into<ASTNode<'a>>,
level: usize,
) -> RhaiResultOf<Option<DebuggerCommand>> {
if let Some((_, ref on_debugger)) = self.debugger {
) -> RhaiResultOf<Option<DebuggerStatus>> {
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<ASTNode<'a>>,
level: usize,
) -> RhaiResultOf<Option<DebuggerStatus>> {
let node = node.into();
// Skip transitive nodes
@ -402,15 +470,44 @@ impl Engine {
}
let stop = match global.debugger.status {
DebuggerCommand::Continue => false,
DebuggerCommand::Next => matches!(node, ASTNode::Stmt(_)),
DebuggerCommand::StepInto | DebuggerCommand::StepOver => true,
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);
}
};
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<Option<DebuggerStatus>, Box<crate::EvalAltResult>> {
let source = global.source.clone();
let source = if source.is_empty() {
None
@ -428,24 +525,36 @@ impl Engine {
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 {

View File

@ -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),

View File

@ -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<crate::Locked<std::collections::BTreeMap<Identifier, crate::Dynamic>>>;
/// _(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<crate::Locked<std::collections::BTreeMap<Identifier, crate::Dynamic>>>,
>,
pub(crate) constants: Option<GlobalConstants>,
/// Debugging interface.
#[cfg(feature = "debugging")]
pub debugger: super::Debugger,

View File

@ -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};

View File

@ -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,6 +817,36 @@ impl Engine {
};
let export = options.contains(AST_OPTION_EXPORTED);
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,
};
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),
}
} else {
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);
@ -822,9 +860,9 @@ impl Engine {
&& 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(),
)));
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());
@ -853,6 +891,7 @@ impl Engine {
value_result
}
}
}
// Import statement
#[cfg(not(feature = "no_module"))]
@ -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::<crate::ImmutableString>().ok_or_else(|| {
self.make_type_mismatch_err::<crate::ImmutableString>(
"",
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();

View File

@ -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,7 +368,14 @@ impl Engine {
is_op_assign,
);
if let Some(FnResolutionCacheEntry { func, source }) = func {
if func.is_some() {
let is_method = func.map(|f| f.func.is_method()).unwrap_or(false);
// Push a new call stack frame
#[cfg(feature = "debugging")]
let orig_call_stack_len = global.debugger.call_stack().len();
let mut _result = if let Some(FnResolutionCacheEntry { func, source }) = func {
assert!(func.is_native());
// Calling pure function but the first argument is a reference?
@ -379,13 +389,23 @@ impl Engine {
.change_first_arg_to_copy(args);
}
// Run external function
let source = match (source.as_str(), parent_source.as_str()) {
("", "") => None,
("", s) | (s, _) => Some(s),
};
let context = (self, name, source, &*global, lib, pos).into();
#[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()
@ -400,8 +420,40 @@ impl Engine {
bk.restore_first_arg(args)
}
result
} else {
unreachable!("`Some`");
};
#[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::<FnPtr>() {
return Err(self.make_type_mismatch_err::<FnPtr>(
@ -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::<FnPtr>() {
return Err(self.make_type_mismatch_err::<FnPtr>(
@ -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::<ImmutableString>(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::<ImmutableString>(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::<ImmutableString>(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");
if fn_def.body.is_empty() {
Ok(Dynamic::UNIT)
} else {
let new_scope = &mut Scope::new();
let mut source = module.id_raw().clone();
mem::swap(&mut global.source, &mut 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,
new_scope, global, state, lib, &mut None, fn_def, &mut args, true, pos, level,
);
global.source = source;
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)
}

View File

@ -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<str> + 'a + ?Sized>
@ -80,6 +82,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef<str> + '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<str> + 'a + ?Sized>
&'a GlobalRuntimeState,
&'a M,
Position,
usize,
),
) -> Self {
Self {
@ -100,6 +104,7 @@ impl<'a, M: AsRef<[&'a Module]> + ?Sized, S: AsRef<str> + '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<str> + '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<Optio
#[cfg(feature = "sync")]
pub type OnVarCallback =
dyn Fn(&str, usize, &EvalContext) -> RhaiResultOf<Option<Dynamic>> + Send + Sync;
/// Callback function for variable definition.
#[cfg(not(feature = "sync"))]
pub type OnDefVarCallback = dyn Fn(&str, bool, usize, bool, &EvalContext) -> RhaiResultOf<bool>;
/// Callback function for variable definition.
#[cfg(feature = "sync")]
pub type OnDefVarCallback =
dyn Fn(&str, bool, usize, bool, &EvalContext) -> RhaiResultOf<bool> + Send + Sync;

View File

@ -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,7 +97,7 @@ impl Engine {
// Push a new call stack frame
#[cfg(feature = "debugging")]
#[cfg(not(feature = "no_function"))]
if self.debugger.is_some() {
global.debugger.push_call_stack_frame(
fn_def.name.clone(),
scope
@ -96,12 +108,25 @@ impl Engine {
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 {
#[cfg(not(feature = "no_module"))]
let mut lib_merged = crate::StaticVec::with_capacity(lib.len() + 1);
#[cfg(not(feature = "no_module"))]
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 {
@ -109,20 +134,21 @@ impl Engine {
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
(lib, None)
};
#[cfg(not(feature = "no_module"))]
if let Some(ref modules) = fn_def.global {
for (n, m) in modules.iter().cloned() {
global.push_import(n, m)
}
#[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?

View File

@ -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;

View File

@ -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);
});
}

View File

@ -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,

View File

@ -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,

View File

@ -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<Identifier, Position>,
pub external_vars: Vec<crate::ast::Ident>,
/// 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::<ExclusiveRange>();
@ -1055,14 +1060,14 @@ fn parse_switch(
} else if let Some(range) = value.read_lock::<InclusiveRange>() {
(None, Some((*range.start(), *range.end(), true)))
} else if value.is::<INT>() && !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 (mut params, externals) = {
let externals: StaticVec<Identifier> = state
.external_vars
.iter()
.map(|(name, _)| name.clone())
.map(|crate::ast::Ident { name, .. }| name.clone())
.collect();
#[cfg(not(feature = "no_closure"))]
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,

View File

@ -37,9 +37,9 @@ fn check_struct_sizes() {
assert_eq!(
size_of::<NativeCallContext>(),
if cfg!(feature = "no_position") {
64
} else {
72
} else {
80
}
);
}

View File

@ -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::<String>() {
return if shorthands { "string" } else { "String" };
}
if name == type_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::<rust_decimal::Decimal>() {
return if shorthands { "decimal" } else { "Decimal" };
}
if name == type_name::<FnPtr>() {
return if shorthands { "Fn" } else { "FnPtr" };
}
#[cfg(not(feature = "no_index"))]
if name == type_name::<crate::Array>() {
return if shorthands { "array" } else { "Array" };
}
#[cfg(not(feature = "no_index"))]
if name == type_name::<crate::Blob>() {
return if shorthands { "blob" } else { "Blob" };
}
#[cfg(not(feature = "no_object"))]
if name == type_name::<crate::Map>() {
return if shorthands { "map" } else { "Map" };
}
#[cfg(not(feature = "no_std"))]
if name == type_name::<Instant>() {
return if shorthands { "timestamp" } else { "Instant" };
}
if name == type_name::<ExclusiveRange>() || name == "ExclusiveRange" {
return if shorthands {
"range"
} else if cfg!(feature = "only_i32") {
"Range<i32>"
} else {
"Range<i64>"
};
}
if name == type_name::<InclusiveRange>() || name == "InclusiveRange" {
return if shorthands {
"range="
} else if cfg!(feature = "only_i32") {
"RangeInclusive<i32>"
} else {
"RangeInclusive<i64>"
};
}
name
}
impl fmt::Display for Dynamic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {

View File

@ -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)

View File

@ -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;

View File

@ -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),

View File

@ -9,7 +9,12 @@ use rhai::Map;
#[test]
fn test_debugging() -> Result<(), Box<EvalAltResult>> {
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<EvalAltResult>> {
"
fn foo(x) {
if x >= 5 {
stack_trace()
back_trace()
} else {
foo(x+1)
}
@ -30,7 +35,7 @@ fn test_debugging() -> Result<(), Box<EvalAltResult>> {
assert_eq!(r.len(), 6);
assert_eq!(engine.eval::<INT>("len(stack_trace())")?, 0);
assert_eq!(engine.eval::<INT>("len(back_trace())")?, 0);
}
Ok(())
@ -41,7 +46,7 @@ fn test_debugging() -> Result<(), Box<EvalAltResult>> {
fn test_debugger_state() -> Result<(), Box<EvalAltResult>> {
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<EvalAltResult>> {
state.insert("foo".into(), false.into());
Dynamic::from_map(state)
},
|context, _, _, _| {
|context, _, _, _, _| {
// Get global runtime state
let global = context.global_runtime_state_mut();

View File

@ -514,3 +514,47 @@ fn test_module_file() -> Result<(), Box<EvalAltResult>> {
Module::eval_ast_as_new(Scope::new(), &ast, &engine)?;
Ok(())
}
#[cfg(not(feature = "no_function"))]
#[test]
fn test_module_environ() -> Result<(), Box<EvalAltResult>> {
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::<String>(
r#"
const SECRET = "hello";
fn foo(x) {
print(global::SECRET);
global::SECRET + x
}
let t = test::foo(0);
foo(t)
"#
)?,
"hello42"
);
Ok(())
}

View File

@ -78,33 +78,24 @@ fn test_optimizer_parse() -> Result<(), Box<EvalAltResult>> {
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(())
}

View File

@ -1,4 +1,4 @@
use rhai::{Engine, EvalAltResult};
use rhai::{Engine, EvalAltResult, Scope, INT};
#[test]
fn test_options_allow() -> Result<(), Box<EvalAltResult>> {
@ -31,6 +31,20 @@ fn test_options_allow() -> Result<(), Box<EvalAltResult>> {
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(())
}

View File

@ -120,3 +120,26 @@ fn test_var_resolver() -> Result<(), Box<EvalAltResult>> {
Ok(())
}
#[test]
fn test_var_def_filter() -> Result<(), Box<EvalAltResult>> {
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::<INT>("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(())
}