use std::rc::Rc;
use miden_assembly_syntax::diagnostics::Report;
use rustyline::{DefaultEditor, error::ReadlineError};
use super::commands::ReplCommand;
use crate::{
config::DebuggerConfig,
debug::{Breakpoint, BreakpointType},
ui::state::State,
};
pub struct ReplSession {
state: State,
editor: DefaultEditor,
}
impl ReplSession {
pub fn new(config: Box<DebuggerConfig>) -> Result<Self, Report> {
let state = State::new(config)?;
let editor = DefaultEditor::new()
.map_err(|e| Report::msg(format!("failed to create editor: {e}")))?;
Ok(Self { state, editor })
}
pub fn run(&mut self) -> Result<(), Report> {
self.print_welcome();
self.print_location();
loop {
let prompt = self.make_prompt();
match self.editor.readline(&prompt) {
Ok(line) => {
let line = line.trim();
if line.is_empty() {
continue;
}
let _ = self.editor.add_history_entry(line);
match line.parse::<ReplCommand>() {
Ok(cmd) => {
if matches!(cmd, ReplCommand::Quit) {
println!("\x1b[36mGoodbye!\x1b[0m");
break;
}
if let Err(e) = self.execute_command(cmd) {
eprintln!("\x1b[31mError:\x1b[0m {e}");
}
}
Err(e) => {
eprintln!("\x1b[31mError:\x1b[0m {e}");
}
}
}
Err(ReadlineError::Interrupted) => {
println!("^C");
continue;
}
Err(ReadlineError::Eof) => {
println!("\x1b[36mGoodbye!\x1b[0m");
break;
}
Err(e) => {
eprintln!("\x1b[31mError reading line:\x1b[0m {e}");
break;
}
}
}
Ok(())
}
fn print_welcome(&self) {
println!("\x1b[1;36mMiden Debugger REPL\x1b[0m");
println!("Type \x1b[33mhelp\x1b[0m for available commands.");
println!();
}
fn make_prompt(&self) -> String {
let cycle = self.state.executor().cycle;
if self.state.executor().stopped {
if self.state.execution_failed().is_some() {
format!("\x1b[36m[\x1b[0mcycle {cycle} \x1b[1;31mERR\x1b[0m\x1b[36m]\x1b[0m > ")
} else {
format!("\x1b[36m[\x1b[0mcycle {cycle} \x1b[1;32mEND\x1b[0m\x1b[36m]\x1b[0m > ")
}
} else if self.state.stopped {
format!("\x1b[36m[\x1b[0mcycle {cycle} \x1b[1;33mSTOP\x1b[0m\x1b[36m]\x1b[0m > ")
} else {
format!("\x1b[36m[\x1b[0mcycle {cycle}\x1b[36m]\x1b[0m > ")
}
}
fn print_location(&self) {
let proc_name = self.state.current_procedure().unwrap_or_else(|| Rc::from("<unknown>"));
if let Some(resolved) = self.state.current_display_location() {
println!("at {} in {}", resolved, proc_name);
} else if self.state.executor().callstack.current_frame().is_some() {
println!("in {}", proc_name);
}
}
fn execute_command(&mut self, cmd: ReplCommand) -> Result<(), String> {
match cmd {
ReplCommand::Step => self.cmd_step(1),
ReplCommand::StepN(n) => self.cmd_step(n),
ReplCommand::Next => self.cmd_next(),
ReplCommand::NextLine => self.cmd_next_line(),
ReplCommand::Continue => self.cmd_continue(),
ReplCommand::Finish => self.cmd_finish(),
ReplCommand::Break(bp_type) => self.cmd_break(bp_type),
ReplCommand::Breakpoints => self.cmd_breakpoints(),
ReplCommand::Delete(id) => self.cmd_delete(id),
ReplCommand::Stack => self.cmd_stack(),
ReplCommand::Memory(expr) => self.cmd_memory(&expr),
ReplCommand::Locals => self.cmd_locals(),
ReplCommand::Vars(show_all) => self.cmd_vars(show_all),
ReplCommand::Where => self.cmd_where(),
ReplCommand::List => self.cmd_list(),
ReplCommand::Backtrace => self.cmd_backtrace(),
ReplCommand::Reload => self.cmd_reload(),
ReplCommand::Help => self.cmd_help(),
ReplCommand::Quit => unreachable!("quit handled in run loop"),
}
}
fn cmd_step(&mut self, n: usize) -> Result<(), String> {
if self.state.executor().stopped {
return Err("program has terminated, cannot step".into());
}
for _ in 0..n {
if self.state.executor().stopped {
break;
}
match self.state.executor_mut().step() {
Ok(_) => {}
Err(err) => {
let msg = format!("execution error: {err}");
self.state.set_execution_failed(err);
return Err(msg);
}
}
}
self.print_location();
Ok(())
}
fn cmd_next(&mut self) -> Result<(), String> {
if self.state.executor().stopped {
return Err("program has terminated, cannot continue".into());
}
self.state.create_breakpoint(BreakpointType::Next);
self.state.stopped = false;
self.run_until_stopped();
self.print_location();
Ok(())
}
fn cmd_next_line(&mut self) -> Result<(), String> {
if self.state.executor().stopped {
return Err("program has terminated, cannot continue".into());
}
self.state.create_breakpoint(BreakpointType::NextLine);
self.state.stopped = false;
self.run_until_stopped();
self.print_location();
Ok(())
}
fn cmd_continue(&mut self) -> Result<(), String> {
if self.state.executor().stopped {
return Err("program has terminated, cannot continue".into());
}
self.state.stopped = false;
self.run_until_stopped();
if self.state.executor().stopped {
if let Some(err) = self.state.execution_failed() {
println!("Program terminated with error: {}", err);
} else {
println!("Program terminated successfully");
}
} else {
self.print_location();
}
Ok(())
}
fn cmd_finish(&mut self) -> Result<(), String> {
if self.state.executor().stopped {
return Err("program has terminated, cannot continue".into());
}
self.state.create_breakpoint(BreakpointType::Finish);
self.state.stopped = false;
self.run_until_stopped();
self.print_location();
Ok(())
}
fn run_until_stopped(&mut self) {
let start_cycle = self.state.executor().cycle;
let start_asmop = self.state.executor().current_asmop.clone();
let start_proc = self.state.current_procedure();
let start_line_loc = self.state.current_display_location();
let mut previous_proc = self.state.current_procedure();
let mut pending_called_breakpoints = Vec::new();
let mut breakpoints: Vec<Breakpoint> = core::mem::take(&mut self.state.breakpoints);
self.state.breakpoints_hit.clear();
loop {
if self.state.executor().stopped {
self.state.stopped = true;
break;
}
let mut consume_most_recent_finish = false;
match self.state.executor_mut().step() {
Ok(Some(ref exited)) if exited.should_break_on_exit() => {
consume_most_recent_finish = true;
}
Ok(_) => {}
Err(err) => {
self.state.set_execution_failed(err);
self.state.stopped = true;
break;
}
}
if breakpoints.is_empty() {
continue;
}
let is_op_boundary = self.state.executor().current_asmop.is_some();
let loc = self.state.current_location();
let line_loc = self.state.current_display_location();
let proc = self.state.current_procedure();
let current_cycle = self.state.executor().cycle;
let cycles_stepped = current_cycle - start_cycle;
breakpoints.retain_mut(|bp| {
if let Some(n) = bp.cycles_to_skip(current_cycle) {
if cycles_stepped >= n {
let retained = !bp.is_one_shot();
if retained {
self.state.breakpoints_hit.push(bp.clone());
} else {
self.state.breakpoints_hit.push(core::mem::take(bp));
}
return retained;
}
return true;
}
if cycles_stepped > 0
&& is_op_boundary
&& matches!(&bp.ty, BreakpointType::Next)
&& self.state.executor().current_asmop != start_asmop
{
self.state.breakpoints_hit.push(core::mem::take(bp));
return false;
}
if cycles_stepped > 0
&& is_op_boundary
&& matches!(&bp.ty, BreakpointType::NextLine)
{
let has_source_context = start_line_loc.is_some() || line_loc.is_some();
let reached_next = if has_source_context {
State::is_next_source_line(
start_proc.as_deref(),
start_line_loc.as_ref(),
proc.as_deref(),
line_loc.as_ref(),
)
} else {
self.state.executor().current_asmop != start_asmop
};
if reached_next {
self.state.breakpoints_hit.push(core::mem::take(bp));
return false;
}
}
if let Some(loc) = loc.as_ref()
&& bp.should_break_at(loc)
{
let retained = !bp.is_one_shot();
if retained {
self.state.breakpoints_hit.push(bp.clone());
} else {
self.state.breakpoints_hit.push(core::mem::take(bp));
}
return retained;
}
if matches!(&bp.ty, BreakpointType::Called(_))
&& let Some(proc) = proc.as_deref()
{
let matched = bp.should_break_in(proc);
if !matched {
pending_called_breakpoints.retain(|id| *id != bp.id);
return true;
}
let was_matched = previous_proc
.as_deref()
.is_some_and(|previous| bp.should_break_in(previous));
let matched_at_start =
start_proc.as_deref().is_some_and(|start| bp.should_break_in(start));
let pending = pending_called_breakpoints.contains(&bp.id);
let entered_matching_proc = !was_matched && !matched_at_start;
if entered_matching_proc
&& self.state.executor().procedure_has_debug_vars(proc)
&& self.state.executor().last_debug_var_count == 0
{
if !pending {
pending_called_breakpoints.push(bp.id);
}
return true;
}
if entered_matching_proc
|| (pending && self.state.executor().last_debug_var_count > 0)
{
pending_called_breakpoints.retain(|id| *id != bp.id);
let retained = !bp.is_one_shot();
if retained {
self.state.breakpoints_hit.push(bp.clone());
} else {
self.state.breakpoints_hit.push(core::mem::take(bp));
}
return retained;
}
}
true
});
if consume_most_recent_finish
&& let Some(id) = breakpoints.iter().rev().find_map(|bp| {
if matches!(bp.ty, BreakpointType::Finish) {
Some(bp.id)
} else {
None
}
})
{
breakpoints.retain(|bp| bp.id != id);
self.state.stopped = true;
break;
}
if !self.state.breakpoints_hit.is_empty() {
self.state.stopped = true;
break;
}
previous_proc = proc;
}
self.state.breakpoints = breakpoints;
}
fn cmd_break(&mut self, bp_type: BreakpointType) -> Result<(), String> {
self.state.create_breakpoint(bp_type.clone());
let id = self.state.breakpoints.last().map(|bp| bp.id).unwrap_or(0);
println!("Breakpoint {} set: {}", id, format_bp_type(&bp_type));
Ok(())
}
fn cmd_breakpoints(&mut self) -> Result<(), String> {
if self.state.breakpoints.is_empty() {
println!("No breakpoints set");
return Ok(());
}
println!("Breakpoints:");
for bp in &self.state.breakpoints {
if !bp.is_internal() {
println!(" [{}] {}", bp.id, format_bp_type(&bp.ty));
}
}
Ok(())
}
fn cmd_delete(&mut self, id: Option<u8>) -> Result<(), String> {
match id {
Some(id) => {
let count_before = self.state.breakpoints.len();
self.state.breakpoints.retain(|bp| bp.id != id);
if self.state.breakpoints.len() < count_before {
println!("Deleted breakpoint {}", id);
} else {
return Err(format!("no breakpoint with id {}", id));
}
}
None => {
self.state.breakpoints.retain(|bp| bp.is_internal());
println!("Deleted all breakpoints");
}
}
Ok(())
}
fn cmd_stack(&mut self) -> Result<(), String> {
let stack = &self.state.executor().current_stack;
if stack.is_empty() {
println!("Stack is empty");
return Ok(());
}
println!("Operand Stack ({} elements):", stack.len());
for (i, elem) in stack.iter().enumerate() {
let val = elem.as_canonical_u64();
let marker = if i == 0 { ">" } else { " " };
println!(" {} [{}] {} (0x{:x})", marker, i, val, val);
}
Ok(())
}
fn cmd_memory(&mut self, expr: &crate::debug::ReadMemoryExpr) -> Result<(), String> {
let result = self.state.read_memory(expr)?;
println!("{}", result);
Ok(())
}
fn cmd_locals(&mut self) -> Result<(), String> {
let output = self.state.format_variables(false);
println!("{}", output);
Ok(())
}
fn cmd_vars(&mut self, show_all: bool) -> Result<(), String> {
let output = self.state.format_variables(show_all);
println!("{}", output);
Ok(())
}
fn cmd_where(&mut self) -> Result<(), String> {
if self.state.executor().callstack.current_frame().is_some() {
let proc_name = self.state.current_procedure().unwrap_or_else(|| Rc::from("<unknown>"));
if let Some(resolved) = self.state.current_display_location() {
println!(
"{}:{}:{} in {}",
resolved.source_file.uri().as_str(),
resolved.line,
resolved.col,
proc_name
);
} else {
println!("in {} (no source location available)", proc_name);
}
} else {
println!("No current frame");
}
Ok(())
}
fn cmd_list(&mut self) -> Result<(), String> {
if let Some(frame) = self.state.executor().callstack.current_frame() {
let recent = frame.recent();
if recent.is_empty() {
println!("No recent instructions");
return Ok(());
}
println!("Recent instructions:");
for (i, op) in recent.iter().enumerate() {
let marker = if i == recent.len() - 1 { ">" } else { " " };
println!(" {} {}", marker, op.display());
}
} else {
println!("No current frame");
}
Ok(())
}
fn cmd_backtrace(&mut self) -> Result<(), String> {
let frames = self.state.executor().callstack.frames();
if frames.is_empty() {
println!("No call stack");
return Ok(());
}
println!("Backtrace ({} frames):", frames.len());
for (i, frame) in frames.iter().rev().enumerate() {
let proc_name = frame.procedure("").unwrap_or_else(|| Rc::from("<unknown>"));
let loc_str = frame
.last_resolved(&*self.state.source_manager)
.map(|r| format!(" at {}", r))
.unwrap_or_default();
println!(" #{} {}{}", i, proc_name, loc_str);
}
Ok(())
}
fn cmd_reload(&mut self) -> Result<(), String> {
self.state.reload().map_err(|e| format!("reload failed: {e}"))?;
println!("Program reloaded");
self.print_location();
Ok(())
}
fn cmd_help(&mut self) -> Result<(), String> {
println!("{}", ReplCommand::help_text());
Ok(())
}
}
fn format_bp_type(ty: &BreakpointType) -> String {
match ty {
BreakpointType::Step => "next cycle".into(),
BreakpointType::StepN(n) => format!("after {} cycles", n),
BreakpointType::StepTo(c) => format!("at cycle {}", c),
BreakpointType::Next => "next instruction".into(),
BreakpointType::NextLine => "next source line".into(),
BreakpointType::Finish => "function return".into(),
BreakpointType::File(pat) => pat.as_str().to_string(),
BreakpointType::Line { pattern, line } => format!("{}:{}", pattern.as_str(), line),
BreakpointType::Opcode(op) => format!("opcode {:?}", op),
BreakpointType::Called(pat) => format!("call {}", pat.as_str()),
}
}