Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## v0.29.1 (Unreleased)

#### Enhancements

- Added inline call-chain metadata to package source maps so debuggers can reconstruct inlined stack frames, and preserved source-node context during stepped execution ([#3427](https://github.com/0xMiden/miden-vm/pull/3427)).

## v0.29.0 (2026-08-04)

#### Changes
Expand Down
59 changes: 59 additions & 0 deletions core/src/operations/debug_metadata/inline_call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use alloc::sync::Arc;

use miden_debug_types::FileLineCol;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// Debug information describing one source-level function in an active inline call chain.
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct DebugInlineCallInfo {
name: Arc<str>,
linkage_name: Option<Arc<str>>,
declaration: FileLineCol,
call_site: FileLineCol,
}

impl DebugInlineCallInfo {
pub fn new(
name: impl Into<Arc<str>>,
declaration: FileLineCol,
call_site: FileLineCol,
) -> Self {
Self {
name: name.into(),
linkage_name: None,
declaration,
call_site,
}
}

pub fn with_linkage_name(mut self, linkage_name: impl Into<Arc<str>>) -> Self {
self.linkage_name = Some(linkage_name.into());
self
}

pub fn name(&self) -> &str {
&self.name
}

pub fn linkage_name(&self) -> Option<&str> {
self.linkage_name.as_deref()
}

pub fn declaration(&self) -> &FileLineCol {
&self.declaration
}

pub fn call_site(&self) -> &FileLineCol {
&self.call_site
}

pub fn set_declaration(&mut self, declaration: FileLineCol) {
self.declaration = declaration;
}

pub fn set_call_site(&mut self, call_site: FileLineCol) {
self.call_site = call_site;
}
}
3 changes: 3 additions & 0 deletions core/src/operations/debug_metadata/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
mod assembly_op;
pub use assembly_op::AssemblyOp;

mod inline_call;
pub use inline_call::DebugInlineCallInfo;
2 changes: 1 addition & 1 deletion core/src/operations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use core::fmt;
use serde::{Deserialize, Serialize};

mod debug_metadata;
pub use debug_metadata::AssemblyOp;
pub use debug_metadata::{AssemblyOp, DebugInlineCallInfo};

use crate::{
Felt,
Expand Down
4 changes: 3 additions & 1 deletion crates/assembly-syntax/src/ast/instruction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ pub enum Instruction {

// ----- debug decorators --------------------------------------------------------------------
DebugVar(DebugVarInfo),
DebugInlineCall(miden_core::operations::DebugInlineCallInfo),
DebugInlineCallClear,

// ----- event decorators --------------------------------------------------------------------
Emit,
Expand All @@ -295,7 +297,7 @@ impl Instruction {
/// Some instructions (like [`DebugVar`](Self::DebugVar)) are compiler-internal and have
/// no surface syntax. They should be skipped during pretty-printing.
pub const fn has_textual_representation(&self) -> bool {
!matches!(self, Self::DebugVar(_))
!matches!(self, Self::DebugVar(_) | Self::DebugInlineCall(_) | Self::DebugInlineCallClear)
}
}

Expand Down
4 changes: 3 additions & 1 deletion crates/assembly-syntax/src/ast/instruction/print.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,9 @@ impl PrettyPrint for Instruction {
Self::EmitImm(value) => inst_with_felt_imm("emit", value),

// Handled by the early return for !has_textual_representation()
Self::DebugVar(_) => unreachable!(),
Self::DebugVar(_) | Self::DebugInlineCall(_) | Self::DebugInlineCallClear => {
unreachable!()
},
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/assembly-syntax/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ mod r#type;
mod visibility;
pub mod visit;

pub use miden_core::operations::DebugInlineCallInfo;

pub use self::{
advice_map_entry::AdviceMapEntry,
attribute::{
Expand Down
8 changes: 4 additions & 4 deletions crates/assembly-syntax/src/ast/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,8 +504,8 @@ where
| Caller | Clk | MemLoad | MemLoadWBe | MemLoadWLe | MemStore | MemStoreWBe
| MemStoreWLe | MemStream | AdvPipe | AdvPush | AdvPushW | AdvLoadW | Hash | HMerge
| HPerm | MTreeGet | MTreeSet | MTreeMerge | MTreeVerify | FriExt2Fold4 | DynExec
| DynCall | DebugVar(_) | HornerBase | HornerExt | CryptoStream | EvalCircuit
| LogDeferred | Emit => ControlFlow::Continue(()),
| DynCall | DebugVar(_) | DebugInlineCall(_) | DebugInlineCallClear | HornerBase
| HornerExt | CryptoStream | EvalCircuit | LogDeferred | Emit => ControlFlow::Continue(()),
}
}

Expand Down Expand Up @@ -1068,8 +1068,8 @@ where
| Caller | Clk | MemLoad | MemLoadWBe | MemLoadWLe | MemStore | MemStoreWBe
| MemStoreWLe | MemStream | AdvPipe | AdvPush | AdvPushW | AdvLoadW | Hash | HMerge
| HPerm | MTreeGet | MTreeSet | MTreeMerge | MTreeVerify | FriExt2Fold4 | DynExec
| DynCall | DebugVar(_) | HornerBase | HornerExt | EvalCircuit | CryptoStream
| LogDeferred | Emit => ControlFlow::Continue(()),
| DynCall | DebugVar(_) | DebugInlineCall(_) | DebugInlineCallClear | HornerBase
| HornerExt | EvalCircuit | CryptoStream | LogDeferred | Emit => ControlFlow::Continue(()),
}
}

Expand Down
92 changes: 85 additions & 7 deletions crates/assembly/src/basic_block_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ use miden_assembly_syntax::{
diagnostics::Report,
};
use miden_core::{
Felt,
Felt, Word,
events::SystemEvent,
operations::{AssemblyOp, Operation},
operations::{AssemblyOp, DebugInlineCallInfo, Operation},
};
use miden_mast_package::debug_info::{
DebugFunctionIdx, DebugLocIdx, DebugSourceAsmOp, DebugSourceInlineCall, DebugSourceVar,
FunctionInfo,
};
use miden_mast_package::debug_info::{DebugSourceAsmOp, DebugSourceVar};

use crate::{
ProcedureContext,
Expand Down Expand Up @@ -43,6 +46,12 @@ struct PendingAsmOp {
op: String,
}

#[derive(Debug, Clone, Copy)]
struct ActiveInlineCall {
callee_idx: DebugFunctionIdx,
loc_idx: DebugLocIdx,
}

// BASIC BLOCK BUILDER
// ================================================================================================

Expand All @@ -64,6 +73,10 @@ pub struct BasicBlockBuilder<'a> {
asm_ops: Vec<DebugSourceAsmOp>,
/// Debug variables attached to operations in this block.
debug_vars: Vec<DebugSourceVar>,
/// Inline call chains attached to operations in this block.
inline_calls: Vec<DebugSourceInlineCall>,
/// The source-level inline call chain active for subsequently generated operations.
active_inline_calls: Vec<ActiveInlineCall>,
mast_forest_builder: &'a mut MastForestBuilder,
}

Expand All @@ -85,6 +98,8 @@ impl<'a> BasicBlockBuilder<'a> {
pending_asm_op: None,
asm_ops: Vec::new(),
debug_vars: Vec::new(),
inline_calls: Vec::new(),
active_inline_calls: Vec::new(),
mast_forest_builder,
},
None => Self {
Expand All @@ -93,6 +108,8 @@ impl<'a> BasicBlockBuilder<'a> {
pending_asm_op: None,
asm_ops: Vec::new(),
debug_vars: Default::default(),
inline_calls: Default::default(),
active_inline_calls: Default::default(),
mast_forest_builder,
},
}
Expand All @@ -116,6 +133,7 @@ impl BasicBlockBuilder<'_> {
impl BasicBlockBuilder<'_> {
/// Adds the specified operation to the list of basic block operations.
pub fn push_op(&mut self, op: Operation) {
self.record_active_inline_calls(self.ops.len() as u32);
self.ops.push(op);
}

Expand All @@ -125,13 +143,16 @@ impl BasicBlockBuilder<'_> {
I: IntoIterator<Item = O>,
O: Borrow<Operation>,
{
self.ops.extend(ops.into_iter().map(|o| *o.borrow()));
for op in ops {
self.push_op(*op.borrow());
}
}

/// Adds the specified operation n times to the list of basic block operations.
pub fn push_op_many(&mut self, op: Operation, n: usize) {
let new_len = self.ops.len() + n;
self.ops.resize(new_len, op);
for _ in 0..n {
self.push_op(op);
}
}

/// Converts the system event into its corresponding event ID, and adds an `Emit` operation
Expand Down Expand Up @@ -232,6 +253,62 @@ impl BasicBlockBuilder<'_> {
self.debug_vars.push(debug_var);
Ok(())
}

/// Appends one frame to the inline call chain active for subsequently generated operations.
pub fn push_debug_inline_call(
&mut self,
inline_call: &DebugInlineCallInfo,
source_manager: &dyn miden_assembly_syntax::debuginfo::SourceManager,
) {
let Some(call_site_span) =
source_manager.file_line_col_to_span(inline_call.call_site().clone())
else {
return;
};
let Ok(call_site) = source_manager.location(call_site_span) else {
return;
};

let debug_info = self.mast_forest_builder.debug_info_mut();
let declaration = inline_call.declaration();
let file_idx = debug_info.add_file(declaration.uri.clone(), None);
let name_idx = debug_info.add_string(inline_call.name());
let mut function = FunctionInfo::new(
None,
name_idx,
file_idx,
declaration.line,
declaration.column,
Word::default(),
);
if let Some(linkage_name) = inline_call.linkage_name() {
function = function.with_linkage_name(debug_info.add_string(linkage_name));
}
let callee_idx = debug_info
.debug_info()
.functions()
.iter()
.position(|existing| existing == &function)
.map(|index| DebugFunctionIdx::from(index as u32))
.unwrap_or_else(|| debug_info.add_function(function));
let loc_idx = debug_info.add_location(call_site);
self.active_inline_calls.push(ActiveInlineCall { callee_idx, loc_idx });
}

/// Clears the inline call chain active for subsequently generated operations.
pub fn clear_debug_inline_calls(&mut self) {
self.active_inline_calls.clear();
}

fn record_active_inline_calls(&mut self, op_idx: u32) {
self.inline_calls.extend(self.active_inline_calls.iter().map(|inline_call| {
DebugSourceInlineCall {
op_idx,
callee_idx: inline_call.callee_idx,
loc_idx: inline_call.loc_idx,
}
}));
}
}

/// Basic Block Constructors
Expand All @@ -247,12 +324,13 @@ impl BasicBlockBuilder<'_> {
let ops = self.ops.drain(..).collect();
let asm_ops = core::mem::take(&mut self.asm_ops);
let debug_vars = self.debug_vars.drain(..).collect();
let inline_calls = self.inline_calls.drain(..).collect();

let basic_block_node_ref = self.mast_forest_builder.ensure_block_ref(
ops,
asm_ops,
debug_vars,
vec![],
inline_calls,
vec![],
)?;

Expand Down
6 changes: 6 additions & 0 deletions crates/assembly/src/instruction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,12 @@ impl Assembler {
Instruction::DebugVar(debug_var_info) => {
block_builder.push_debug_var(debug_var_info.clone())?;
},
Instruction::DebugInlineCall(inline_call) => {
block_builder.push_debug_inline_call(inline_call, proc_ctx.source_manager());
},
Instruction::DebugInlineCallClear => {
block_builder.clear_debug_inline_calls();
},

// ----- emit instruction -------------------------------------------------------------
// emit: reads event ID from top of stack and execute the corresponding handler.
Expand Down
35 changes: 35 additions & 0 deletions crates/assembly/src/mast_forest_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1643,6 +1643,41 @@ mod tests {
);
}

#[test]
fn test_source_graph_uses_full_merged_occurrence_as_control_flow_child() {
let mut builder = MastForestBuilder::new(&[]).unwrap();

let first_asm_op = add_test_asm_op(&mut builder, test_asm_op("merge::first", "add"));
let second_asm_op = add_test_asm_op(&mut builder, test_asm_op("merge::second", "mul"));
let first_block_ref = builder
.ensure_block_ref(vec![Operation::Add], vec![first_asm_op], vec![], vec![], vec![])
.unwrap();
let second_block_ref = builder
.ensure_block_ref(vec![Operation::Mul], vec![second_asm_op], vec![], vec![], vec![])
.unwrap();
let merged_ref =
builder.merge_basic_block_refs(&[first_block_ref, second_block_ref]).unwrap()[0];
let external_ref = builder
.ensure_external_link_with_source_ref(test_word(1), None, None, None)
.unwrap();
let root_ref = builder.join_node_refs(vec![merged_ref, external_ref], None).unwrap();
record_test_root(&mut builder, root_ref);

let (_, _, source_graph, _) = builder.build().unwrap().into_parts_with_debug_info();
let root = source_graph.roots()[0];
let merged_child = &source_graph[source_graph[root].children[0]];

assert_eq!((merged_child.op_start, merged_child.op_end), (0, 2));
assert_eq!(
merged_child
.asm_ops
.iter()
.map(|asm_op| source_graph[asm_op.context_name_idx].as_ref())
.collect::<Vec<_>>(),
vec!["merge::first", "merge::second"],
);
}

#[test]
fn test_source_graph_preserves_repeated_deduped_block_ranges_in_merge_window() {
let mut builder = MastForestBuilder::new(&[]).unwrap();
Expand Down
7 changes: 7 additions & 0 deletions processor/src/fast/step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ impl ResumeContext {
self.package_debug_info.clone()
}

/// Returns the source/debug occurrence associated with the next continuation, if available.
pub fn next_source_node_id(&self) -> Option<DebugSourceNodeId> {
self.continuation_stack
.peek_continuation_with_source_node_id()
.and_then(|(_, source_node_id)| source_node_id)
}

/// Returns a reference to the kernel being currently executed.
pub fn kernel(&self) -> &KernelDescriptor {
&self.kernel
Expand Down
Loading
Loading