use rhai::debugger::{BreakPoint, DebuggerCommand, DebuggerEvent}; use rhai::{Dynamic, Engine, EvalAltResult, ImmutableString, Position, Scope, INT}; use std::{ env, fs::File, io::{stdin, stdout, Read, Write}, path::Path, process::exit, }; /// Pretty-print source line. fn print_source(lines: &[String], pos: Position, offset: usize, window: (usize, usize)) { if pos.is_none() { // No position println!(); return; } let line = pos.line().unwrap() - 1; let start = if line >= window.0 { line - window.0 } else { 0 }; let end = usize::min(line + window.1, lines.len() - 1); let line_no_len = format!("{}", end).len(); // Print error position if start >= end { println!("{}: {}", start + 1, lines[start]); if let Some(pos) = pos.position() { println!("{0:>1$}", "^", pos + offset + line_no_len + 2); } } else { for n in start..=end { let marker = if n == line { "> " } else { " " }; println!( "{0}{1}{2:>3$}{5}│ {0}{4}{5}", if n == line { "\x1b[33m" } else { "" }, marker, n + 1, line_no_len, lines[n], if n == line { "\x1b[39m" } else { "" }, ); if n == line { if let Some(pos) = pos.position() { let shift = offset + line_no_len + marker.len() + 2; println!("{0:>1$}{2:>3$}", "│ ", shift, "\x1b[36m^\x1b[39m", pos + 10); } } } } } fn print_current_source( context: &mut rhai::EvalContext, source: Option<&str>, pos: Position, lines: &[String], window: (usize, usize), ) { let current_source = &mut *context .global_runtime_state_mut() .debugger .state_mut() .write_lock::() .unwrap(); let src = source.unwrap_or(""); if src != current_source { println!(">>> Source => {}", source.unwrap_or("main script")); *current_source = src.into(); } if !src.is_empty() { // Print just a line number for imported modules println!("{} @ {:?}", src, pos); } else { // Print the current source line print_source(lines, pos, 0, window); } } /// 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, h => print this help"); println!("quit, q, exit, kill => quit"); println!("scope => print the scope"); println!("print, p => print all variables de-duplicated"); println!("print/p this => print the 'this' pointer"); println!("print/p => print the current value of a variable"); #[cfg(not(feature = "no_module"))] println!("imports => print all imported modules"); println!("node => print the current AST node"); println!("list, l => print the current source line"); println!("list/l => print a source line"); println!("backtrace, bt => print the current call-stack"); println!("info break, i b => print all break-points"); println!("enable/en => enable a break-point"); println!("disable/dis => disable a break-point"); println!("delete, d => delete all break-points"); println!("delete/d => delete a break-point"); #[cfg(not(feature = "no_position"))] println!("break, b => set a new break-point at the current position"); #[cfg(not(feature = "no_position"))] println!("break/b => set a new break-point at a line number"); #[cfg(not(feature = "no_object"))] println!("break/b . => set a new break-point for a property access"); println!("break/b => set a new break-point for a function call"); println!( "break/b <#args> => set a new break-point for a function call with #args arguments" ); println!("throw => throw a runtime exception"); println!("throw => throw an exception with string data"); println!("throw <#> => throw an exception with numeric data"); println!("run, r => restart the script evaluation from beginning"); println!("step, s => go to the next expression, diving into functions"); println!("over, o => go to the next expression, skipping oer functions"); println!("next, n, => go to the next statement, skipping over functions"); println!("finish, f => continue until the end of the current function call"); println!("continue, c => continue normal execution"); println!(); } /// Display the current scope. fn print_scope(scope: &Scope, dedup: bool) { let flattened_clone; let scope = if dedup { flattened_clone = scope.clone_visible(); &flattened_clone } else { scope }; for (i, (name, constant, value)) in scope.iter_raw().enumerate() { #[cfg(not(feature = "no_closure"))] let value_is_shared = if value.is_shared() { " (shared)" } else { "" }; #[cfg(feature = "no_closure")] let value_is_shared = ""; if dedup { println!( "{}{}{} = {:?}", if constant { "const " } else { "" }, name, value_is_shared, *value.read_lock::().unwrap(), ); } else { println!( "[{}] {}{}{} = {:?}", i + 1, if constant { "const " } else { "" }, name, value_is_shared, *value.read_lock::().unwrap(), ); } } } // Load script to debug. fn load_script(engine: &Engine) -> (rhai::AST, String) { if let Some(filename) = env::args().skip(1).next() { let mut contents = String::new(); let filename = match Path::new(&filename).canonicalize() { Err(err) => { eprintln!( "\x1b[31mError script file path: {}\n{}\x1b[39m", 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!( "\x1b[31mError reading script file: {}\n{}\x1b[39m", filename.to_string_lossy(), err ); exit(1); } Ok(f) => f, }; if let Err(err) = f.read_to_string(&mut contents) { println!( "Error reading script file: {}\n{}", filename.to_string_lossy(), err ); exit(1); } let script = if contents.starts_with("#!") { // Skip shebang &contents[contents.find('\n').unwrap_or(0)..] } else { &contents[..] }; let 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()); (ast, contents) } else { eprintln!("\x1b[31mNo script file specified.\x1b[39m"); exit(1); } } // Main callback for debugging. fn debug_callback( context: &mut rhai::EvalContext, event: DebuggerEvent, node: rhai::ASTNode, source: Option<&str>, pos: Position, lines: &[String], ) -> Result> { // Check event 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 .call_stack() .last() .unwrap() .fn_name, r ) } DebuggerEvent::FunctionExitWithError(err) => { println!( "! Return from function call '{}' with error: {}", context .global_runtime_state() .debugger .call_stack() .last() .unwrap() .fn_name, err ) } } // Print current source line print_current_source(context, source, pos, lines, (0, 0)); // Read stdin for commands let mut input = String::new(); loop { print!("rhai-dbg> "); stdout().flush().expect("couldn't flush stdout"); input.clear(); match stdin().read_line(&mut input) { Ok(0) => break Ok(DebuggerCommand::Continue), Ok(_) => match input .trim() .split_whitespace() .collect::>() .as_slice() { ["help" | "h"] => print_debug_help(), ["exit" | "quit" | "q" | "kill", ..] => { println!("Script terminated. Bye!"); exit(0); } ["node"] => { if pos.is_none() { println!("{:?}", node); } else if let Some(source) = source { println!("{:?} {} @ {:?}", node, source, pos); } else { println!("{:?} @ {:?}", node, pos); } println!(); } ["list" | "l"] => print_current_source(context, source, pos, &lines, (3, 6)), ["list" | "l", n] if n.parse::().is_ok() => { let num = n.parse::().unwrap(); if num <= 0 || num > lines.len() { eprintln!("\x1b[31mInvalid line: {}\x1b[39m", num); } else { let pos = Position::new(num as u16, 0); print_current_source(context, source, pos, &lines, (3, 6)); } } ["continue" | "c"] => break Ok(DebuggerCommand::Continue), ["finish" | "f"] => break Ok(DebuggerCommand::FunctionExit), [] | ["step" | "s"] => break Ok(DebuggerCommand::StepInto), ["over" | "o"] => break Ok(DebuggerCommand::StepOver), ["next" | "n"] => break Ok(DebuggerCommand::Next), ["scope"] => print_scope(context.scope(), false), ["print" | "p", "this"] => { if let Some(value) = context.this_ptr() { println!("=> {:?}", value); } else { println!("'this' pointer is unbound."); } } ["print" | "p", var_name] => { if let Some(value) = context.scope().get_value::(var_name) { println!("=> {:?}", value); } else { eprintln!("Variable not found: {}", var_name); } } ["print" | "p"] => { print_scope(context.scope(), true); if let Some(value) = context.this_ptr() { println!("this = {:?}", value); } } #[cfg(not(feature = "no_module"))] ["imports"] => { for (i, (name, module)) in context .global_runtime_state() .scan_imports_raw() .enumerate() { println!( "[{}] {} = {}", i + 1, name, module.id().unwrap_or("") ); } println!(); } #[cfg(not(feature = "no_function"))] ["backtrace" | "bt"] => { for frame in context .global_runtime_state() .debugger .call_stack() .iter() .rev() { println!("{}", frame) } } ["info" | "i", "break" | "b"] => Iterator::for_each( context .global_runtime_state() .debugger .break_points() .iter() .enumerate(), |(i, bp)| match bp { #[cfg(not(feature = "no_position"))] rhai::debugger::BreakPoint::AtPosition { pos, .. } => { let line_num = format!("[{}] line ", i + 1); print!("{}", line_num); print_source(&lines, *pos, line_num.len(), (0, 0)); } _ => println!("[{}] {}", i + 1, bp), }, ), ["enable" | "en", n] => { if let Ok(n) = n.parse::() { let range = 1..=context .global_runtime_state_mut() .debugger .break_points() .len(); if range.contains(&n) { context .global_runtime_state_mut() .debugger .break_points_mut() .get_mut(n - 1) .unwrap() .enable(true); println!("Break-point #{} enabled.", n) } else { eprintln!("\x1b[31mInvalid break-point: {}\x1b[39m", n); } } else { eprintln!("\x1b[31mInvalid break-point: '{}'\x1b[39m", n); } } ["disable" | "dis", n] => { if let Ok(n) = n.parse::() { let range = 1..=context .global_runtime_state_mut() .debugger .break_points() .len(); if range.contains(&n) { context .global_runtime_state_mut() .debugger .break_points_mut() .get_mut(n - 1) .unwrap() .enable(false); println!("Break-point #{} disabled.", n) } else { eprintln!("\x1b[31mInvalid break-point: {}\x1b[39m", n); } } else { eprintln!("\x1b[31mInvalid break-point: '{}'\x1b[39m", n); } } ["delete" | "d", n] => { if let Ok(n) = n.parse::() { let range = 1..=context .global_runtime_state_mut() .debugger .break_points() .len(); if range.contains(&n) { context .global_runtime_state_mut() .debugger .break_points_mut() .remove(n - 1); println!("Break-point #{} deleted.", n) } else { eprintln!("\x1b[31mInvalid break-point: {}\x1b[39m", n); } } else { eprintln!("\x1b[31mInvalid break-point: '{}'\x1b[39m", n); } } ["delete" | "d"] => { context .global_runtime_state_mut() .debugger .break_points_mut() .clear(); println!("All break-points deleted."); } ["break" | "b", fn_name, args] => { if let Ok(args) = args.parse::() { let bp = rhai::debugger::BreakPoint::AtFunctionCall { name: fn_name.trim().into(), args, enabled: true, }; println!("Break-point added for {}", bp); context .global_runtime_state_mut() .debugger .break_points_mut() .push(bp); } else { eprintln!("\x1b[31mInvalid number of arguments: '{}'\x1b[39m", args); } } // Property name #[cfg(not(feature = "no_object"))] ["break" | "b", param] if param.starts_with('.') && param.len() > 1 => { let bp = rhai::debugger::BreakPoint::AtProperty { name: param[1..].into(), enabled: true, }; println!("Break-point added for {}", bp); context .global_runtime_state_mut() .debugger .break_points_mut() .push(bp); } // Numeric parameter #[cfg(not(feature = "no_position"))] ["break" | "b", param] if param.parse::().is_ok() => { let n = param.parse::().unwrap(); let range = if source.is_none() { 1..=lines.len() } else { 1..=(u16::MAX as usize) }; if range.contains(&n) { let bp = rhai::debugger::BreakPoint::AtPosition { source: source.unwrap_or("").into(), pos: Position::new(n as u16, 0), enabled: true, }; println!("Break-point added {}", bp); context .global_runtime_state_mut() .debugger .break_points_mut() .push(bp); } else { eprintln!("\x1b[31mInvalid line number: '{}'\x1b[39m", n); } } // Function name parameter ["break" | "b", param] => { let bp = rhai::debugger::BreakPoint::AtFunctionName { name: param.trim().into(), enabled: true, }; println!("Break-point added for {}", bp); context .global_runtime_state_mut() .debugger .break_points_mut() .push(bp); } #[cfg(not(feature = "no_position"))] ["break" | "b"] => { let bp = rhai::debugger::BreakPoint::AtPosition { source: source.unwrap_or("").into(), pos, enabled: true, }; println!("Break-point added {}", bp); context .global_runtime_state_mut() .debugger .break_points_mut() .push(bp); } ["throw"] => break Err(EvalAltResult::ErrorRuntime(Dynamic::UNIT, pos).into()), ["throw", num] if num.trim().parse::().is_ok() => { let value = num.trim().parse::().unwrap().into(); break Err(EvalAltResult::ErrorRuntime(value, pos).into()); } #[cfg(not(feature = "no_float"))] ["throw", num] if num.trim().parse::().is_ok() => { let value = num.trim().parse::().unwrap().into(); break Err(EvalAltResult::ErrorRuntime(value, pos).into()); } ["throw", ..] => { let msg = input.trim().splitn(2, ' ').skip(1).next().unwrap_or(""); break Err(EvalAltResult::ErrorRuntime(msg.trim().into(), pos).into()); } ["run" | "r"] => { println!("Restarting script..."); break Err(EvalAltResult::ErrorTerminated(Dynamic::UNIT, pos).into()); } _ => eprintln!( "\x1b[31mInvalid debugger command: '{}'\x1b[39m", input.trim() ), }, Err(err) => panic!("input error: {}", err), } } } 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(); #[cfg(not(feature = "no_optimize"))] engine.set_optimization_level(rhai::OptimizationLevel::None); let (ast, script) = load_script(&engine); // Hook up debugger let lines: Vec<_> = script.trim().split('\n').map(|s| s.to_string()).collect(); #[allow(deprecated)] engine.register_debugger( // Store the current source in the debugger state || "".into(), // Main debugging interface move |context, event, node, source, pos| { debug_callback(context, event, node, source, pos, &lines) }, ); // 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); } println!("Type 'help' for commands list."); println!(); // Evaluate while let Err(err) = engine.run_ast_with_scope(&mut Scope::new(), &ast) { match *err { // Loop back to restart EvalAltResult::ErrorTerminated(..) => (), // Break evaluation _ => { print_error(&script, *err); break; } } } }