diff --git a/Cargo.lock b/Cargo.lock index c8dbfc5..25f2d76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1603,8 +1603,7 @@ dependencies = [ [[package]] name = "miden-ace-codegen" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66d3f9f8abd86405b76e5fb33425ca663a6c7db5bc3b93d1386f6aa817633f6" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "miden-constraint-compiler", "miden-core", @@ -1615,8 +1614,7 @@ dependencies = [ [[package]] name = "miden-air" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06523a8e7130f70e5974c40861e808085a34eec27df1d397fb767850425a44e1" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "miden-ace-codegen", "miden-core", @@ -1630,8 +1628,7 @@ dependencies = [ [[package]] name = "miden-assembly" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c580e0f6b4a1fe06aa1ebe5dfa8924ed58ab5a0d0afeb845fe0cbafd481917f" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "log", "miden-assembly-syntax", @@ -1647,8 +1644,7 @@ dependencies = [ [[package]] name = "miden-assembly-syntax" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2acfd54fda86d88c08e2d4b23538c4f6011327e21a9576de88be90dc95fa77b" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "log", "miden-assembly-syntax-cst", @@ -1668,8 +1664,7 @@ dependencies = [ [[package]] name = "miden-assembly-syntax-cst" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c983ea80cf3c747f39f61a9cffbf048f9107b7a5175b873d37527398019c6760" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "miden-debug-types", "miden-rowan", @@ -1680,8 +1675,7 @@ dependencies = [ [[package]] name = "miden-constraint-compiler" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc2de03e3ca664006f6a523810c1a5a3b4254bbda8bf78e5aa009e2502c4f5d" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "miden-core", "miden-crypto", @@ -1690,8 +1684,7 @@ dependencies = [ [[package]] name = "miden-core" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8394188e9b1fb828ad304d9984247b78660e53595f34880d8a403216a0e804e" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "derive_more", "log", @@ -1708,8 +1701,7 @@ dependencies = [ [[package]] name = "miden-crypto" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8bc266f7dc1f7069eb4f0786500241ad9b499d34cb5b8aa5b1a42fcb55e2b" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "blake3", "cc", @@ -1749,8 +1741,7 @@ dependencies = [ [[package]] name = "miden-crypto-derive" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "832fa19dfec7601a3e7ca007e55e16b38e333e553f233249e00417432409674d" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "quote", "syn 2.0.119", @@ -1832,8 +1823,7 @@ dependencies = [ [[package]] name = "miden-debug-types" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4556cf4e43be205707d04d9532dd302cd313162ee986160a37923b4a613a71c1" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "memchr", "miden-crypto", @@ -1852,8 +1842,7 @@ dependencies = [ [[package]] name = "miden-field" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d86dc6aae6776bfdd2c3d148d2c7c29c56510887056dc48aa41ffc87c2e6eae4" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "miden-serde-utils", "num-bigint 0.5.1", @@ -1880,8 +1869,7 @@ dependencies = [ [[package]] name = "miden-lifted-air" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb6a87c95e6bb384ec290d29daf0d7d3fc5104d0ed9b9ff0c4a311fab65b73" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "p3-air", "p3-challenger", @@ -1894,8 +1882,7 @@ dependencies = [ [[package]] name = "miden-lifted-stark" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17c540c72c6a0adfa7e473f98bbf30b31ebd726026a76266eb77a5d00961931" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "miden-lifted-air", "miden-stark-transcript", @@ -1917,8 +1904,7 @@ dependencies = [ [[package]] name = "miden-mast-package" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffa6fd0d84e932cbd8c51fd2e71c4f48dc4edea1dbef9259ff440e4212341e7d" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "hashbrown 0.17.1", "log", @@ -1971,8 +1957,7 @@ dependencies = [ [[package]] name = "miden-package-registry" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe36161cad2d5945b81c21c55a71bcb5d36445183556908eb7196a2465e1852" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "miden-assembly-syntax", "miden-core", @@ -1987,8 +1972,7 @@ dependencies = [ [[package]] name = "miden-precompiles" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee334f0a36c1a24d041323898b087da3b9bf2aa3b520d0d44ebc2ed07f5f617" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "miden-core", "miden-crypto", @@ -1997,8 +1981,7 @@ dependencies = [ [[package]] name = "miden-processor" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b1429a0c3ac67db377fa5656937888ad89c6f7d8e6cb51afa549066d432d60" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "hashbrown 0.17.1", "itertools 0.15.0", @@ -2018,8 +2001,7 @@ dependencies = [ [[package]] name = "miden-project" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aff0af79739a77d0128a307811cf6d1d134d14637fd2295d44eb7c4af28378d" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "miden-assembly-syntax", "miden-core", @@ -2045,8 +2027,7 @@ dependencies = [ [[package]] name = "miden-serde-utils" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8bc2e1ffd6d9c0a445259f3e32f02b79af2776d34d7dc5dbd6e39e19bbebdce" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "p3-field", "p3-goldilocks", @@ -2056,8 +2037,7 @@ dependencies = [ [[package]] name = "miden-stark-transcript" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc72d9321645757904aebc53e2ee78de46f992a961a04cd15a996137fedd0ce" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "p3-challenger", "p3-field", @@ -2068,8 +2048,7 @@ dependencies = [ [[package]] name = "miden-stateful-hasher" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99c1612fbad6502deb79a34bd852a53b30cbcae92fbf20a2f2c220a708e1aec7" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "p3-field", "p3-symmetric", @@ -2078,8 +2057,7 @@ dependencies = [ [[package]] name = "miden-utils-core-derive" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21bd1f2a35b3e8e48b391c51bd5d4d3d080066d9f0cd8a1567be477fd6acbed" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "proc-macro2", "quote", @@ -2089,8 +2067,7 @@ dependencies = [ [[package]] name = "miden-utils-diagnostics" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9927aebed020f53666d1daf01b834e134ce80ced02cef6f6b82c3a2f855fb85c" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "miden-debug-types", "miden-miette", @@ -2100,8 +2077,7 @@ dependencies = [ [[package]] name = "miden-utils-indexing" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e1ce02f96261808a50f86088c83bc9eef809205bbc51f76f10432bb9a5e1337" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "miden-serde-utils", "proptest", @@ -2112,8 +2088,7 @@ dependencies = [ [[package]] name = "miden-utils-sync" version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579fdffa79eb4e9d90f53ee6cc29d7122b50b095c388a0f4ca1e1f6e04df7faf" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "lock_api", "loom", @@ -2124,8 +2099,7 @@ dependencies = [ [[package]] name = "midenc-hir-type" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e03aa1e30a8eec3e08eba9a1fd17c7c4462f0d49dfbd64cb65193b68ee14cdcc" +source = "git+https://github.com/walnuthq/miden-vm?branch=feat%2Finline-calls#28d4f758d56f98dbbb493b665661e1343c0972aa" dependencies = [ "miden-formatting", "miden-serde-utils", diff --git a/Cargo.toml b/Cargo.toml index 8b6b31f..7014e28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,24 +47,14 @@ codegen-units = 1 codegen-units = 1 [patch.crates-io] -#miden-assembly = { path = "../miden-vm/crates/assembly" } -#miden-assembly-syntax = { path = "../miden-vm/crates/assembly-syntax" } -#miden-core = { path = "../miden-vm/core" } -#miden-debug-types = { path = "../miden-vm/crates/debug-types" } -#miden-processor = { path = "../miden-vm/processor" } -#miden-mast-package = { path = "../miden-vm/crates/mast-package" } -#miden-package-registry = { path = "../miden-vm/crates/package-registry" } -#miden-project = { path = "../miden-vm/crates/project" } -#miden-assembly = { git = "https://github.com/0xMiden/miden-vm", branch = "bitwalker/package-debug-info-consolidation" } -#miden-assembly-syntax = { git = "https://github.com/0xMiden/miden-vm", branch = "bitwalker/package-debug-info-consolidation" } -#miden-core = { git = "https://github.com/0xMiden/miden-vm", branch = "bitwalker/package-debug-info-consolidation" } -#miden-debug-types = { git = "https://github.com/0xMiden/miden-vm", branch = "bitwalker/package-debug-info-consolidation" } -#miden-processor = { git = "https://github.com/0xMiden/miden-vm", branch = "bitwalker/package-debug-info-consolidation" } -#miden-mast-package = { git = "https://github.com/0xMiden/miden-vm", branch = "bitwalker/package-debug-info-consolidation" } -#miden-package-registry = { git = "https://github.com/0xMiden/miden-vm", branch = "bitwalker/package-debug-info-consolidation" } -#miden-project = { git = "https://github.com/0xMiden/miden-vm", branch = "bitwalker/package-debug-info-consolidation" } - - +miden-assembly = { git = "https://github.com/walnuthq/miden-vm", branch = "feat/inline-calls" } +miden-assembly-syntax = { git = "https://github.com/walnuthq/miden-vm", branch = "feat/inline-calls" } +miden-core = { git = "https://github.com/walnuthq/miden-vm", branch = "feat/inline-calls" } +miden-debug-types = { git = "https://github.com/walnuthq/miden-vm", branch = "feat/inline-calls" } +miden-processor = { git = "https://github.com/walnuthq/miden-vm", branch = "feat/inline-calls" } +miden-mast-package = { git = "https://github.com/walnuthq/miden-vm", branch = "feat/inline-calls" } +miden-package-registry = { git = "https://github.com/walnuthq/miden-vm", branch = "feat/inline-calls" } +miden-project = { git = "https://github.com/walnuthq/miden-vm", branch = "feat/inline-calls" } [package] name = "miden-debug" diff --git a/crates/engine/src/debug/mod.rs b/crates/engine/src/debug/mod.rs index 0e6244f..369e4d5 100644 --- a/crates/engine/src/debug/mod.rs +++ b/crates/engine/src/debug/mod.rs @@ -9,8 +9,9 @@ pub use self::{ memory::{FormatType, MemoryMode, ReadMemoryExpr}, native_ptr::NativePtr, stacktrace::{ - CallFrame, CallStack, ControlFlowOp, CurrentFrame, OpDetail, ResolvedLocation, StackTrace, - StepInfo, is_internal_source_uri, resolve_location_from_filesystem, + CallFrame, CallStack, ControlFlowOp, CurrentFrame, InlineCallFrame, LogicalFrameKind, + LogicalStackFrame, OpDetail, ResolvedLocation, StackTrace, StepInfo, + inline_frames_for_operation, is_internal_source_uri, resolve_location_from_filesystem, resolve_source_file_for_location, resolve_source_path, }, variables::{ diff --git a/crates/engine/src/debug/stacktrace.rs b/crates/engine/src/debug/stacktrace.rs index 4ebd782..5c49460 100644 --- a/crates/engine/src/debug/stacktrace.rs +++ b/crates/engine/src/debug/stacktrace.rs @@ -9,6 +9,7 @@ use std::{ use miden_core::operations::AssemblyOp; use miden_debug_types::{Location, SourceFile, SourceManager, SourceManagerExt, SourceSpan, Uri}; +use miden_mast_package::debug_info::{DebugSourceNodeId, PackageDebugInfo}; use miden_processor::{ContextId, operation::Operation, trace::RowIndex}; use crate::Event; @@ -28,6 +29,91 @@ pub struct StepInfo<'a> { pub asmop: Option<&'a AssemblyOp>, pub clk: RowIndex, pub ctx: ContextId, + pub inline_frames: &'a [InlineCallFrame], +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InlineCallFrame { + name: Arc, + call_site: Location, +} + +impl InlineCallFrame { + pub fn name(&self) -> &str { + &self.name + } + + pub fn call_site(&self) -> &Location { + &self.call_site + } + + pub fn display_name(&self) -> String { + demangle(&self.name) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LogicalFrameKind { + Physical, + Inline, +} + +#[derive(Debug, Clone)] +enum LogicalFrameLocation { + Assembly(Location), +} + +#[derive(Debug, Clone)] +pub struct LogicalStackFrame { + name: Arc, + kind: LogicalFrameKind, + location: Option, + physical_index: usize, +} + +impl LogicalStackFrame { + pub fn name(&self) -> &str { + &self.name + } + + pub fn kind(&self) -> LogicalFrameKind { + self.kind + } + + pub fn physical_index(&self) -> usize { + self.physical_index + } + + pub fn display_name(&self) -> String { + match self.kind { + LogicalFrameKind::Physical => self.name.to_string(), + LogicalFrameKind::Inline => format!("[inlined] {}", self.name), + } + } + + pub fn resolved(&self, source_manager: &dyn SourceManager) -> Option { + match self.location.as_ref()? { + LogicalFrameLocation::Assembly(location) => { + resolve_assembly_location(source_manager, location) + } + } + } +} + +pub fn inline_frames_for_operation( + debug_info: &PackageDebugInfo, + source_node: DebugSourceNodeId, + op_idx: u32, +) -> Vec { + debug_info + .inline_calls_for_operation(source_node, op_idx) + .filter_map(|row| { + let function = debug_info.get_function(row.callee_idx)?; + let name = debug_info.get_string(function.name_idx)?; + let call_site = debug_info.get_location(row.loc_idx)?; + Some(InlineCallFrame { name, call_site }) + }) + .collect() } #[derive(Debug, Clone)] @@ -83,6 +169,43 @@ impl CallStack { self.frames.as_slice() } + pub fn logical_frames(&self, strip_prefix: &str) -> Vec { + let mut logical = Vec::new(); + for (physical_index, frame) in self.frames.iter().enumerate() { + let current_location = + frame.last_location().cloned().map(LogicalFrameLocation::Assembly); + let location = frame + .inline_frames + .last() + .map(|inline| LogicalFrameLocation::Assembly(inline.call_site.clone())) + .or_else(|| current_location.clone()); + logical.push(LogicalStackFrame { + name: frame.procedure(strip_prefix).unwrap_or_else(|| Arc::from("")), + kind: LogicalFrameKind::Physical, + location, + physical_index, + }); + + for inline_index in (0..frame.inline_frames.len()).rev() { + let inline = &frame.inline_frames[inline_index]; + let location = if inline_index == 0 { + current_location.clone() + } else { + Some(LogicalFrameLocation::Assembly( + frame.inline_frames[inline_index - 1].call_site.clone(), + )) + }; + logical.push(LogicalStackFrame { + name: Arc::from(inline.display_name().into_boxed_str()), + kind: LogicalFrameKind::Inline, + location, + physical_index, + }); + } + } + logical + } + /// Updates the call stack from `info` /// /// Returns the call frame exited this cycle, if any @@ -98,7 +221,8 @@ impl CallStack { }; log::trace!("handling {:?}/{:?} at cycle {}: {:?}", info.control, info.op, info.clk, event); let is_frame_start = event.as_ref().is_some_and(|event| event.is_frame_start()); - let popped_frame = self.handle_event(event, procedure.clone(), info.op, info.asmop); + let popped_frame = + self.handle_event(event, procedure.clone(), info.op, info.asmop, info.inline_frames); let is_frame_end = popped_frame.is_some(); match info.control { @@ -164,6 +288,7 @@ impl CallStack { } let current_frame = self.frames.last_mut().unwrap(); + current_frame.inline_frames = info.inline_frames.to_vec(); // Does the current frame have a procedure context/location? Use the one from this op if // so @@ -210,6 +335,7 @@ impl CallStack { procedure: Option>, op: Option, asmop: Option<&AssemblyOp>, + inline_frames: &[InlineCallFrame], ) -> Option { // Do we need to handle any frame events? match event? { @@ -220,6 +346,7 @@ impl CallStack { } // The event is emitted at the start of the callee. let mut frame = CallFrame::new(procedure); + frame.inline_frames = inline_frames.to_vec(); if let Some(op) = op { frame.push(op, 0, asmop); } @@ -240,6 +367,7 @@ pub struct CallFrame { context: VecDeque, display_name: std::cell::OnceCell>, finishing: bool, + inline_frames: Vec, } impl CallFrame { pub fn new(procedure: Option>) -> Self { @@ -248,6 +376,7 @@ impl CallFrame { context: Default::default(), display_name: Default::default(), finishing: false, + inline_frames: Vec::new(), } } @@ -273,6 +402,7 @@ impl CallFrame { context, display_name: Default::default(), finishing: false, + inline_frames: Vec::new(), } } @@ -537,9 +667,9 @@ impl<'a> StackTrace<'a> { recent: &'a VecDeque, source_manager: &'a dyn SourceManager, ) -> Self { - let current_frame = callstack.current_frame().map(|frame| { - let location = frame.last_resolved(source_manager).cloned(); - let procedure = frame.procedure(""); + let current_frame = callstack.logical_frames("").last().map(|frame| { + let location = frame.resolved(source_manager); + let procedure = Some(Arc::from(frame.display_name().into_boxed_str())); CurrentFrame { procedure, location, @@ -562,29 +692,30 @@ impl fmt::Display for StackTrace<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use std::fmt::Write; - let num_frames = self.callstack.frames.len(); + let frames = self.callstack.logical_frames(""); + let num_frames = frames.len(); writeln!(f, "\nStack Trace:")?; - for (i, frame) in self.callstack.frames.iter().enumerate() { + for (i, frame) in frames.iter().enumerate() { let is_top = i + 1 == num_frames; - let name = frame.procedure(""); - let name = name.as_deref().unwrap_or(""); + let name = frame.display_name(); if is_top { write!(f, " `-> {name}")?; } else { write!(f, " |-> {name}")?; } - if let Some(resolved) = frame.last_resolved(self.source_manager) { + if let Some(resolved) = frame.resolved(self.source_manager) { write!(f, " in {resolved}")?; } else { write!(f, " in ")?; } if is_top { + let physical_frame = &self.callstack.frames[frame.physical_index()]; // Print op context - let context_size = frame.context.len(); + let context_size = physical_frame.context.len(); writeln!(f, ":\n\nLast {context_size} Instructions (of current frame):")?; - for (i, op) in frame.context.iter().enumerate() { + for (i, op) in physical_frame.context.iter().enumerate() { let is_last = i + 1 == context_size; if let Some(callee) = op.callee("") { write!(f, " | exec.{callee}")?; @@ -618,6 +749,21 @@ impl fmt::Display for StackTrace<'_> { } } +fn resolve_assembly_location( + source_manager: &dyn SourceManager, + location: &Location, +) -> Option { + let source_file = resolve_source_file_for_location(source_manager, location)?; + let span = SourceSpan::new(source_file.id(), location.start..location.end); + let file_line_col = source_file.location(span); + Some(ResolvedLocation { + source_file, + line: file_line_col.line.to_u32(), + col: file_line_col.column.to_u32(), + span, + }) +} + fn demangle(name: &str) -> String { let mut input = name.as_bytes(); let mut demangled = Vec::with_capacity(input.len() * 2); @@ -661,6 +807,65 @@ mod tests { fs::remove_dir_all(path.parent().unwrap().parent().unwrap()).ok(); } + #[test] + fn logical_frames_place_innermost_inline_frame_on_top() { + let path = test_source_path("inline-frames"); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + let source = "physical call\nouter call\ninner body\n"; + fs::write(&path, source).unwrap(); + let uri = Uri::from(path.display().to_string()); + + let mut frame = CallFrame::new(Some(Arc::from("crate::physical"))); + let outer_start = "physical call\n".len() as u32; + frame.inline_frames = vec![ + InlineCallFrame { + name: Arc::from("crate::inner"), + call_site: Location::new( + uri.clone(), + ByteIndex::new(outer_start), + ByteIndex::new(outer_start + "outer call".len() as u32), + ), + }, + InlineCallFrame { + name: Arc::from("crate::outer"), + call_site: Location::new( + uri.clone(), + ByteIndex::new(0), + ByteIndex::new("physical call".len() as u32), + ), + }, + ]; + let inner_start = "physical call\nouter call\n".len() as u32; + let asmop = AssemblyOp::new( + Some(Location::new( + uri, + ByteIndex::new(inner_start), + ByteIndex::new(inner_start + "inner body".len() as u32), + )), + "crate::physical".to_string(), + 1, + "add".to_string(), + ); + frame.push(Operation::Add, 1, Some(&asmop)); + + let mut callstack = CallStack::new(Arc::new(Mutex::new(BTreeMap::new()))); + callstack.frames.push(frame); + let source_manager = DefaultSourceManager::default(); + let logical = callstack.logical_frames(""); + + assert_eq!(logical.len(), 3); + assert_eq!(logical[0].name(), "crate::physical"); + assert_eq!(logical[0].kind(), LogicalFrameKind::Physical); + assert_eq!(logical[0].resolved(&source_manager).unwrap().line, 1); + assert_eq!(logical[1].name(), "crate::outer"); + assert_eq!(logical[1].resolved(&source_manager).unwrap().line, 2); + assert_eq!(logical[2].name(), "crate::inner"); + assert_eq!(logical[2].kind(), LogicalFrameKind::Inline); + assert_eq!(logical[2].resolved(&source_manager).unwrap().line, 3); + + fs::remove_dir_all(path.parent().unwrap().parent().unwrap()).ok(); + } + fn test_source_path(test_name: &str) -> PathBuf { PathBuf::from("target") .join("debugger-source-tests") diff --git a/crates/engine/src/exec/dap.rs b/crates/engine/src/exec/dap.rs index 912ad27..cf1d1bc 100644 --- a/crates/engine/src/exec/dap.rs +++ b/crates/engine/src/exec/dap.rs @@ -33,8 +33,8 @@ use super::{ }; use crate::{ debug::{ - DebugVarSnapshot, DebugVarTracker, FormatType, ReadMemoryExpr, resolve_variable_value, - snapshot_transient_debug_values, + DebugVarSnapshot, DebugVarTracker, FormatType, ReadMemoryExpr, inline_frames_for_operation, + resolve_variable_value, snapshot_transient_debug_values, }, exec::state::CurrentCycleInfo, }; @@ -150,6 +150,24 @@ struct DapCallFrame { source_path: Option, line: i64, column: i64, + inline_frames: Vec, +} + +#[derive(Debug, Clone)] +struct DapInlineFrame { + name: String, + source_path: String, + line: i64, + column: i64, +} + +#[derive(Debug, Clone)] +struct DapPresentedFrame { + name: Arc, + source_path: Option, + line: i64, + column: i64, + inline: bool, } /// A host wrapper that intercepts trace events to track the call stack for DAP stack traces, @@ -277,6 +295,7 @@ impl Host for DapHostWrapper<'_, H> { source_path: None, line: 0, column: 0, + inline_frames: Vec::new(), }); } crate::Event::FrameEnd => { @@ -465,49 +484,90 @@ fn build_ui_state( current_asmop: Option<&AssemblyOp>, cycle: usize, ) -> crate::exec::DapUiState { - // Build callstack from the host's frame stack (bottom-to-top order, reversed so the - // top-of-stack / most-recent frame comes first — matching DAP convention). - let callstack: Vec = if host.frames.is_empty() { - // Fallback when no frames have been recorded yet. + let callstack = presented_frames(host, current_asmop, cycle) + .into_iter() + .map(|frame| crate::exec::DapUiFrame { + name: frame.name, + source_path: frame.source_path, + line: frame.line, + column: frame.column, + inline: frame.inline, + }) + .collect(); + + let current_stack = processor + .state() + .get_stack_state() + .iter() + .map(|felt| felt.as_canonical_u64()) + .collect(); + + crate::exec::DapUiState { + cycle, + current_stack, + callstack, + } +} + +fn presented_frames( + host: &DapHostWrapper<'_, H>, + current_asmop: Option<&AssemblyOp>, + cycle: usize, +) -> Vec { + if host.frames.is_empty() { let (name, source_path, line) = match current_asmop { Some(asmop) => { let loc = resolve_asmop_location(asmop, host); let (source_path, line) = loc.map_or((None, 0), |(path, line)| (Some(path), line)); (asmop.context_name().clone(), source_path, line) } - None => (format!("cycle {cycle}").into_boxed_str().into(), None, 0), + None => (Arc::from(format!("cycle {cycle}")), None, 0), }; - vec![crate::exec::DapUiFrame { + return vec![DapPresentedFrame { name, source_path, line, column: 0, - }] - } else { - host.frames - .iter() - .rev() - .map(|frame| crate::exec::DapUiFrame { - name: frame.name.clone(), - source_path: frame.source_path.clone(), - line: frame.line, - column: frame.column, - }) - .collect() - }; + inline: false, + }]; + } - let current_stack = processor - .state() - .get_stack_state() - .iter() - .map(|felt| felt.as_canonical_u64()) - .collect(); + present_recorded_frames(&host.frames) +} - crate::exec::DapUiState { - cycle, - current_stack, - callstack, +fn present_recorded_frames(frames: &[DapCallFrame]) -> Vec { + let mut presented = Vec::new(); + for frame in frames.iter().rev() { + for (inline_index, inline) in frame.inline_frames.iter().enumerate() { + let (source_path, line, column) = if inline_index == 0 { + (frame.source_path.clone(), frame.line, frame.column) + } else { + let call_site = &frame.inline_frames[inline_index - 1]; + (Some(call_site.source_path.clone()), call_site.line, call_site.column) + }; + presented.push(DapPresentedFrame { + name: Arc::from(format!("[inlined] {}", inline.name)), + source_path, + line, + column, + inline: true, + }); + } + + let (source_path, line, column) = frame.inline_frames.last().map_or_else( + || (frame.source_path.clone(), frame.line, frame.column), + |inline| (Some(inline.source_path.clone()), inline.line, inline.column), + ); + presented.push(DapPresentedFrame { + name: frame.name.clone(), + source_path, + line, + column, + inline: false, + }); } + + presented } // BREAKPOINT STORAGE @@ -552,16 +612,24 @@ fn resolve_asmop_location(asmop: &AssemblyOp, host: &H) -> Option<(Stri } fn resolve_location(location: &Location, host: &H) -> Option<(String, i64)> { + resolve_location_with_column(location, host).map(|(path, line, _)| (path, line)) +} + +fn resolve_location_with_column( + location: &Location, + host: &H, +) -> Option<(String, i64, i64)> { let (span, source_file) = host.get_label_and_source_file(location); if let Some(source_file) = source_file { let file_line_col = source_file.location(span); let path = file_line_col.uri.as_ref().to_string(); let line = file_line_col.line.to_u32() as i64; - return Some((path, line)); + let column = file_line_col.column.to_u32() as i64; + return Some((path, line, column)); } crate::debug::resolve_location_from_filesystem(location) - .map(|(path, line)| (path.display().to_string(), line as i64)) + .map(|(path, line)| (path.display().to_string(), line as i64, 0)) } fn should_defer_function_breakpoint(resolved: Option<&(String, i64)>, context_name: &str) -> bool { @@ -987,6 +1055,7 @@ fn update_top_frame(host: &mut DapHostWrapper<'_, H>, current_asmop: Op source_path, line, column: 0, + inline_frames: Vec::new(), }); } else if let Some(top) = host.frames.last_mut() { top.name = name; @@ -995,6 +1064,40 @@ fn update_top_frame(host: &mut DapHostWrapper<'_, H>, current_asmop: Op } } +fn update_top_frame_with_debug( + host: &mut DapHostWrapper<'_, H>, + current_asmop: Option<&AssemblyOp>, + debug_info: Option<&PackageDebugInfo>, + source_node_id: Option, + op_idx: Option, +) { + update_top_frame(host, current_asmop); + + let inline_frames = debug_info + .zip(source_node_id) + .zip(op_idx) + .map(|((debug_info, source_node_id), op_idx)| { + inline_frames_for_operation(debug_info, source_node_id, op_idx as u32) + .into_iter() + .filter_map(|frame| { + let (source_path, line, column) = + resolve_location_with_column(frame.call_site(), &*host)?; + Some(DapInlineFrame { + name: frame.display_name().to_string(), + source_path, + line, + column, + }) + }) + .collect() + }) + .unwrap_or_default(); + + if let Some(top) = host.frames.last_mut() { + top.inline_frames = inline_frames; + } +} + // DAP EXECUTOR // ================================================================================================ @@ -1203,6 +1306,8 @@ impl DapExecutor { let mut cycle: usize = 0; let mut current_debug_info: Option> = None; let mut current_asmop: Option = None; + let mut current_source_node_id = None; + let mut current_op_idx = None; let mut debug_state = DapDebugVarState::new(); // Extract initial asmop and populate the root frame. @@ -1213,10 +1318,18 @@ impl DapExecutor { op_idx, .. } = extract_current_op(ctx); + current_source_node_id = source_node_id; + current_op_idx = op_idx; current_asmop = extract_asm_op(current_debug_info.as_deref(), source_node_id, op_idx); } - update_top_frame(&mut wrapper, current_asmop.as_ref()); + update_top_frame_with_debug( + &mut wrapper, + current_asmop.as_ref(), + current_debug_info.as_deref(), + current_source_node_id, + current_op_idx, + ); // On restart, emit initial state immediately (no handshake needed). if is_restart { @@ -1595,59 +1708,28 @@ impl DapExecutor { } Command::StackTrace(ref _args) => { - // Build stack frames from the host's frame stack (reversed: top-of-stack - // first, matching DAP convention). - let frames: Vec = if wrapper.frames.is_empty() { - // Fallback when no frames have been recorded. - let (name, source, line) = if let Some(asmop) = current_asmop.as_ref() { - let loc = resolve_asmop_location(asmop, &wrapper); - let (path, line_num) = - loc.unwrap_or_else(|| ("".into(), 0)); - let source = types::Source { - name: Some( - path.rsplit('/').next().unwrap_or(&path).to_string(), - ), - path: Some(path), + let frames = presented_frames(&wrapper, current_asmop.as_ref(), cycle) + .into_iter() + .enumerate() + .map(|(id, frame)| { + let source = frame.source_path.as_ref().map(|path| types::Source { + name: Some(path.rsplit('/').next().unwrap_or(path).to_string()), + path: Some(path.clone()), ..Default::default() - }; - (asmop.context_name().clone(), Some(source), line_num) - } else { - (format!("cycle {cycle}").into_boxed_str().into(), None, 0) - }; - vec![types::StackFrame { - id: 0, - name, - source, - line, - column: 0, - ..Default::default() - }] - } else { - wrapper - .frames - .iter() - .rev() - .enumerate() - .map(|(id, frame)| { - let source = - frame.source_path.as_ref().map(|path| types::Source { - name: Some( - path.rsplit('/').next().unwrap_or(path).to_string(), - ), - path: Some(path.clone()), - ..Default::default() - }); - types::StackFrame { - id: id as i64, - name: frame.name.clone(), - source, - line: frame.line, - column: frame.column, - ..Default::default() - } - }) - .collect() - }; + }); + types::StackFrame { + id: id as i64, + name: frame.name, + source, + line: frame.line, + column: frame.column, + presentation_hint: frame + .inline + .then_some(types::StackFramePresentationhint::Subtle), + ..Default::default() + } + }) + .collect::>(); let total = frames.len() as i64; let resp = @@ -2164,6 +2246,13 @@ fn advance_one( *cycle += 1; record_debug_vars(debug_state, *cycle, debug_var_infos); *current_asmop = executed_asmop; + update_top_frame_with_debug( + host, + current_asmop.as_ref(), + debug_info.as_deref(), + source_node_id, + op_idx, + ); Ok(Some(new_ctx)) } Ok(None) => { @@ -2734,4 +2823,38 @@ mod tests { Some(39), )); } + + #[test] + fn dap_presents_inline_frames_in_innermost_first_order() { + let frames = vec![DapCallFrame { + name: "crate::physical".into(), + source_path: Some("src/lib.rs".into()), + line: 30, + column: 5, + inline_frames: vec![ + DapInlineFrame { + name: "crate::inner".into(), + source_path: "src/lib.rs".into(), + line: 20, + column: 3, + }, + DapInlineFrame { + name: "crate::outer".into(), + source_path: "src/lib.rs".into(), + line: 10, + column: 1, + }, + ], + }]; + + let presented = present_recorded_frames(&frames); + + assert_eq!(presented.len(), 3); + assert_eq!(presented[0].name.as_ref(), "[inlined] crate::inner"); + assert_eq!((presented[0].line, presented[0].column), (30, 5)); + assert_eq!(presented[1].name.as_ref(), "[inlined] crate::outer"); + assert_eq!((presented[1].line, presented[1].column), (20, 3)); + assert_eq!(presented[2].name.as_ref(), "crate::physical"); + assert_eq!((presented[2].line, presented[2].column), (10, 1)); + } } diff --git a/crates/engine/src/exec/dap_types.rs b/crates/engine/src/exec/dap_types.rs index b953079..6ce28dc 100644 --- a/crates/engine/src/exec/dap_types.rs +++ b/crates/engine/src/exec/dap_types.rs @@ -17,4 +17,6 @@ pub struct DapUiFrame { pub source_path: Option, pub line: i64, pub column: i64, + #[serde(default)] + pub inline: bool, } diff --git a/crates/engine/src/exec/state.rs b/crates/engine/src/exec/state.rs index 8d19c54..95d3f82 100644 --- a/crates/engine/src/exec/state.rs +++ b/crates/engine/src/exec/state.rs @@ -19,7 +19,7 @@ use crate::{ Breakpoint, BreakpointType, OperationMatcher, debug::{ CallFrame, CallStack, ControlFlowOp, DebugVarTracker, StepInfo, - snapshot_transient_debug_values, + inline_frames_for_operation, snapshot_transient_debug_values, }, profiling::Profiler, }; @@ -284,6 +284,13 @@ impl DebugExecutor { Some(op_idx) => source_node.asm_op_for_operation(op_idx as u32), None => source_node.asm_op_for_operation(0), }); + let inline_frames = if let (Some(di), Some(dnid), Some(op_idx)) = + (debug_info.as_deref(), debug_node_id, op_idx) + { + inline_frames_for_operation(di, dnid, op_idx as u32) + } else { + Vec::new() + }; // Look up debug vars from MAST forest for the current operation let mut debug_var_infos: Vec<_> = source_node @@ -347,6 +354,7 @@ impl DebugExecutor { asmop: self.current_asmop.as_ref(), clk: RowIndex::from(self.cycle as u32), ctx: self.current_context, + inline_frames: &inline_frames, }; let exited = self.callstack.next(&step_info); diff --git a/docs/external/src/repl.md b/docs/external/src/repl.md index f81145f..6accbed 100644 --- a/docs/external/src/repl.md +++ b/docs/external/src/repl.md @@ -72,6 +72,11 @@ arguments. | `where` | `w` | Print the current source location and procedure | | `list` | `l` | Print recently executed instructions | | `backtrace` | `bt` | Print the call stack | +| `frame ` | `f` | Select logical frame `N` from the backtrace | + +Inlined calls appear as `[inlined] ` frames. Frame `0` is the +currently executing logical frame; use `frame ` before `where` to inspect a +caller or inlined frame's source location. ### Other diff --git a/examples/compile-masm.rs b/examples/compile-masm.rs index 55a501a..3750122 100644 --- a/examples/compile-masm.rs +++ b/examples/compile-masm.rs @@ -6,32 +6,38 @@ //! If `-o` is omitted, this produces `examples/simple.masp`, which you can then debug: //! cargo run -- examples/simple.masp -use std::{env, path::PathBuf, sync::Arc}; +use std::{ + env, io, + path::{Path, PathBuf}, + sync::Arc, +}; use miden_assembly::{Assembler, DefaultSourceManager, SourceManager}; -use miden_mast_package::Package; +use miden_core::serde::Serializable; +use miden_debug_types::{ColumnNumber, FileLineCol, LineNumber, Uri}; +use miden_mast_package::{ + Package, Section, SectionId, + debug_info::{ + DebugFunctionInfo, DebugSourceInlineCall, DebugSourceNodeId, PackageDebugInfoBuilder, + }, +}; -fn main() -> Result<(), Box> { - let args: Vec = env::args().collect(); - if args.len() < 2 { - eprintln!("Usage: compile-masm [-o ]"); - std::process::exit(1); - } +struct InlineCallSpec { + name: String, + line: LineNumber, + column: ColumnNumber, +} - let input_path = PathBuf::from(&args[1]); - let output_path = match args.as_slice() { - [_, _] => input_path.with_extension(Package::EXTENSION), - [_, _, flag, output] if flag == "-o" || flag == "--output" => PathBuf::from(output), - _ => { - eprintln!("Usage: compile-masm [-o ]"); - std::process::exit(1); - } - }; +fn main() -> Result<(), Box> { + let (input_path, output_path, inline_calls) = parse_args()?; let source_manager: Arc = Arc::new(DefaultSourceManager::default()); // Read and assemble the MASM source as a program let assembler = Assembler::new(source_manager.clone()); - let package = assembler.assemble_program("program", input_path.as_path())?; + let mut package = assembler.assemble_program("program", input_path.as_path())?; + if !inline_calls.is_empty() { + add_inline_calls(&mut package, &input_path, &inline_calls, source_manager.as_ref())?; + } // Write the .masp file package.write_to_file(&output_path)?; @@ -42,3 +48,133 @@ fn main() -> Result<(), Box> { Ok(()) } + +fn parse_args() -> Result<(PathBuf, PathBuf, Vec), Box> { + let mut args = env::args().skip(1); + let input_path = args.next().map(PathBuf::from).ok_or_else(|| { + invalid_argument( + "Usage: compile-masm [-o ] [--inline-call \ + ]...", + ) + })?; + let mut output_path = None; + let mut inline_calls = Vec::new(); + + while let Some(arg) = args.next() { + match arg.as_str() { + "-o" | "--output" => { + let output = args + .next() + .ok_or_else(|| invalid_argument(format!("{arg} requires a path")))?; + output_path = Some(PathBuf::from(output)); + } + "--inline-call" => { + let spec = args + .next() + .ok_or_else(|| invalid_argument("--inline-call requires name,line,column"))?; + inline_calls.push(parse_inline_call(&spec)?); + } + _ => return Err(invalid_argument(format!("unrecognized argument '{arg}'")).into()), + } + } + + let output_path = output_path.unwrap_or_else(|| input_path.with_extension(Package::EXTENSION)); + Ok((input_path, output_path, inline_calls)) +} + +fn parse_inline_call(spec: &str) -> Result> { + let mut parts = spec.rsplitn(3, ','); + let column = parts + .next() + .ok_or_else(|| invalid_argument("inline call is missing a column"))? + .parse::()?; + let line = parts + .next() + .ok_or_else(|| invalid_argument("inline call is missing a line"))? + .parse::()?; + let name = parts + .next() + .filter(|name| !name.is_empty()) + .ok_or_else(|| invalid_argument("inline call is missing a name"))?; + + Ok(InlineCallSpec { + name: name.to_string(), + line: LineNumber::new(line) + .ok_or_else(|| invalid_argument("inline call line must be greater than zero"))?, + column: ColumnNumber::new(column) + .ok_or_else(|| invalid_argument("inline call column must be greater than zero"))?, + }) +} + +fn add_inline_calls( + package: &mut Package, + input_path: &Path, + specs: &[InlineCallSpec], + source_manager: &dyn SourceManager, +) -> Result<(), Box> { + let debug_info = package + .debug_info()? + .ok_or_else(|| io::Error::other("assembled package has no debug information"))?; + let mut debug_info = PackageDebugInfoBuilder::from(Box::new(debug_info)); + let source_uri = Uri::from(input_path); + let mut callees = Vec::with_capacity(specs.len()); + for spec in specs { + let call_site = FileLineCol::new(source_uri.clone(), spec.line, spec.column); + let call_site_span = source_manager.file_line_col_to_span(call_site).ok_or_else(|| { + invalid_argument(format!( + "inline call location {}:{} is outside {}", + spec.line, + spec.column, + input_path.display() + )) + })?; + let call_site_idx = debug_info.add_location(source_manager.location(call_site_span)?); + let file_idx = debug_info.debug_info().locations()[call_site_idx].file_idx; + let name_idx = debug_info.add_string(Arc::from(spec.name.as_str())); + let callee_idx = debug_info.add_function(DebugFunctionInfo::new( + None, + name_idx, + file_idx, + spec.line, + spec.column, + Default::default(), + )); + callees.push((callee_idx, call_site_idx)); + } + + let source_nodes = debug_info + .debug_info() + .nodes() + .iter() + .enumerate() + .map(|(source_index, source_node)| { + let source_node_id = u32::try_from(source_index) + .map(DebugSourceNodeId::from) + .map_err(|_| io::Error::other("too many debug source nodes"))?; + Ok((source_node_id, source_node.op_start..source_node.op_end)) + }) + .collect::, io::Error>>()?; + for (source_node_id, operation_range) in source_nodes { + for op_idx in operation_range { + for (callee_idx, call_site_idx) in callees.iter().copied() { + debug_info[source_node_id].inline_calls.push(DebugSourceInlineCall { + op_idx, + callee_idx, + loc_idx: call_site_idx, + }); + } + } + } + + replace_section(package, SectionId::DEBUG_INFO, debug_info.build().to_bytes()); + Ok(()) +} + +fn replace_section(package: &mut Package, id: SectionId, data: Vec) { + package.sections.retain(|section| section.id != id); + package.sections.push(Section::new(id, data)); +} + +fn invalid_argument(message: impl Into) -> io::Error { + io::Error::new(io::ErrorKind::InvalidInput, message.into()) +} diff --git a/src/repl/commands.rs b/src/repl/commands.rs index a8b38a3..29fcec1 100644 --- a/src/repl/commands.rs +++ b/src/repl/commands.rs @@ -37,6 +37,8 @@ pub enum ReplCommand { List, /// Show call stack / backtrace Backtrace, + /// Select a logical stack frame by backtrace index + Frame(usize), /// Restart program Reload, /// Show help @@ -101,6 +103,13 @@ impl FromStr for ReplCommand { "where" | "w" => Ok(ReplCommand::Where), "l" | "list" => Ok(ReplCommand::List), "bt" | "backtrace" => Ok(ReplCommand::Backtrace), + "f" | "frame" => { + let index = args + .ok_or("frame requires a backtrace index")? + .parse::() + .map_err(|e| format!("invalid frame index: {e}"))?; + Ok(ReplCommand::Frame(index)) + } // Control commands "reload" => Ok(ReplCommand::Reload), @@ -141,6 +150,7 @@ Inspection: where Show current source location l, list Show recent instructions bt, backtrace Show call stack + f, frame Select logical frame N Scripting: script Execute one Python snippet @@ -192,6 +202,7 @@ Inspection: where Show current source location l, list Show recent instructions bt, backtrace Show call stack + f, frame Select logical frame N Other: h, help Show this help @@ -199,3 +210,14 @@ Other: "# } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_frame_selection() { + assert!(matches!("frame 2".parse(), Ok(ReplCommand::Frame(2)))); + assert!("frame".parse::().is_err()); + } +} diff --git a/src/repl/engine.rs b/src/repl/engine.rs index 7c150eb..d5a5ce3 100644 --- a/src/repl/engine.rs +++ b/src/repl/engine.rs @@ -1,4 +1,4 @@ -use std::{io::Write, sync::Arc}; +use std::io::Write; use miden_assembly_syntax::diagnostics::Report; @@ -23,6 +23,7 @@ pub enum Outcome { /// [`super::script::run_script`]). pub struct ReplEngine { state: State, + selected_frame: usize, } impl ReplEngine { @@ -30,12 +31,16 @@ impl ReplEngine { pub fn new(config: Box) -> Result { Ok(Self { state: State::new(config)?, + selected_frame: 0, }) } /// Create an engine from an already constructed debugger state. pub(crate) fn from_state(state: State) -> Self { - Self { state } + Self { + state, + selected_frame: 0, + } } /// Create an engine from a debugger configuration. @@ -90,11 +95,14 @@ impl ReplEngine { /// Print the current source location / procedure to `out`. pub fn print_location(&self, out: &mut dyn Write) { - let proc_name = self.state.current_procedure().unwrap_or_else(|| Arc::from("")); - if let Some(resolved) = self.state.current_display_location() { - let _ = writeln!(out, "at {} in {}", resolved, proc_name); - } else if self.state.executor().callstack.current_frame().is_some() { - let _ = writeln!(out, "in {}", proc_name); + let frames = self.state.executor().callstack.logical_frames(""); + if let Some(frame) = frames.iter().rev().nth(self.selected_frame) { + let name = frame.display_name(); + if let Some(resolved) = frame.resolved(&*self.state.source_manager) { + let _ = writeln!(out, "at {} in {}", resolved, name); + } else { + let _ = writeln!(out, "in {}", name); + } } } @@ -129,6 +137,7 @@ impl ReplEngine { ReplCommand::Where => self.cmd_where(out), ReplCommand::List => self.cmd_list(out), ReplCommand::Backtrace => self.cmd_backtrace(out), + ReplCommand::Frame(index) => self.cmd_frame(index, out), ReplCommand::Reload => self.cmd_reload(out), ReplCommand::Help => self.cmd_help(out), ReplCommand::Quit => unreachable!("quit handled in execute_line"), @@ -154,6 +163,7 @@ impl ReplEngine { } } + self.selected_frame = 0; self.print_location(out); Ok(()) } @@ -170,6 +180,7 @@ impl ReplEngine { self.ensure_can_continue()?; self.state.run_until_stopped(); + self.selected_frame = 0; if self.state.executor().stopped { if let Some(err) = self.state.execution_failed() { @@ -197,6 +208,7 @@ impl ReplEngine { self.state.create_breakpoint(bp_type); self.state.run_until_stopped(); + self.selected_frame = 0; self.print_location(out); Ok(()) } @@ -291,21 +303,20 @@ impl ReplEngine { } fn cmd_where(&mut self, out: &mut dyn Write) -> Result<(), String> { - if self.state.executor().callstack.current_frame().is_some() { - let proc_name = - self.state.current_procedure().unwrap_or_else(|| Arc::from("")); - - if let Some(resolved) = self.state.current_display_location() { + let frames = self.state.executor().callstack.logical_frames(""); + if let Some(frame) = frames.iter().rev().nth(self.selected_frame) { + let name = frame.display_name(); + if let Some(resolved) = frame.resolved(&*self.state.source_manager) { let _ = writeln!( out, "{}:{}:{} in {}", resolved.source_file.uri().as_str(), resolved.line, resolved.col, - proc_name + name ); } else { - let _ = writeln!(out, "in {} (no source location available)", proc_name); + let _ = writeln!(out, "in {} (no source location available)", name); } } else { let _ = writeln!(out, "No current frame"); @@ -333,7 +344,7 @@ impl ReplEngine { } fn cmd_backtrace(&mut self, out: &mut dyn Write) -> Result<(), String> { - let frames = self.state.executor().callstack.frames(); + let frames = self.state.executor().callstack.logical_frames(""); if frames.is_empty() { let _ = writeln!(out, "No call stack"); return Ok(()); @@ -341,19 +352,30 @@ impl ReplEngine { let _ = writeln!(out, "Backtrace ({} frames):", frames.len()); for (i, frame) in frames.iter().rev().enumerate() { - let proc_name = frame.procedure("").unwrap_or_else(|| Arc::from("")); let loc_str = frame - .last_resolved(&*self.state.source_manager) + .resolved(&*self.state.source_manager) .map(|r| format!(" at {}", r)) .unwrap_or_default(); + let marker = if i == self.selected_frame { "*" } else { " " }; - let _ = writeln!(out, " #{} {}{}", i, proc_name, loc_str); + let _ = writeln!(out, "{} #{} {}{}", marker, i, frame.display_name(), loc_str); } Ok(()) } + fn cmd_frame(&mut self, index: usize, out: &mut dyn Write) -> Result<(), String> { + let num_frames = self.state.executor().callstack.logical_frames("").len(); + if index >= num_frames { + return Err(format!("invalid frame index {index}; backtrace has {num_frames} frames")); + } + self.selected_frame = index; + self.print_location(out); + Ok(()) + } + fn cmd_reload(&mut self, out: &mut dyn Write) -> Result<(), String> { self.state.reload().map_err(|e| format!("reload failed: {e}"))?; + self.selected_frame = 0; let _ = writeln!(out, "Program reloaded"); self.print_location(out); Ok(()) diff --git a/src/ui/panes/source_code.rs b/src/ui/panes/source_code.rs index c7511d8..3631869 100644 --- a/src/ui/panes/source_code.rs +++ b/src/ui/panes/source_code.rs @@ -105,11 +105,8 @@ impl SourceCodePane { /// Get the [ResolvedLocation] for the current state fn current_location(&self, state: &State) -> Option { - match state.executor().callstack.current_frame() { - Some(frame) => { - let resolved = frame.last_resolved(&state.source_manager); - resolved.cloned() - } + match state.selected_display_location() { + Some(resolved) => Some(resolved), None if !self.current_source_id.is_unknown() => { let source_file = state.source_manager.get(self.current_source_id).ok(); source_file.map(|src| ResolvedLocation { @@ -185,10 +182,8 @@ impl SourceCodePane { self.selected_line = 0; self.current_file = None; - if let Some(frame) = state.executor().callstack.current_frame() - && let Some(loc) = frame.last_resolved(&state.source_manager) - { - self.current_file = Some(self.highlight_file(loc)); + if let Some(loc) = state.selected_display_location() { + self.current_file = Some(self.highlight_file(&loc)); self.current_source_id = loc.source_file.id(); self.current_span = loc.span; self.current_line = loc.line; @@ -230,10 +225,8 @@ impl Pane for SourceCodePane { fn init(&mut self, state: &State) -> Result<(), Report> { self.enable_syntax_highlighting(state); - if let Some(frame) = state.executor().callstack.current_frame() - && let Some(loc) = frame.last_resolved(&state.source_manager) - { - self.current_file = Some(self.highlight_file(loc)); + if let Some(loc) = state.selected_display_location() { + self.current_file = Some(self.highlight_file(&loc)); self.current_source_id = loc.source_file.id(); self.current_span = loc.span; self.current_line = loc.line; @@ -292,6 +285,9 @@ impl Pane for SourceCodePane { } else if self.selected_line != loc.line { self.selected_line = loc.line; } + if let Some(current_file) = self.current_file.as_mut() { + current_file.selected_span = loc.span; + } self.current_span = loc.span; self.current_line = loc.line; self.current_col = loc.col; diff --git a/src/ui/panes/stacktrace.rs b/src/ui/panes/stacktrace.rs index 47e2c33..1bbc360 100644 --- a/src/ui/panes/stacktrace.rs +++ b/src/ui/panes/stacktrace.rs @@ -1,7 +1,10 @@ use miden_assembly_syntax::diagnostics::Report; use ratatui::{prelude::*, widgets::*}; -use crate::ui::{action::Action, panes::Pane, state::State, tui::Frame}; +use crate::{ + debug::LogicalFrameKind, + ui::{action::Action, panes::Pane, state::State, tui::Frame}, +}; pub struct StackTracePane { focused: bool, @@ -39,10 +42,19 @@ impl Pane for StackTracePane { } } - fn update(&mut self, action: Action, _state: &mut State) -> Result, Report> { + fn update(&mut self, action: Action, state: &mut State) -> Result, Report> { match action { + Action::Up => { + state.select_older_stack_frame(); + return Ok(Some(Action::Update)); + } + Action::Down => { + state.select_newer_stack_frame(); + return Ok(Some(Action::Update)); + } Action::Focus => { self.focused = true; + return Ok(Some(Action::TimedStatusLine("[j,k → frame selection]".into(), 3))); } Action::UnFocus => { self.focused = false; @@ -55,13 +67,14 @@ impl Pane for StackTracePane { fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, state: &State) -> Result<(), Report> { let mut lines = Vec::default(); - let num_frames = state.executor().callstack.frames().len(); + let logical_frames = state.logical_stack_frames(); + let num_frames = logical_frames.len(); // For the top frame, prefer the live AsmOp's context_name over the // frame's cached procedure, which is set once on frame entry and // stays stale for programs that use `exec` instead of `call`. let live_top_name: Option = state.executor().current_asmop.as_ref().map(|op| op.context_name().to_string()); - for (i, frame) in state.executor().callstack.frames().iter().enumerate() { + for (i, logical_frame) in logical_frames.iter().enumerate() { let is_top = i + 1 == num_frames; let mut parts = vec![]; /* @@ -73,23 +86,26 @@ impl Pane for StackTracePane { */ let gutter = Span::styled(" ", Color::Gray); parts.push(gutter); - let name = if is_top { - live_top_name.clone().or_else(|| frame.procedure("").map(|p| p.to_string())) + let name = if is_top && logical_frame.kind() == LogicalFrameKind::Physical { + live_top_name.clone().unwrap_or_else(|| logical_frame.display_name()) } else { - frame.procedure("").map(|p| p.to_string()) + logical_frame.display_name() }; - let name = name.unwrap_or_else(|| "".to_string()); let name = if is_top { Span::styled(name, Color::Gray) } else { Span::styled(name, Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) }; parts.push(name); - if let Some(resolved) = frame.last_resolved(&state.source_manager) { + if let Some(resolved) = logical_frame.resolved(&*state.source_manager) { parts.push(Span::styled(" in ", Color::DarkGray)); - let path = std::path::Path::new(resolved.source_file.as_ref().uri().as_str()); - let path = path.strip_prefix(state.config.working_dir()).ok().unwrap_or(path); - let path_str = path.to_string_lossy(); + let source_path = + std::path::PathBuf::from(resolved.source_file.as_ref().uri().as_str()); + let path = source_path + .strip_prefix(state.config.working_dir()) + .ok() + .unwrap_or(&source_path); + let path_str = path.to_string_lossy().into_owned(); let max_width = (area.as_size().width as usize).saturating_sub(4); let path_width = path_str.chars().count(); if path_width >= max_width { @@ -128,7 +144,8 @@ impl Pane for StackTracePane { lines.push(Line::from(parts)); } - let selected_line = lines.len().saturating_sub(1); + let selected_line = + lines.len().saturating_sub(state.selected_stack_frame().saturating_add(1)); let list = List::new(lines) .block(Block::default().borders(Borders::ALL)) diff --git a/src/ui/state.rs b/src/ui/state.rs index d405424..f1281d2 100644 --- a/src/ui/state.rs +++ b/src/ui/state.rs @@ -50,6 +50,7 @@ pub struct State { pub next_breakpoint_id: u8, pub stopped: bool, pub debug_mode: DebugMode, + selected_stack_frame: usize, session: SessionState, } @@ -267,6 +268,7 @@ impl State { next_breakpoint_id: 0, stopped: true, debug_mode, + selected_stack_frame: 0, session: SessionState::Local(Box::new(local)), } } @@ -376,6 +378,7 @@ impl State { let local = create_local_state(&self.config, self.source_manager.clone())?; self.session = SessionState::Local(Box::new(local)); + self.selected_stack_frame = 0; self.breakpoints_hit.clear(); let breakpoints = core::mem::take(&mut self.breakpoints); self.breakpoints.reserve(breakpoints.len()); @@ -617,6 +620,7 @@ impl State { self.breakpoints = breakpoints; self.stopped = stopped; + self.selected_stack_frame = 0; } pub fn create_breakpoint(&mut self, ty: BreakpointType) { @@ -704,6 +708,31 @@ impl State { None } + pub fn logical_stack_frames(&self) -> Vec { + self.executor().callstack.logical_frames("") + } + + pub fn selected_stack_frame(&self) -> usize { + self.selected_stack_frame + } + + pub fn select_older_stack_frame(&mut self) { + let last = self.logical_stack_frames().len().saturating_sub(1); + self.selected_stack_frame = self.selected_stack_frame.saturating_add(1).min(last); + } + + pub fn select_newer_stack_frame(&mut self) { + self.selected_stack_frame = self.selected_stack_frame.saturating_sub(1); + } + + pub fn selected_display_location(&self) -> Option { + self.logical_stack_frames() + .iter() + .rev() + .nth(self.selected_stack_frame) + .and_then(|frame| frame.resolved(&*self.source_manager)) + } + /// Return the current source position as seen from the nearest non-internal /// (user) call frame. /// @@ -1297,6 +1326,7 @@ impl State { next_breakpoint_id: 0, stopped: true, debug_mode: DebugMode::Remote, + selected_stack_frame: 0, session: SessionState::Remote(Box::new(remote)), }) } @@ -1313,6 +1343,7 @@ impl State { match &result { crate::exec::DapStopReason::Stopped(snapshot) => { remote.refresh_executor(&source_manager, snapshot); + self.selected_stack_frame = 0; self.stopped = true; } crate::exec::DapStopReason::Terminated => { @@ -1340,6 +1371,7 @@ fn convert_ui_state( let call_frames: Vec = snapshot .callstack .iter() + .rev() .map(|frame| { let resolved = resolve_remote_frame(frame, source_manager); CallFrame::from_remote(Some(frame.name.clone()), resolved) diff --git a/tests/lit/README.md b/tests/lit/README.md index d0e35da..b63700f 100644 --- a/tests/lit/README.md +++ b/tests/lit/README.md @@ -74,3 +74,14 @@ in mind: Prefer asserting on stable facts (a known computed result) over incidental formatting. + +For stack-trace tests, `compile-masm` accepts repeated +`--inline-call ` options. They attach an innermost-first +inline call chain to the fixture's operations without requiring `midenc` in the +debugger CI job: + +```text +compile-masm inline_call.masm -o inline_call.masp \ + --inline-call fixture::inner,3,1 \ + --inline-call fixture::outer,2,1 +``` diff --git a/tests/lit/inline_call.masm b/tests/lit/inline_call.masm new file mode 100644 index 0000000..7aa71eb --- /dev/null +++ b/tests/lit/inline_call.masm @@ -0,0 +1,6 @@ +begin + push.10 + push.20 + add + add +end diff --git a/tests/lit/inline_call.test b/tests/lit/inline_call.test new file mode 100644 index 0000000..6ba3048 --- /dev/null +++ b/tests/lit/inline_call.test @@ -0,0 +1,19 @@ +# Verify that inline-call metadata is presented as selectable logical frames. +# +# RUN: compile-masm %S/inline_call.masm -o %t.masp \ +# RUN: --inline-call fixture::inner,3,1 \ +# RUN: --inline-call fixture::outer,2,1 +# RUN: miden-debug --commands %s %t.masp 2>&1 | filecheck %s + +break inline_call.masm:4 +continue +backtrace +frame 1 +frame 2 + +# CHECK: Backtrace (3 frames): +# CHECK-NEXT: * #0 [inlined] fixture::inner at {{.*}}inline_call.masm:4:5 +# CHECK-NEXT: #1 [inlined] fixture::outer at {{.*}}inline_call.masm:3:1 +# CHECK-NEXT: #2 $exec::$main at {{.*}}inline_call.masm:2:1 +# CHECK: at {{.*}}inline_call.masm:3:1 in [inlined] fixture::outer +# CHECK: at {{.*}}inline_call.masm:2:1 in $exec::$main diff --git a/tests/lit/line_breakpoint.test b/tests/lit/line_breakpoint.test index 7ff41d0..cf6a356 100644 --- a/tests/lit/line_breakpoint.test +++ b/tests/lit/line_breakpoint.test @@ -13,6 +13,6 @@ stack # CHECK: Breakpoint 0 set: **/breakpoint.masm:6 # CHECK: at {{.*}}breakpoint.masm:6:5 -# CHECK: breakpoint.masm:6:5 in ::$exec::$main +# CHECK: breakpoint.masm:6:5 in $exec::$main # CHECK: Program terminated successfully # CHECK: [0] 150