diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index df310705..2e2e0312 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -3,6 +3,7 @@ on: push: branches: - master + - debugger jobs: benchmark: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01b165a7..eb7b4483 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,7 @@ on: branches: - main - master + - debugger pull_request: {} jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6987d52e..ebe85c55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ Rhai Release Notes ================== +Version 1.5.0 +============= + +New features +------------ + +* A debugging interface is added. +* A new bin tool, `rhai-dbg` (aka _The Rhai Debugger_), is added to showcase the debugging interface. + + Version 1.4.2 ============= diff --git a/Cargo.toml b/Cargo.toml index 81d16837..4ad7a8c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ no_module = [] # no modules internals = [] # expose internal data structures unicode-xid-ident = ["unicode-xid"] # allow Unicode Standard Annex #31 for identifiers. metadata = ["serde", "serde_json", "rhai_codegen/metadata", "smartstring/serde"] # enable exporting functions metadata +debugging = ["internals"] # enable debugging no_std = ["no-std-compat", "num-traits/libm", "core-error", "libm", "ahash/compile-time-rng"] @@ -101,4 +102,4 @@ optional = true instant = { version = "0.1.10" } # WASM implementation of std::time::Instant [package.metadata.docs.rs] -features = ["metadata", "serde", "internals", "decimal"] # compiling for no-std +features = ["metadata", "serde", "internals", "decimal", "debugging"] diff --git a/src/api/call_fn.rs b/src/api/call_fn.rs index 94108484..003dcc67 100644 --- a/src/api/call_fn.rs +++ b/src/api/call_fn.rs @@ -155,6 +155,10 @@ impl Engine { ) -> RhaiResult { let state = &mut EvalState::new(); let global = &mut GlobalRuntimeState::new(); + + #[cfg(feature = "debugging")] + global.debugger.activate(self.debugger.is_some()); + let statements = ast.statements(); let orig_scope_len = scope.len(); diff --git a/src/api/eval.rs b/src/api/eval.rs index e90b6421..11038263 100644 --- a/src/api/eval.rs +++ b/src/api/eval.rs @@ -186,6 +186,9 @@ impl Engine { ) -> RhaiResultOf { let global = &mut GlobalRuntimeState::new(); + #[cfg(feature = "debugging")] + global.debugger.activate(self.debugger.is_some()); + let result = self.eval_ast_with_scope_raw(scope, global, ast, 0)?; let typ = self.map_type_name(result.type_name()); diff --git a/src/api/events.rs b/src/api/events.rs index 7b3eb67b..64921f97 100644 --- a/src/api/events.rs +++ b/src/api/events.rs @@ -64,7 +64,7 @@ impl Engine { self.resolve_var = Some(Box::new(callback)); self } - /// _(internals)_ Provide a callback that will be invoked during parsing to remap certain tokens. + /// _(internals)_ Register a callback that will be invoked during parsing to remap certain tokens. /// Exported under the `internals` feature only. /// /// # Callback Function Signature @@ -261,4 +261,22 @@ impl Engine { self.debug = Some(Box::new(callback)); self } + /// _(debugging)_ Register a callback for debugging. + /// Exported under the `debugging` feature only. + #[cfg(feature = "debugging")] + #[inline(always)] + pub fn on_debugger( + &mut self, + callback: impl Fn( + &mut EvalContext, + crate::ast::ASTNode, + Option<&str>, + Position, + ) -> crate::eval::DebuggerCommand + + SendSync + + 'static, + ) -> &mut Self { + self.debugger = Some(Box::new(callback)); + self + } } diff --git a/src/api/run.rs b/src/api/run.rs index f5ec3c78..1160c3c9 100644 --- a/src/api/run.rs +++ b/src/api/run.rs @@ -44,8 +44,10 @@ impl Engine { /// Evaluate an [`AST`] with own scope, returning any error (if any). #[inline] pub fn run_ast_with_scope(&self, scope: &mut Scope, ast: &AST) -> RhaiResultOf<()> { + let state = &mut EvalState::new(); let global = &mut GlobalRuntimeState::new(); - let mut state = EvalState::new(); + #[cfg(feature = "debugging")] + global.debugger.activate(self.debugger.is_some()); global.source = ast.source_raw().clone(); #[cfg(not(feature = "no_module"))] @@ -64,7 +66,7 @@ impl Engine { } else { &lib }; - self.eval_global_statements(scope, global, &mut state, statements, lib, 0)?; + self.eval_global_statements(scope, global, state, statements, lib, 0)?; } Ok(()) } diff --git a/src/ast/ast.rs b/src/ast/ast.rs index 3a7b96b9..948df827 100644 --- a/src/ast/ast.rs +++ b/src/ast/ast.rs @@ -811,7 +811,7 @@ impl AsRef> for AST { /// _(internals)_ An [`AST`] node, consisting of either an [`Expr`] or a [`Stmt`]. /// Exported under the `internals` feature only. -#[derive(Debug, Clone, Hash)] +#[derive(Debug, Clone, Copy, Hash)] pub enum ASTNode<'a> { /// A statement ([`Stmt`]). Stmt(&'a Stmt), @@ -831,6 +831,19 @@ impl<'a> From<&'a Expr> for ASTNode<'a> { } } +impl PartialEq for ASTNode<'_> { + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Stmt(x), Self::Stmt(y)) => std::ptr::eq(*x, *y), + (Self::Expr(x), Self::Expr(y)) => std::ptr::eq(*x, *y), + _ => false, + } + } +} + +impl Eq for ASTNode<'_> {} + impl ASTNode<'_> { /// Get the [`Position`] of this [`ASTNode`]. pub const fn position(&self) -> Position { diff --git a/src/bin/rhai-dbg.rs b/src/bin/rhai-dbg.rs new file mode 100644 index 00000000..cd1f0dd9 --- /dev/null +++ b/src/bin/rhai-dbg.rs @@ -0,0 +1,279 @@ +use rhai::{Dynamic, Engine, EvalAltResult, Position, Scope}; + +#[cfg(feature = "debugging")] +use rhai::debugger::{BreakPoint, DebuggerCommand}; + +use std::{ + env, + fs::File, + io::{stdin, stdout, Read, Write}, + path::Path, + process::exit, +}; + +/// Pretty-print source line. +fn print_source(lines: &[String], pos: Position) { + let line_no = if lines.len() > 1 { + if pos.is_none() { + "".to_string() + } else { + format!("{}: ", pos.line().unwrap()) + } + } else { + "".to_string() + }; + + // Print error position + if pos.is_none() { + // No position + println!(); + } else { + // Specific position - print line text + println!("{}{}", line_no, lines[pos.line().unwrap() - 1]); + + // Display position marker + println!("{0:>1$}", "^", line_no.len() + pos.position().unwrap(),); + } +} + +/// Pretty-print error. +fn print_error(input: &str, mut err: EvalAltResult) { + let lines: Vec<_> = input.trim().split('\n').collect(); + let pos = err.take_position(); + + let line_no = if lines.len() > 1 { + if pos.is_none() { + "".to_string() + } else { + format!("{}: ", pos.line().unwrap()) + } + } else { + "".to_string() + }; + + // Print error position + if pos.is_none() { + // No position + println!("{}", err); + } else { + // Specific position - print line text + println!("{}{}", line_no, lines[pos.line().unwrap() - 1]); + + // Display position marker + println!( + "{0:>1$} {2}", + "^", + line_no.len() + pos.position().unwrap(), + err + ); + } +} + +/// Print debug help. +fn print_debug_help() { + println!("help => print this help"); + println!("quit, exit => quit"); + println!("scope => print all variables in the scope"); + println!("node => print the current AST node"); + println!("breakpoints => print all break-points"); + println!("clear => delete all break-points"); + println!("break => set a new break-point at the current position"); + println!("step => go to the next expression, diving into functions"); + println!("next => go to the next statement but don't dive into functions"); + println!("continue => continue normal execution"); + println!(); +} + +/// Display the scope. +fn print_scope(scope: &Scope) { + scope + .iter_raw() + .enumerate() + .for_each(|(i, (name, constant, value))| { + #[cfg(not(feature = "no_closure"))] + let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; + #[cfg(feature = "no_closure")] + let value_is_shared = ""; + + println!( + "[{}] {}{}{} = {:?}", + i + 1, + if constant { "const " } else { "" }, + name, + value_is_shared, + *value.read_lock::().unwrap(), + ) + }); + + println!(); +} + +#[cfg(feature = "debugging")] +fn main() { + let title = format!("Rhai Debugger (version {})", env!("CARGO_PKG_VERSION")); + println!("{}", title); + println!("{0:=<1$}", "", title.len()); + + // Initialize scripting engine + let mut engine = Engine::new(); + + let mut script = String::new(); + let main_ast; + + #[cfg(not(feature = "no_module"))] + #[cfg(not(feature = "no_std"))] + { + // Load init scripts + if let Some(filename) = env::args().skip(1).next() { + let filename = match Path::new(&filename).canonicalize() { + Err(err) => { + eprintln!("Error script file path: {}\n{}", filename, err); + exit(1); + } + Ok(f) => { + match f.strip_prefix(std::env::current_dir().unwrap().canonicalize().unwrap()) { + Ok(f) => f.into(), + _ => f, + } + } + }; + + let mut f = match File::open(&filename) { + Err(err) => { + eprintln!( + "Error reading script file: {}\n{}", + filename.to_string_lossy(), + err + ); + exit(1); + } + Ok(f) => f, + }; + + if let Err(err) = f.read_to_string(&mut script) { + println!( + "Error reading script file: {}\n{}", + filename.to_string_lossy(), + err + ); + exit(1); + } + + let script = if script.starts_with("#!") { + // Skip shebang + &script[script.find('\n').unwrap_or(0)..] + } else { + &script[..] + }; + + main_ast = match engine + .compile(&script) + .map_err(Into::>::into) + { + Err(err) => { + print_error(&script, *err); + exit(1); + } + Ok(ast) => ast, + }; + + println!("Script '{}' loaded.", filename.to_string_lossy()); + println!(); + } else { + eprintln!("No script file specified."); + exit(1); + } + } + + // Hook up debugger + let lines: Vec<_> = script.trim().split('\n').map(|s| s.to_string()).collect(); + + engine.on_debugger(move |context, node, source, pos| { + print_source(&lines, pos); + + let mut input = String::new(); + + loop { + print!("rhai-dbg> "); + stdout().flush().expect("couldn't flush stdout"); + + input.clear(); + + match stdin().read_line(&mut input) { + Ok(0) => break DebuggerCommand::Continue, + Ok(_) => match input.as_str().trim_end() { + "help" => print_debug_help(), + "exit" | "quit" => { + println!("Script terminated. Bye!"); + exit(0); + } + "node" => { + println!("{:?} {}@{:?}", node, source.unwrap_or_default(), pos); + println!(); + } + "continue" => break DebuggerCommand::Continue, + "" | "step" => break DebuggerCommand::StepInto, + "next" => break DebuggerCommand::StepOver, + "scope" => print_scope(context.scope()), + "clear" => { + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .clear(); + println!("All break-points cleared."); + } + "breakpoints" => context + .global_runtime_state_mut() + .debugger + .iter_break_points() + .enumerate() + .for_each(|(i, bp)| match bp { + BreakPoint::AtPosition { pos, .. } => { + println!("[{}]", i); + print_source(&lines, *pos); + } + _ => println!("[{}]\n{}", i, bp), + }), + "break" => { + context + .global_runtime_state_mut() + .debugger + .break_points_mut() + .push(rhai::debugger::BreakPoint::AtPosition { + source: source.unwrap_or("").into(), + pos, + }); + println!("Break-point added at the current position."); + } + cmd => eprintln!("Invalid debugger command: '{}'", cmd), + }, + Err(err) => panic!("input error: {}", err), + } + } + }); + + // Set a file module resolver without caching + #[cfg(not(feature = "no_module"))] + #[cfg(not(feature = "no_std"))] + { + let mut resolver = rhai::module_resolvers::FileModuleResolver::new(); + resolver.enable_cache(false); + engine.set_module_resolver(resolver); + } + + // Create scope + let mut scope = Scope::new(); + + print_debug_help(); + + // Evaluate + if let Err(err) = engine.run_ast_with_scope(&mut scope, &main_ast) { + print_error(&script, *err); + } +} + +#[cfg(not(feature = "debugging"))] +fn main() { + panic!("rhai-dbg requires the 'debugging' feature.") +} diff --git a/src/engine.rs b/src/engine.rs index 58abe68b..3ff2c44e 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -137,6 +137,10 @@ pub struct Engine { /// Max limits. #[cfg(not(feature = "unchecked"))] pub(crate) limits: crate::api::limits::Limits, + + /// Callback closure for debugging. + #[cfg(feature = "debugging")] + pub(crate) debugger: Option, } impl fmt::Debug for Engine { @@ -226,7 +230,7 @@ impl Engine { engine.print = Some(Box::new(|s| println!("{}", s))); engine.debug = Some(Box::new(|s, source, pos| { if let Some(source) = source { - println!("{}{:?} | {}", source, pos, s); + println!("{} @ {:?} | {}", source, pos, s); } else if pos.is_none() { println!("{}", s); } else { @@ -280,6 +284,9 @@ impl Engine { #[cfg(not(feature = "unchecked"))] limits: crate::api::limits::Limits::new(), + + #[cfg(feature = "debugging")] + debugger: None, }; // Add the global namespace module diff --git a/src/eval/debugger.rs b/src/eval/debugger.rs new file mode 100644 index 00000000..b8f5f069 --- /dev/null +++ b/src/eval/debugger.rs @@ -0,0 +1,253 @@ +//! Module defining the debugging interface. +#![cfg(feature = "debugging")] + +use super::{EvalContext, EvalState, GlobalRuntimeState}; +use crate::ast::{ASTNode, Expr, Stmt}; +use crate::{Dynamic, Engine, Identifier, Module, Position, Scope, StaticVec}; +use std::fmt; +#[cfg(feature = "no_std")] +use std::prelude::v1::*; + +/// A standard callback function for debugging. +#[cfg(not(feature = "sync"))] +pub type OnDebuggerCallback = + Box, Position) -> DebuggerCommand + 'static>; +/// A standard callback function for debugging. +#[cfg(feature = "sync")] +pub type OnDebuggerCallback = Box< + dyn Fn(&mut EvalContext, ASTNode, Option<&str>, Position) -> DebuggerCommand + + Send + + Sync + + 'static, +>; + +/// A command for the debugger on the next iteration. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum DebuggerCommand { + // Continue normal execution. + Continue, + // Step into the next expression, diving into functions. + StepInto, + // Run to the next statement, stepping over functions. + StepOver, +} + +/// A break-point for debugging. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum BreakPoint { + /// Break at a particular position under a particular source. + /// Not available under `no_position`. + /// + /// Source is empty if not available. + #[cfg(not(feature = "no_position"))] + AtPosition { source: Identifier, pos: Position }, + /// Break at a particular function call. + AtFunctionName { fn_name: Identifier }, + /// Break at a particular function call with a particular number of arguments. + AtFunctionCall { fn_name: Identifier, args: usize }, +} + +impl fmt::Display for BreakPoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AtPosition { source, pos } => { + if !source.is_empty() { + write!(f, "{} @ {:?}", source, pos) + } else { + write!(f, "@ {:?}", pos) + } + } + Self::AtFunctionName { fn_name } => write!(f, "{} (...)", fn_name), + Self::AtFunctionCall { fn_name, args } => write!( + f, + "{} ({})", + fn_name, + std::iter::repeat("_") + .take(*args) + .collect::>() + .join(", ") + ), + } + } +} + +#[derive(Debug, Clone, Hash)] +pub struct CallStackFrame { + pub fn_name: Identifier, + pub args: StaticVec, + pub source: Identifier, + pub pos: Position, +} + +impl fmt::Display for CallStackFrame { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut fp = f.debug_tuple(&self.fn_name); + + for arg in &self.args { + fp.field(arg); + } + + fp.finish()?; + + if !self.pos.is_none() { + if self.source.is_empty() { + write!(f, " @ {:?}", self.pos)?; + } else { + write!(f, ": {} @ {:?}", self.source, self.pos)?; + } + } + + Ok(()) + } +} + +/// A type providing debugging facilities. +#[derive(Debug, Clone, Hash)] +pub struct Debugger { + active: bool, + break_points: Vec, + call_stack: Vec, +} + +impl Debugger { + /// Create a new [`Debugger`]. + pub const fn new() -> Self { + Self { + active: false, + break_points: Vec::new(), + call_stack: Vec::new(), + } + } + /// Get the function call stack depth. + #[inline(always)] + pub fn call_stack_len(&self) -> usize { + self.call_stack.len() + } + /// Rewind the function call stack to a particular depth. + #[inline(always)] + pub fn rewind_call_stack(&mut self, len: usize) { + self.call_stack.truncate(len); + } + /// Add a new frame to the function call stack. + #[inline(always)] + pub fn push_call_stack_frame( + &mut self, + fn_name: impl Into, + args: StaticVec, + source: impl Into, + pos: Position, + ) { + let fp = CallStackFrame { + fn_name: fn_name.into(), + args, + source: source.into(), + pos, + }; + println!("{}", fp); + self.call_stack.push(fp); + } + /// Is this [`Debugger`] currently active? + #[inline(always)] + #[must_use] + pub fn is_active(&self) -> bool { + self.active + } + /// Activate or deactivate this [`Debugger`]. + #[inline(always)] + pub fn activate(&mut self, active: bool) { + self.active = active; + } + /// Does a particular [`AST` Node][ASTNode] trigger a break-point? + pub fn is_break_point(&self, src: &str, node: ASTNode) -> bool { + self.iter_break_points().any(|bp| match bp { + #[cfg(not(feature = "no_position"))] + BreakPoint::AtPosition { source, pos } => node.position() == *pos && src == source, + BreakPoint::AtFunctionName { fn_name } => match node { + ASTNode::Expr(Expr::FnCall(x, _)) | ASTNode::Stmt(Stmt::FnCall(x, _)) => { + x.name == *fn_name + } + _ => false, + }, + BreakPoint::AtFunctionCall { fn_name, args } => match node { + ASTNode::Expr(Expr::FnCall(x, _)) | ASTNode::Stmt(Stmt::FnCall(x, _)) => { + x.args.len() == *args && x.name == *fn_name + } + _ => false, + }, + }) + } + /// Get a slice of all [`BreakPoint`]'s. + #[inline(always)] + #[must_use] + pub fn break_points(&mut self) -> &[BreakPoint] { + &self.break_points + } + /// Get the underlying [`Vec`] holding all [`BreakPoint`]'s. + #[inline(always)] + #[must_use] + pub fn break_points_mut(&mut self) -> &mut Vec { + &mut self.break_points + } + /// Get an iterator over all [`BreakPoint`]'s. + #[inline(always)] + #[must_use] + pub fn iter_break_points(&self) -> impl Iterator { + self.break_points.iter() + } + /// Get a mutable iterator over all [`BreakPoint`]'s. + #[inline(always)] + #[must_use] + pub fn iter_break_points_mut(&mut self) -> impl Iterator { + self.break_points.iter_mut() + } +} + +impl Engine { + pub(crate) fn run_debugger( + &self, + scope: &mut Scope, + global: &mut GlobalRuntimeState, + state: &mut EvalState, + lib: &[&Module], + this_ptr: &mut Option<&mut Dynamic>, + node: ASTNode, + level: usize, + ) -> bool { + if let Some(ref on_debugger) = self.debugger { + if global.debugger.active || global.debugger.is_break_point(&global.source, node) { + let source = global.source.clone(); + let source = if source.is_empty() { + None + } else { + Some(source.as_str()) + }; + let mut context = crate::EvalContext { + engine: self, + scope, + global, + state, + lib, + this_ptr, + level, + }; + + match on_debugger(&mut context, node, source, node.position()) { + DebuggerCommand::Continue => { + global.debugger.activate(false); + return false; + } + DebuggerCommand::StepInto => { + global.debugger.activate(true); + return true; + } + DebuggerCommand::StepOver => { + global.debugger.activate(false); + return true; + } + } + } + } + + false + } +} diff --git a/src/eval/eval_context.rs b/src/eval/eval_context.rs index 7b0aa5dd..346242de 100644 --- a/src/eval/eval_context.rs +++ b/src/eval/eval_context.rs @@ -24,7 +24,7 @@ pub struct EvalContext<'a, 'x, 'px, 'm, 'pm, 's, 'ps, 'b, 't, 'pt> { pub(crate) level: usize, } -impl<'x, 'px, 'pt> EvalContext<'_, 'x, 'px, '_, '_, '_, '_, '_, '_, 'pt> { +impl<'x, 'px, 'm, 'pm, 'pt> EvalContext<'_, 'x, 'px, 'm, 'pm, '_, '_, '_, '_, 'pt> { /// The current [`Engine`]. #[inline(always)] #[must_use] @@ -46,7 +46,7 @@ impl<'x, 'px, 'pt> EvalContext<'_, 'x, 'px, '_, '_, '_, '_, '_, '_, 'pt> { pub const fn scope(&self) -> &Scope<'px> { self.scope } - /// Mutable reference to the current [`Scope`]. + /// Get a mutable reference to the current [`Scope`]. #[inline(always)] #[must_use] pub fn scope_mut(&mut self) -> &mut &'x mut Scope<'px> { @@ -67,6 +67,15 @@ impl<'x, 'px, 'pt> EvalContext<'_, 'x, 'px, '_, '_, '_, '_, '_, '_, 'pt> { pub const fn global_runtime_state(&self) -> &GlobalRuntimeState { self.global } + /// _(internals)_ Get a mutable reference to the current [`GlobalRuntimeState`]. + /// Exported under the `internals` feature only. + #[cfg(feature = "internals")] + #[cfg(not(feature = "no_module"))] + #[inline(always)] + #[must_use] + pub fn global_runtime_state_mut(&mut self) -> &mut &'m mut GlobalRuntimeState<'pm> { + &mut self.global + } /// Get an iterator over the namespaces containing definition of all script-defined functions. #[inline] pub fn iter_namespaces(&self) -> impl Iterator { diff --git a/src/eval/eval_state.rs b/src/eval/eval_state.rs index d0e9b2bc..943831b7 100644 --- a/src/eval/eval_state.rs +++ b/src/eval/eval_state.rs @@ -7,16 +7,17 @@ use std::marker::PhantomData; #[cfg(feature = "no_std")] use std::prelude::v1::*; -/// _(internals)_ A type that holds all the current states of the [`Engine`]. +/// _(internals)_ A type that holds all the current states of the [`Engine`][crate::Engine]. /// Exported under the `internals` feature only. #[derive(Debug, Clone)] pub struct EvalState<'a> { - /// Force a [`Scope`] search by name. + /// Force a [`Scope`][crate::Scope] search by name. /// - /// Normally, access to variables are parsed with a relative offset into the [`Scope`] to avoid a lookup. + /// Normally, access to variables are parsed with a relative offset into the + /// [`Scope`][crate::Scope] to avoid a lookup. /// - /// In some situation, e.g. after running an `eval` statement, or after a custom syntax statement, - /// subsequent offsets may become mis-aligned. + /// In some situation, e.g. after running an `eval` statement, or after a custom syntax + /// statement, subsequent offsets may become mis-aligned. /// /// When that happens, this flag is turned on. pub always_search_scope: bool, diff --git a/src/eval/expr.rs b/src/eval/expr.rs index 47fbffef..d82c7b59 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -242,6 +242,13 @@ impl Engine { } /// Evaluate an expression. + // + // # Implementation Notes + // + // Do not use the `?` operator within the main body as it makes this function return early, + // possibly by-passing important cleanup tasks at the end. + // + // Errors that are not recoverable, such as system errors or safety errors, can use `?`. pub(crate) fn eval_expr( &self, scope: &mut Scope, @@ -252,6 +259,10 @@ impl Engine { expr: &Expr, level: usize, ) -> RhaiResult { + #[cfg(feature = "debugging")] + let reset_debugger_command = + self.run_debugger(scope, global, state, lib, this_ptr, expr.into(), level); + // Coded this way for better branch prediction. // Popular branches are lifted out of the `match` statement into their own branches. @@ -261,7 +272,13 @@ impl Engine { #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, expr.position())?; - return self.eval_fn_call_expr(scope, global, state, lib, this_ptr, x, *pos, level); + let result = + self.eval_fn_call_expr(scope, global, state, lib, this_ptr, x, *pos, level); + + #[cfg(feature = "debugging")] + global.debugger.activate(reset_debugger_command); + + return result; } // Then variable access. @@ -271,7 +288,7 @@ impl Engine { #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, expr.position())?; - return if index.is_none() && x.0.is_none() && x.2 == KEYWORD_THIS { + let result = if index.is_none() && x.0.is_none() && x.2 == KEYWORD_THIS { this_ptr .as_deref() .cloned() @@ -280,12 +297,17 @@ impl Engine { self.search_namespace(scope, global, state, lib, this_ptr, expr) .map(|(val, _)| val.take_or_clone()) }; + + #[cfg(feature = "debugging")] + global.debugger.activate(reset_debugger_command); + + return result; } #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, expr.position())?; - match expr { + let result = match expr { // Constants Expr::DynamicConstant(x, _) => Ok(x.as_ref().clone()), Expr::IntegerConstant(x, _) => Ok((*x).into()), @@ -299,47 +321,62 @@ impl Engine { // `... ${...} ...` Expr::InterpolatedString(x, pos) => { let mut pos = *pos; - let mut result: Dynamic = self.const_empty_string().into(); + let mut concat: Dynamic = self.const_empty_string().into(); + let mut result = Ok(Dynamic::UNIT); for expr in x.iter() { - let item = self.eval_expr(scope, global, state, lib, this_ptr, expr, level)?; + let item = + match self.eval_expr(scope, global, state, lib, this_ptr, expr, level) { + Ok(r) => r, + err => { + result = err; + break; + } + }; - self.eval_op_assignment( + if let Err(err) = self.eval_op_assignment( global, state, lib, Some(OpAssignment::new(OP_CONCAT)), pos, - &mut (&mut result).into(), + &mut (&mut concat).into(), ("", Position::NONE), item, - ) - .map_err(|err| err.fill_position(expr.position()))?; + ) { + result = Err(err.fill_position(expr.position())); + break; + } pos = expr.position(); } - Ok(result) + result.map(|_| concat) } #[cfg(not(feature = "no_index"))] Expr::Array(x, _) => { - let mut arr = Dynamic::from_array(crate::Array::with_capacity(x.len())); + let mut arr = crate::Array::with_capacity(x.len()); + let mut result = Ok(Dynamic::UNIT); #[cfg(not(feature = "unchecked"))] let mut sizes = (0, 0, 0); for item_expr in x.iter() { - let value = self - .eval_expr(scope, global, state, lib, this_ptr, item_expr, level)? - .flatten(); + let value = match self + .eval_expr(scope, global, state, lib, this_ptr, item_expr, level) + { + Ok(r) => r.flatten(), + err => { + result = err; + break; + } + }; #[cfg(not(feature = "unchecked"))] let val_sizes = Self::calc_data_sizes(&value, true); - arr.write_lock::() - .expect("`Array`") - .push(value); + arr.push(value); #[cfg(not(feature = "unchecked"))] if self.has_data_size_limit() { @@ -352,29 +389,33 @@ impl Engine { } } - Ok(arr) + result.map(|_| arr.into()) } #[cfg(not(feature = "no_object"))] Expr::Map(x, _) => { - let mut map = Dynamic::from_map(x.1.clone()); + let mut map = x.1.clone(); + let mut result = Ok(Dynamic::UNIT); #[cfg(not(feature = "unchecked"))] let mut sizes = (0, 0, 0); for (crate::ast::Ident { name, .. }, value_expr) in x.0.iter() { let key = name.as_str(); - let value = self - .eval_expr(scope, global, state, lib, this_ptr, value_expr, level)? - .flatten(); + let value = match self + .eval_expr(scope, global, state, lib, this_ptr, value_expr, level) + { + Ok(r) => r.flatten(), + err => { + result = err; + break; + } + }; #[cfg(not(feature = "unchecked"))] let val_sizes = Self::calc_data_sizes(&value, true); - *map.write_lock::() - .expect("`Map`") - .get_mut(key) - .unwrap() = value; + *map.get_mut(key).unwrap() = value; #[cfg(not(feature = "unchecked"))] if self.has_data_size_limit() { @@ -387,33 +428,53 @@ impl Engine { } } - Ok(map) + result.map(|_| map.into()) } Expr::And(x, _) => { - Ok((self - .eval_expr(scope, global, state, lib, this_ptr, &x.lhs, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, x.lhs.position()))? - && // Short-circuit using && - self - .eval_expr(scope, global, state, lib, this_ptr, &x.rhs, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, x.rhs.position()))?) - .into()) + let lhs = self + .eval_expr(scope, global, state, lib, this_ptr, &x.lhs, level) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::(typ, x.lhs.position()) + }) + }); + + if let Ok(true) = lhs { + self.eval_expr(scope, global, state, lib, this_ptr, &x.rhs, level) + .and_then(|v| { + v.as_bool() + .map_err(|typ| { + self.make_type_mismatch_err::(typ, x.rhs.position()) + }) + .map(Into::into) + }) + } else { + lhs.map(Into::into) + } } Expr::Or(x, _) => { - Ok((self - .eval_expr(scope, global, state, lib, this_ptr, &x.lhs, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, x.lhs.position()))? - || // Short-circuit using || - self - .eval_expr(scope, global, state, lib, this_ptr, &x.rhs, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, x.rhs.position()))?) - .into()) + let lhs = self + .eval_expr(scope, global, state, lib, this_ptr, &x.lhs, level) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::(typ, x.lhs.position()) + }) + }); + + if let Ok(false) = lhs { + self.eval_expr(scope, global, state, lib, this_ptr, &x.rhs, level) + .and_then(|v| { + v.as_bool() + .map_err(|typ| { + self.make_type_mismatch_err::(typ, x.rhs.position()) + }) + .map(Into::into) + }) + } else { + lhs.map(Into::into) + } } Expr::Custom(custom, pos) => { @@ -459,6 +520,11 @@ impl Engine { } _ => unreachable!("expression cannot be evaluated: {:?}", expr), - } + }; + + #[cfg(feature = "debugging")] + global.debugger.activate(reset_debugger_command); + + return result; } } diff --git a/src/eval/global_state.rs b/src/eval/global_state.rs index b2dc0181..241f6d16 100644 --- a/src/eval/global_state.rs +++ b/src/eval/global_state.rs @@ -46,6 +46,9 @@ pub struct GlobalRuntimeState<'a> { #[cfg(not(feature = "no_function"))] constants: Option>>>, + /// Debugging interface. + #[cfg(feature = "debugging")] + pub debugger: super::Debugger, /// Take care of the lifetime parameter. dummy: PhantomData<&'a ()>, } @@ -75,6 +78,8 @@ impl GlobalRuntimeState<'_> { #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_function"))] constants: None, + #[cfg(feature = "debugging")] + debugger: crate::eval::Debugger::new(), dummy: PhantomData::default(), } } @@ -179,6 +184,15 @@ impl GlobalRuntimeState<'_> { .rev() .find_map(|m| m.get_qualified_iter(id)) } + /// Get the current source. + #[inline] + #[must_use] + pub fn source(&self) -> Option<&str> { + match self.source.as_str() { + "" => None, + s => Some(s), + } + } /// Get a mutable reference to the cache of globally-defined constants. #[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_function"))] diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 9429672e..23893682 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -1,5 +1,6 @@ mod chaining; mod data_check; +mod debugger; mod eval_context; mod eval_state; mod expr; @@ -9,6 +10,8 @@ mod target; #[cfg(any(not(feature = "no_index"), not(feature = "no_object")))] pub use chaining::{ChainArgument, ChainType}; +#[cfg(feature = "debugging")] +pub use debugger::{BreakPoint, Debugger, DebuggerCommand, OnDebuggerCallback}; pub use eval_context::EvalContext; pub use eval_state::EvalState; pub use global_state::GlobalRuntimeState; diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index 96aa85bc..92d5d6eb 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -11,6 +11,13 @@ use std::prelude::v1::*; impl Engine { /// Evaluate a statements block. + // + // # Implementation Notes + // + // Do not use the `?` operator within the main body as it makes this function return early, + // possibly by-passing important cleanup tasks at the end. + // + // Errors that are not recoverable, such as system errors or safety errors, can use `?`. pub(crate) fn eval_stmt_block( &self, scope: &mut Scope, @@ -28,7 +35,7 @@ impl Engine { let orig_always_search_scope = state.always_search_scope; let orig_scope_len = scope.len(); - let orig_mods_len = global.num_imports(); + let orig_imports_len = global.num_imports(); let orig_fn_resolution_caches_len = state.fn_resolution_caches_len(); if restore_orig_state { @@ -38,7 +45,8 @@ impl Engine { let mut result = Ok(Dynamic::UNIT); for stmt in statements { - let _mods_len = global.num_imports(); + #[cfg(not(feature = "no_module"))] + let imports_len = global.num_imports(); result = self.eval_stmt( scope, @@ -61,7 +69,7 @@ impl Engine { // Without global functions, the extra modules never affect function resolution. if global .scan_imports_raw() - .skip(_mods_len) + .skip(imports_len) .any(|(_, m)| m.contains_indexed_global_functions()) { if state.fn_resolution_caches_len() > orig_fn_resolution_caches_len { @@ -86,7 +94,7 @@ impl Engine { if restore_orig_state { scope.rewind(orig_scope_len); state.scope_level -= 1; - global.truncate_imports(orig_mods_len); + global.truncate_imports(orig_imports_len); // The impact of new local variables goes away at the end of a block // because any new variables introduced will go out of scope @@ -168,11 +176,13 @@ impl Engine { } /// Evaluate a statement. - /// - /// # Safety - /// - /// This method uses some unsafe code, mainly for avoiding cloning of local variable names via - /// direct lifetime casting. + // + // # Implementation Notes + // + // Do not use the `?` operator within the main body as it makes this function return early, + // possibly by-passing important cleanup tasks at the end. + // + // Errors that are not recoverable, such as system errors or safety errors, can use `?`. pub(crate) fn eval_stmt( &self, scope: &mut Scope, @@ -184,6 +194,10 @@ impl Engine { rewind_scope: bool, level: usize, ) -> RhaiResult { + #[cfg(feature = "debugging")] + let reset_debugger_command = + self.run_debugger(scope, global, state, lib, this_ptr, stmt.into(), level); + // Coded this way for better branch prediction. // Popular branches are lifted out of the `match` statement into their own branches. @@ -192,7 +206,13 @@ impl Engine { #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, stmt.position())?; - return self.eval_fn_call_expr(scope, global, state, lib, this_ptr, x, *pos, level); + let result = + self.eval_fn_call_expr(scope, global, state, lib, this_ptr, x, *pos, level); + + #[cfg(feature = "debugging")] + global.debugger.activate(reset_debugger_command); + + return result; } // Then assignments. @@ -202,81 +222,101 @@ impl Engine { #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, stmt.position())?; - return if x.0.is_variable_access(false) { + let result = if x.0.is_variable_access(false) { let (lhs_expr, op_info, rhs_expr) = x.as_ref(); - let rhs_val = self - .eval_expr(scope, global, state, lib, this_ptr, rhs_expr, level)? - .flatten(); - let (mut lhs_ptr, pos) = - self.search_namespace(scope, global, state, lib, this_ptr, lhs_expr)?; + let rhs_result = self + .eval_expr(scope, global, state, lib, this_ptr, rhs_expr, level) + .map(Dynamic::flatten); - let var_name = lhs_expr.get_variable_name(false).expect("`Expr::Variable`"); + if let Ok(rhs_val) = rhs_result { + let search_result = + self.search_namespace(scope, global, state, lib, this_ptr, lhs_expr); - if !lhs_ptr.is_ref() { - return Err(ERR::ErrorAssignmentToConstant(var_name.to_string(), pos).into()); + if let Ok(search_val) = search_result { + let (mut lhs_ptr, pos) = search_val; + + let var_name = lhs_expr.get_variable_name(false).expect("`Expr::Variable`"); + + if !lhs_ptr.is_ref() { + return Err( + ERR::ErrorAssignmentToConstant(var_name.to_string(), pos).into() + ); + } + + #[cfg(not(feature = "unchecked"))] + self.inc_operations(&mut global.num_operations, pos)?; + + self.eval_op_assignment( + global, + state, + lib, + *op_info, + *op_pos, + &mut lhs_ptr, + (var_name, pos), + rhs_val, + ) + .map_err(|err| err.fill_position(rhs_expr.position())) + .map(|_| Dynamic::UNIT) + } else { + search_result.map(|_| Dynamic::UNIT) + } + } else { + rhs_result } - - #[cfg(not(feature = "unchecked"))] - self.inc_operations(&mut global.num_operations, pos)?; - - self.eval_op_assignment( - global, - state, - lib, - *op_info, - *op_pos, - &mut lhs_ptr, - (var_name, pos), - rhs_val, - ) - .map_err(|err| err.fill_position(rhs_expr.position()))?; - - Ok(Dynamic::UNIT) } else { let (lhs_expr, op_info, rhs_expr) = x.as_ref(); - let rhs_val = self - .eval_expr(scope, global, state, lib, this_ptr, rhs_expr, level)? - .flatten(); - let _new_val = Some(((rhs_val, rhs_expr.position()), (*op_info, *op_pos))); + let rhs_result = self + .eval_expr(scope, global, state, lib, this_ptr, rhs_expr, level) + .map(Dynamic::flatten); - // Must be either `var[index] op= val` or `var.prop op= val` - match lhs_expr { - // name op= rhs (handled above) - Expr::Variable(_, _, _) => { - unreachable!("Expr::Variable case is already handled") + if let Ok(rhs_val) = rhs_result { + let _new_val = Some(((rhs_val, rhs_expr.position()), (*op_info, *op_pos))); + + // Must be either `var[index] op= val` or `var.prop op= val` + match lhs_expr { + // name op= rhs (handled above) + Expr::Variable(_, _, _) => { + unreachable!("Expr::Variable case is already handled") + } + // idx_lhs[idx_expr] op= rhs + #[cfg(not(feature = "no_index"))] + Expr::Index(_, _, _) => self + .eval_dot_index_chain( + scope, global, state, lib, this_ptr, lhs_expr, level, _new_val, + ) + .map(|_| Dynamic::UNIT), + // dot_lhs.dot_rhs op= rhs + #[cfg(not(feature = "no_object"))] + Expr::Dot(_, _, _) => self + .eval_dot_index_chain( + scope, global, state, lib, this_ptr, lhs_expr, level, _new_val, + ) + .map(|_| Dynamic::UNIT), + _ => unreachable!("cannot assign to expression: {:?}", lhs_expr), } - // idx_lhs[idx_expr] op= rhs - #[cfg(not(feature = "no_index"))] - Expr::Index(_, _, _) => { - self.eval_dot_index_chain( - scope, global, state, lib, this_ptr, lhs_expr, level, _new_val, - )?; - Ok(Dynamic::UNIT) - } - // dot_lhs.dot_rhs op= rhs - #[cfg(not(feature = "no_object"))] - Expr::Dot(_, _, _) => { - self.eval_dot_index_chain( - scope, global, state, lib, this_ptr, lhs_expr, level, _new_val, - )?; - Ok(Dynamic::UNIT) - } - _ => unreachable!("cannot assign to expression: {:?}", lhs_expr), + } else { + rhs_result } }; + + #[cfg(feature = "debugging")] + global.debugger.activate(reset_debugger_command); + + return result; } #[cfg(not(feature = "unchecked"))] self.inc_operations(&mut global.num_operations, stmt.position())?; - match stmt { + let result = match stmt { // No-op Stmt::Noop(_) => Ok(Dynamic::UNIT), // Expression as statement - Stmt::Expr(expr) => Ok(self - .eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .flatten()), + Stmt::Expr(expr) => self + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .map(Dynamic::flatten), // Block scope Stmt::Block(statements, _) if statements.is_empty() => Ok(Dynamic::UNIT), @@ -287,22 +327,33 @@ impl Engine { // If statement Stmt::If(expr, x, _) => { let guard_val = self - .eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, expr.position()))?; + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::(typ, expr.position()) + }) + }); - if guard_val { - if !x.0.is_empty() { - self.eval_stmt_block(scope, global, state, lib, this_ptr, &x.0, true, level) - } else { - Ok(Dynamic::UNIT) + match guard_val { + Ok(true) => { + if !x.0.is_empty() { + self.eval_stmt_block( + scope, global, state, lib, this_ptr, &x.0, true, level, + ) + } else { + Ok(Dynamic::UNIT) + } } - } else { - if !x.1.is_empty() { - self.eval_stmt_block(scope, global, state, lib, this_ptr, &x.1, true, level) - } else { - Ok(Dynamic::UNIT) + Ok(false) => { + if !x.1.is_empty() { + self.eval_stmt_block( + scope, global, state, lib, this_ptr, &x.1, true, level, + ) + } else { + Ok(Dynamic::UNIT) + } } + err => err.map(Into::into), } } @@ -310,85 +361,107 @@ impl Engine { Stmt::Switch(match_expr, x, _) => { let (table, def_stmt, ranges) = x.as_ref(); - let value = - self.eval_expr(scope, global, state, lib, this_ptr, match_expr, level)?; + let value_result = + self.eval_expr(scope, global, state, lib, this_ptr, match_expr, level); - let stmt_block = if value.is_hashable() { - let hasher = &mut get_hasher(); - value.hash(hasher); - let hash = hasher.finish(); + if let Ok(value) = value_result { + let stmt_block_result = if value.is_hashable() { + let hasher = &mut get_hasher(); + value.hash(hasher); + let hash = hasher.finish(); - // First check hashes - if let Some(t) = table.get(&hash) { - if let Some(ref c) = t.0 { - if self - .eval_expr(scope, global, state, lib, this_ptr, &c, level)? - .as_bool() - .map_err(|typ| { - self.make_type_mismatch_err::(typ, c.position()) - })? + // First check hashes + if let Some(t) = table.get(&hash) { + let cond_result = t + .0 + .as_ref() + .map(|cond| { + self.eval_expr(scope, global, state, lib, this_ptr, cond, level) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::( + typ, + cond.position(), + ) + }) + }) + }) + .unwrap_or(Ok(true)); + + match cond_result { + Ok(true) => Ok(Some(&t.1)), + Ok(false) => Ok(None), + _ => cond_result.map(|_| None), + } + } else if value.is::() && !ranges.is_empty() { + // Then check integer ranges + let value = value.as_int().expect("`INT`"); + let mut result = Ok(None); + + for (_, _, _, condition, stmt_block) in + ranges.iter().filter(|&&(start, end, inclusive, _, _)| { + (!inclusive && (start..end).contains(&value)) + || (inclusive && (start..=end).contains(&value)) + }) { - Some(&t.1) - } else { - None - } - } else { - Some(&t.1) - } - } else if value.is::() && !ranges.is_empty() { - // Then check integer ranges - let value = value.as_int().expect("`INT`"); - let mut result = None; + let cond_result = condition + .as_ref() + .map(|cond| { + self.eval_expr( + scope, global, state, lib, this_ptr, cond, level, + ) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::( + typ, + cond.position(), + ) + }) + }) + }) + .unwrap_or(Ok(true)); - for (_, _, _, condition, stmt_block) in - ranges.iter().filter(|&&(start, end, inclusive, _, _)| { - (!inclusive && (start..end).contains(&value)) - || (inclusive && (start..=end).contains(&value)) - }) - { - if let Some(c) = condition { - if !self - .eval_expr(scope, global, state, lib, this_ptr, &c, level)? - .as_bool() - .map_err(|typ| { - self.make_type_mismatch_err::(typ, c.position()) - })? - { - continue; + match cond_result { + Ok(true) => result = Ok(Some(stmt_block)), + Ok(false) => continue, + _ => result = cond_result.map(|_| None), } + + break; } - result = Some(stmt_block); - break; + result + } else { + // Nothing matches + Ok(None) } - - result } else { - // Nothing matches - None + // Non-hashable + Ok(None) + }; + + if let Ok(Some(statements)) = stmt_block_result { + if !statements.is_empty() { + self.eval_stmt_block( + scope, global, state, lib, this_ptr, statements, true, level, + ) + } else { + Ok(Dynamic::UNIT) + } + } else if let Ok(None) = stmt_block_result { + // Default match clause + if !def_stmt.is_empty() { + self.eval_stmt_block( + scope, global, state, lib, this_ptr, def_stmt, true, level, + ) + } else { + Ok(Dynamic::UNIT) + } + } else { + stmt_block_result.map(|_| Dynamic::UNIT) } } else { - // Non-hashable - None - }; - - if let Some(statements) = stmt_block { - if !statements.is_empty() { - self.eval_stmt_block( - scope, global, state, lib, this_ptr, statements, true, level, - ) - } else { - Ok(Dynamic::UNIT) - } - } else { - // Default match clause - if !def_stmt.is_empty() { - self.eval_stmt_block( - scope, global, state, lib, this_ptr, def_stmt, true, level, - ) - } else { - Ok(Dynamic::UNIT) - } + value_result } } @@ -401,8 +474,8 @@ impl Engine { Ok(_) => (), Err(err) => match *err { ERR::LoopBreak(false, _) => (), - ERR::LoopBreak(true, _) => return Ok(Dynamic::UNIT), - _ => return Err(err), + ERR::LoopBreak(true, _) => break Ok(Dynamic::UNIT), + _ => break Err(err), }, } } else { @@ -414,24 +487,29 @@ impl Engine { // While loop Stmt::While(expr, body, _) => loop { let condition = self - .eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, expr.position()))?; + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::(typ, expr.position()) + }) + }); - if !condition { - return Ok(Dynamic::UNIT); - } - if !body.is_empty() { - match self - .eval_stmt_block(scope, global, state, lib, this_ptr, body, true, level) - { - Ok(_) => (), - Err(err) => match *err { - ERR::LoopBreak(false, _) => (), - ERR::LoopBreak(true, _) => return Ok(Dynamic::UNIT), - _ => return Err(err), - }, + match condition { + Ok(false) => break Ok(Dynamic::UNIT), + Ok(true) if body.is_empty() => (), + Ok(true) => { + match self + .eval_stmt_block(scope, global, state, lib, this_ptr, body, true, level) + { + Ok(_) => (), + Err(err) => match *err { + ERR::LoopBreak(false, _) => (), + ERR::LoopBreak(true, _) => break Ok(Dynamic::UNIT), + _ => break Err(err), + }, + } } + err => break err.map(|_| Dynamic::UNIT), } }, @@ -446,143 +524,154 @@ impl Engine { Ok(_) => (), Err(err) => match *err { ERR::LoopBreak(false, _) => continue, - ERR::LoopBreak(true, _) => return Ok(Dynamic::UNIT), - _ => return Err(err), + ERR::LoopBreak(true, _) => break Ok(Dynamic::UNIT), + _ => break Err(err), }, } } let condition = self - .eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .as_bool() - .map_err(|typ| self.make_type_mismatch_err::(typ, expr.position()))?; + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .and_then(|v| { + v.as_bool().map_err(|typ| { + self.make_type_mismatch_err::(typ, expr.position()) + }) + }); - if condition ^ is_while { - return Ok(Dynamic::UNIT); + match condition { + Ok(condition) if condition ^ is_while => break Ok(Dynamic::UNIT), + Ok(_) => (), + err => break err.map(|_| Dynamic::UNIT), } }, // For loop Stmt::For(expr, x, _) => { let (Ident { name: var_name, .. }, counter, statements) = x.as_ref(); - let iter_obj = self - .eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .flatten(); - let iter_type = iter_obj.type_id(); - // lib should only contain scripts, so technically they cannot have iterators + let iter_result = self + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .map(Dynamic::flatten); - // Search order: - // 1) Global namespace - functions registered via Engine::register_XXX - // 2) Global modules - packages - // 3) Imported modules - functions marked with global namespace - // 4) Global sub-modules - functions marked with global namespace - let func = self - .global_modules - .iter() - .find_map(|m| m.get_iter(iter_type)) - .or_else(|| global.get_iter(iter_type)) - .or_else(|| { - self.global_sub_modules - .values() - .find_map(|m| m.get_qualified_iter(iter_type)) - }); + if let Ok(iter_obj) = iter_result { + let iter_type = iter_obj.type_id(); - if let Some(func) = func { - // Add the loop variables - let orig_scope_len = scope.len(); - let counter_index = if let Some(counter) = counter { - scope.push(counter.name.clone(), 0 as INT); - scope.len() - 1 - } else { - usize::MAX - }; + // lib should only contain scripts, so technically they cannot have iterators - scope.push(var_name.clone(), ()); - let index = scope.len() - 1; + // Search order: + // 1) Global namespace - functions registered via Engine::register_XXX + // 2) Global modules - packages + // 3) Imported modules - functions marked with global namespace + // 4) Global sub-modules - functions marked with global namespace + let func = self + .global_modules + .iter() + .find_map(|m| m.get_iter(iter_type)) + .or_else(|| global.get_iter(iter_type)) + .or_else(|| { + self.global_sub_modules + .values() + .find_map(|m| m.get_qualified_iter(iter_type)) + }); - let mut loop_result = Ok(Dynamic::UNIT); + if let Some(func) = func { + // Add the loop variables + let orig_scope_len = scope.len(); + let counter_index = if let Some(counter) = counter { + scope.push(counter.name.clone(), 0 as INT); + scope.len() - 1 + } else { + usize::MAX + }; - for (x, iter_value) in func(iter_obj).enumerate() { - // Increment counter - if counter_index < usize::MAX { - #[cfg(not(feature = "unchecked"))] - if x > INT::MAX as usize { - loop_result = Err(ERR::ErrorArithmetic( - format!("for-loop counter overflow: {}", x), - counter.as_ref().expect("`Some`").pos, - ) - .into()); - break; + scope.push(var_name.clone(), ()); + let index = scope.len() - 1; + + let mut loop_result = Ok(Dynamic::UNIT); + + for (x, iter_value) in func(iter_obj).enumerate() { + // Increment counter + if counter_index < usize::MAX { + #[cfg(not(feature = "unchecked"))] + if x > INT::MAX as usize { + loop_result = Err(ERR::ErrorArithmetic( + format!("for-loop counter overflow: {}", x), + counter.as_ref().expect("`Some`").pos, + ) + .into()); + break; + } + + let index_value = (x as INT).into(); + + #[cfg(not(feature = "no_closure"))] + { + let index_var = scope.get_mut_by_index(counter_index); + if index_var.is_shared() { + *index_var.write_lock().expect("`Dynamic`") = index_value; + } else { + *index_var = index_value; + } + } + #[cfg(feature = "no_closure")] + { + *scope.get_mut_by_index(counter_index) = index_value; + } } - let index_value = (x as INT).into(); + let value = iter_value.flatten(); #[cfg(not(feature = "no_closure"))] { - let index_var = scope.get_mut_by_index(counter_index); - if index_var.is_shared() { - *index_var.write_lock().expect("`Dynamic`") = index_value; + let loop_var = scope.get_mut_by_index(index); + if loop_var.is_shared() { + *loop_var.write_lock().expect("`Dynamic`") = value; } else { - *index_var = index_value; + *loop_var = value; } } #[cfg(feature = "no_closure")] { - *scope.get_mut_by_index(counter_index) = index_value; + *scope.get_mut_by_index(index) = value; + } + + #[cfg(not(feature = "unchecked"))] + if let Err(err) = self + .inc_operations(&mut global.num_operations, statements.position()) + { + loop_result = Err(err); + break; + } + + if statements.is_empty() { + continue; + } + + let result = self.eval_stmt_block( + scope, global, state, lib, this_ptr, statements, true, level, + ); + + match result { + Ok(_) => (), + Err(err) => match *err { + ERR::LoopBreak(false, _) => (), + ERR::LoopBreak(true, _) => break, + _ => { + loop_result = Err(err); + break; + } + }, } } - let value = iter_value.flatten(); + scope.rewind(orig_scope_len); - #[cfg(not(feature = "no_closure"))] - { - let loop_var = scope.get_mut_by_index(index); - if loop_var.is_shared() { - *loop_var.write_lock().expect("`Dynamic`") = value; - } else { - *loop_var = value; - } - } - #[cfg(feature = "no_closure")] - { - *scope.get_mut_by_index(index) = value; - } - - #[cfg(not(feature = "unchecked"))] - if let Err(err) = - self.inc_operations(&mut global.num_operations, statements.position()) - { - loop_result = Err(err); - break; - } - - if statements.is_empty() { - continue; - } - - let result = self.eval_stmt_block( - scope, global, state, lib, this_ptr, statements, true, level, - ); - - match result { - Ok(_) => (), - Err(err) => match *err { - ERR::LoopBreak(false, _) => (), - ERR::LoopBreak(true, _) => break, - _ => { - loop_result = Err(err); - break; - } - }, - } + loop_result + } else { + Err(ERR::ErrorFor(expr.position()).into()) } - - scope.rewind(orig_scope_len); - - loop_result } else { - Err(ERR::ErrorFor(expr.position()).into()) + iter_result } } @@ -670,12 +759,8 @@ impl Engine { // Throw value Stmt::Return(options, Some(expr), pos) if options.contains(AST_OPTION_BREAK_OUT) => { - Err(ERR::ErrorRuntime( - self.eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .flatten(), - *pos, - ) - .into()) + self.eval_expr(scope, global, state, lib, this_ptr, expr, level) + .and_then(|v| Err(ERR::ErrorRuntime(v.flatten(), *pos).into())) } // Empty throw @@ -684,12 +769,9 @@ impl Engine { } // Return value - Stmt::Return(_, Some(expr), pos) => Err(ERR::Return( - self.eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .flatten(), - *pos, - ) - .into()), + Stmt::Return(_, Some(expr), pos) => self + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .and_then(|v| Err(ERR::Return(v.flatten(), *pos).into())), // Empty return Stmt::Return(_, None, pos) => Err(ERR::Return(Dynamic::UNIT, *pos).into()), @@ -704,40 +786,44 @@ impl Engine { }; let export = options.contains(AST_OPTION_PUBLIC); - let value = self - .eval_expr(scope, global, state, lib, this_ptr, expr, level)? - .flatten(); + let value_result = self + .eval_expr(scope, global, state, lib, this_ptr, expr, level) + .map(Dynamic::flatten); - let _alias = if !rewind_scope { - #[cfg(not(feature = "no_function"))] - #[cfg(not(feature = "no_module"))] - if state.scope_level == 0 - && entry_type == AccessMode::ReadOnly - && lib.iter().any(|&m| !m.is_empty()) - { - // Add a global constant if at top level and there are functions - global.set_constant(var_name.clone(), value.clone()); - } + if let Ok(value) = value_result { + let _alias = if !rewind_scope { + #[cfg(not(feature = "no_function"))] + #[cfg(not(feature = "no_module"))] + if state.scope_level == 0 + && entry_type == AccessMode::ReadOnly + && lib.iter().any(|&m| !m.is_empty()) + { + // Add a global constant if at top level and there are functions + global.set_constant(var_name.clone(), value.clone()); + } - if export { - Some(var_name) + if export { + Some(var_name) + } else { + None + } + } else if export { + unreachable!("exported variable not on global level"); } else { None + }; + + scope.push_dynamic_value(var_name.clone(), entry_type, value); + + #[cfg(not(feature = "no_module"))] + if let Some(alias) = _alias { + scope.add_entry_alias(scope.len() - 1, alias.clone()); } - } else if export { - unreachable!("exported variable not on global level"); + + Ok(Dynamic::UNIT) } else { - None - }; - - scope.push_dynamic_value(var_name.clone(), entry_type, value); - - #[cfg(not(feature = "no_module"))] - if let Some(alias) = _alias { - scope.add_entry_alias(scope.len() - 1, alias.clone()); + value_result } - - Ok(Dynamic::UNIT) } // Import statement @@ -749,10 +835,18 @@ impl Engine { return Err(ERR::ErrorTooManyModules(*_pos).into()); } - if let Some(path) = self - .eval_expr(scope, global, state, lib, this_ptr, &expr, level)? - .try_cast::() - { + let path_result = self + .eval_expr(scope, global, state, lib, this_ptr, &expr, level) + .and_then(|v| { + v.try_cast::().ok_or_else(|| { + self.make_type_mismatch_err::( + "", + expr.position(), + ) + }) + }); + + if let Ok(path) = path_result { use crate::ModuleResolver; let source = match global.source.as_str() { @@ -761,7 +855,7 @@ impl Engine { }; let path_pos = expr.position(); - let module = global + let module_result = global .embedded_module_resolver .as_ref() .and_then(|r| match r.resolve(self, source, &path, path_pos) { @@ -775,32 +869,36 @@ impl Engine { }) .unwrap_or_else(|| { Err(ERR::ErrorModuleNotFound(path.to_string(), path_pos).into()) - })?; + }); - if let Some(name) = export.as_ref().map(|x| x.name.clone()) { - if !module.is_indexed() { - // Index the module (making a clone copy if necessary) if it is not indexed - let mut module = crate::func::native::shared_take_or_clone(module); - module.build_index(); - global.push_import(name, module); - } else { - global.push_import(name, module); + if let Ok(module) = module_result { + if let Some(name) = export.as_ref().map(|x| x.name.clone()) { + if !module.is_indexed() { + // Index the module (making a clone copy if necessary) if it is not indexed + let mut module = crate::func::native::shared_take_or_clone(module); + module.build_index(); + global.push_import(name, module); + } else { + global.push_import(name, module); + } } + + global.num_modules_loaded += 1; + + Ok(Dynamic::UNIT) + } else { + module_result.map(|_| Dynamic::UNIT) } - - global.num_modules_loaded += 1; - - Ok(Dynamic::UNIT) } else { - Err(self.make_type_mismatch_err::("", expr.position())) + path_result.map(|_| Dynamic::UNIT) } } // Export statement #[cfg(not(feature = "no_module"))] Stmt::Export(list, _) => { - list.iter().try_for_each( - |(Ident { name, pos, .. }, Ident { name: rename, .. })| { + list.iter() + .try_for_each(|(Ident { name, pos, .. }, Ident { name: rename, .. })| { // Mark scope variables as public if let Some((index, _)) = scope.get_index(name) { scope.add_entry_alias( @@ -811,9 +909,8 @@ impl Engine { } else { Err(ERR::ErrorVariableNotFound(name.to_string(), *pos).into()) } - }, - )?; - Ok(Dynamic::UNIT) + }) + .map(|_| Dynamic::UNIT) } // Share statement @@ -831,6 +928,11 @@ impl Engine { } _ => unreachable!("statement cannot be evaluated: {:?}", stmt), - } + }; + + #[cfg(feature = "debugging")] + global.debugger.activate(reset_debugger_command); + + return result; } } diff --git a/src/func/call.rs b/src/func/call.rs index b20a34f5..6c934b88 100644 --- a/src/func/call.rs +++ b/src/func/call.rs @@ -889,8 +889,20 @@ impl Engine { ) -> RhaiResultOf<(Dynamic, Position)> { Ok(( if let Expr::Stack(slot, _) = arg_expr { + #[cfg(feature = "debugging")] + let active = + self.run_debugger(scope, global, state, lib, this_ptr, arg_expr.into(), level); + #[cfg(feature = "debugging")] + global.debugger.activate(active); + constants[*slot].clone() } else if let Some(value) = arg_expr.get_literal_value() { + #[cfg(feature = "debugging")] + let active = + self.run_debugger(scope, global, state, lib, this_ptr, arg_expr.into(), level); + #[cfg(feature = "debugging")] + global.debugger.activate(active); + value } else { self.eval_expr(scope, global, state, lib, this_ptr, arg_expr, level)? @@ -1133,9 +1145,17 @@ impl Engine { // convert to method-call style in order to leverage potential &mut first argument and // avoid cloning the value if curry.is_empty() && first_arg.map_or(false, |expr| expr.is_variable_access(false)) { - // func(x, ...) -> x.func(...) let first_expr = first_arg.unwrap(); + #[cfg(feature = "debugging")] + { + let node = first_expr.into(); + let active = + self.run_debugger(scope, global, state, lib, this_ptr, node, level); + global.debugger.activate(active); + } + + // func(x, ...) -> x.func(...) a_expr.iter().try_for_each(|expr| { self.get_arg_value(scope, global, state, lib, this_ptr, level, expr, constants) .map(|(value, _)| arg_values.push(value.flatten())) @@ -1215,6 +1235,14 @@ impl Engine { // If so, convert to method-call style in order to leverage potential // &mut first argument and avoid cloning the value if !args_expr.is_empty() && args_expr[0].is_variable_access(true) { + #[cfg(feature = "debugging")] + { + let node = (&args_expr[0]).into(); + let active = + self.run_debugger(scope, global, state, lib, this_ptr, node, level); + global.debugger.activate(active); + } + // func(x, ...) -> x.func(...) arg_values.push(Dynamic::UNIT); diff --git a/src/func/native.rs b/src/func/native.rs index 17b93eac..3f83c742 100644 --- a/src/func/native.rs +++ b/src/func/native.rs @@ -301,10 +301,7 @@ impl<'a> NativeCallContext<'a> { self.engine() .exec_fn_call( - &mut self - .global - .cloned() - .unwrap_or_else(|| GlobalRuntimeState::new()), + &mut self.global.cloned().unwrap_or_else(GlobalRuntimeState::new), &mut EvalState::new(), self.lib, fn_name, diff --git a/src/func/script.rs b/src/func/script.rs index 396dfedf..48ca4cf7 100644 --- a/src/func/script.rs +++ b/src/func/script.rs @@ -70,7 +70,9 @@ impl Engine { } let orig_scope_len = scope.len(); - let orig_mods_len = global.num_imports(); + let orig_imports_len = global.num_imports(); + #[cfg(feature = "debugging")] + let orig_call_stack_len = global.debugger.call_stack_len(); // Put arguments into scope as variables scope.extend(fn_def.params.iter().cloned().zip(args.into_iter().map(|v| { @@ -78,6 +80,19 @@ impl Engine { mem::take(*v) }))); + // Push a new call stack frame + #[cfg(feature = "debugging")] + global.debugger.push_call_stack_frame( + fn_def.name.clone(), + scope + .iter() + .skip(orig_scope_len) + .map(|(_, _, v)| v.clone()) + .collect(), + global.source.clone(), + pos, + ); + // Merge in encapsulated environment, if any let mut lib_merged = StaticVec::with_capacity(lib.len() + 1); let orig_fn_resolution_caches_len = state.fn_resolution_caches_len(); @@ -144,11 +159,15 @@ impl Engine { // Remove arguments only, leaving new variables in the scope scope.remove_range(orig_scope_len, args.len()) } - global.truncate_imports(orig_mods_len); + global.truncate_imports(orig_imports_len); // Restore state state.rewind_fn_resolution_caches(orig_fn_resolution_caches_len); + // Pop the call stack + #[cfg(feature = "debugging")] + global.debugger.rewind_call_stack(orig_call_stack_len); + result } diff --git a/src/lib.rs b/src/lib.rs index df685645..3802c1b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -156,6 +156,13 @@ pub use types::{ Dynamic, EvalAltResult, FnPtr, ImmutableString, LexError, ParseError, ParseErrorType, Scope, }; +/// _(debugging)_ Module containing types for debugging. +/// Exported under the `debugging` feature only. +#[cfg(feature = "debugging")] +pub mod debugger { + pub use super::eval::{BreakPoint, Debugger, DebuggerCommand}; +} + /// An identifier in Rhai. [`SmartString`](https://crates.io/crates/smartstring) is used because most /// identifiers are ASCII and short, fewer than 23 characters, so they can be stored inline. #[cfg(not(feature = "internals"))] diff --git a/src/module/mod.rs b/src/module/mod.rs index 7e79214e..155b89b9 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -151,7 +151,7 @@ impl FuncInfo { ty => ty.into(), } } - /// Generate a signature of the function. + /// _(metadata)_ Generate a signature of the function. /// Exported under the `metadata` feature only. #[cfg(feature = "metadata")] #[must_use] @@ -462,7 +462,7 @@ impl Module { self.indexed } - /// Generate signatures for all the non-private functions in the [`Module`]. + /// _(metadata)_ Generate signatures for all the non-private functions in the [`Module`]. /// Exported under the `metadata` feature only. #[cfg(feature = "metadata")] #[inline] @@ -759,8 +759,7 @@ impl Module { self } - /// _(metadata)_ Update the metadata (parameter names/types, return type and doc-comments) of a - /// registered function. + /// _(metadata)_ Update the metadata (parameter names/types, return type and doc-comments) of a registered function. /// Exported under the `metadata` feature only. /// /// The [`u64`] hash is returned by the [`set_native_fn`][Module::set_native_fn] call. @@ -1665,7 +1664,11 @@ impl Module { ) -> RhaiResultOf { let mut scope = scope; let mut global = crate::eval::GlobalRuntimeState::new(); - let orig_mods_len = global.num_imports(); + + #[cfg(feature = "debugging")] + global.debugger.activate(engine.debugger.is_some()); + + let orig_imports_len = global.num_imports(); // Run the script engine.eval_ast_with_scope_raw(&mut scope, &mut global, &ast, 0)?; @@ -1698,7 +1701,7 @@ impl Module { #[cfg(not(feature = "no_function"))] let mut func_global = None; - global.into_iter().skip(orig_mods_len).for_each(|kv| { + global.into_iter().skip(orig_imports_len).for_each(|kv| { #[cfg(not(feature = "no_function"))] if func_global.is_none() { func_global = Some(StaticVec::new()); @@ -1914,8 +1917,8 @@ impl Module { } } -/// _(internals)_ A chain of [module][Module] names to namespace-qualify a variable or function -/// call. Exported under the `internals` feature only. +/// _(internals)_ A chain of [module][Module] names to namespace-qualify a variable or function call. +/// Exported under the `internals` feature only. /// /// A [`u64`] offset to the current [stack of imported modules][crate::GlobalRuntimeState] is /// cached for quick search purposes. diff --git a/src/parser.rs b/src/parser.rs index 9bfc84f3..c7895361 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -63,7 +63,7 @@ pub struct ParseState<'e> { pub allow_capture: bool, /// Encapsulates a local stack with imported [module][crate::Module] names. #[cfg(not(feature = "no_module"))] - pub modules: StaticVec, + pub imports: StaticVec, /// Maximum levels of expression nesting. #[cfg(not(feature = "unchecked"))] pub max_expr_depth: Option, @@ -94,7 +94,7 @@ impl<'e> ParseState<'e> { stack: StaticVec::new_const(), entry_stack_len: 0, #[cfg(not(feature = "no_module"))] - modules: StaticVec::new_const(), + imports: StaticVec::new_const(), } } @@ -158,7 +158,7 @@ impl<'e> ParseState<'e> { #[inline] #[must_use] pub fn find_module(&self, name: &str) -> Option { - self.modules + self.imports .iter() .rev() .enumerate() @@ -2564,7 +2564,7 @@ fn parse_import( // import expr as name ... let (name, pos) = parse_var_name(input)?; let name = state.get_identifier("", name); - state.modules.push(name.clone()); + state.imports.push(name.clone()); Ok(Stmt::Import( expr, @@ -2677,7 +2677,7 @@ fn parse_block( state.entry_stack_len = state.stack.len(); #[cfg(not(feature = "no_module"))] - let prev_mods_len = state.modules.len(); + let orig_imports_len = state.imports.len(); loop { // Terminated? @@ -2744,7 +2744,7 @@ fn parse_block( state.entry_stack_len = prev_entry_stack_len; #[cfg(not(feature = "no_module"))] - state.modules.truncate(prev_mods_len); + state.imports.truncate(orig_imports_len); Ok(Stmt::Block(statements.into_boxed_slice(), settings.pos)) }