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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ node_modules/
*.iml
book/

**/src/bindings.rs
*.lit_test_times.txt*
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ midenc-dialect-arith = { version = "0.6.0", path = "dialects/arith" }
midenc-dialect-hir = { version = "0.6.0", path = "dialects/hir" }
midenc-dialect-scf = { version = "0.6.0", path = "dialects/scf" }
midenc-dialect-cf = { version = "0.6.0", path = "dialects/cf" }
midenc-dialect-debuginfo = { version = "0.6.0", path = "dialects/debuginfo" }
midenc-dialect-ub = { version = "0.6.0", path = "dialects/ub" }
midenc-hir = { version = "0.6.0", path = "hir" }
midenc-hir-analysis = { version = "0.6.0", path = "hir-analysis" }
Expand Down
1 change: 1 addition & 0 deletions Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ args = [
"${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/tests/lit/parse",
"${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/tests/lit/wasm-translation",
"${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/tests/lit/source-location",
"${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/tests/lit/debugdump",
]
dependencies = ["litcheck"]

Expand Down
1 change: 1 addition & 0 deletions codegen/masm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ midenc-hir.workspace = true
midenc-hir-analysis.workspace = true
midenc-dialect-arith.workspace = true
midenc-dialect-cf.workspace = true
midenc-dialect-debuginfo.workspace = true
midenc-dialect-hir.workspace = true
midenc-dialect-scf.workspace = true
midenc-dialect-ub.workspace = true
Expand Down
13 changes: 10 additions & 3 deletions codegen/masm/src/emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,16 @@ impl BlockEmitter<'_> {
// operand stack space on operands that will never be used.
//self.drop_unused_operands_at(op);

let lowering = op.as_trait::<dyn HirLowering>().unwrap_or_else(|| {
panic!("illegal operation: no lowering has been defined for '{}'", op.name())
});
let Some(lowering) = op.as_trait::<dyn HirLowering>() else {
// Skip debug info ops that have no lowering (e.g. debuginfo.kill,
// debuginfo.declare) rather than panicking. These ops carry no
// semantic meaning for code generation.
if op.name().dialect().as_str() == "debuginfo" {
log::trace!(target: "codegen", "skipping debug info op with no lowering: {}", op.name());
return;
}
panic!("illegal operation: no lowering has been defined for '{}'", op.name());
};

// Schedule operands for this instruction
lowering
Expand Down
4 changes: 4 additions & 0 deletions codegen/masm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub use self::{
pub fn register_dialect_hooks(context: &midenc_hir::Context) {
use midenc_dialect_arith as arith;
use midenc_dialect_cf as cf;
use midenc_dialect_debuginfo as debuginfo;
use midenc_dialect_hir as hir;
use midenc_dialect_scf as scf;
use midenc_dialect_ub as ub;
Expand All @@ -47,6 +48,9 @@ pub fn register_dialect_hooks(context: &midenc_hir::Context) {
info.register_operation_trait::<builtin::RetImm, dyn HirLowering>();
info.register_operation_trait::<builtin::GlobalSymbol, dyn HirLowering>();
});
context.register_dialect_hook::<debuginfo::DebugInfoDialect, _>(|info, _context| {
info.register_operation_trait::<debuginfo::DebugValue, dyn HirLowering>();
});
context.register_dialect_hook::<arith::ArithDialect, _>(|info, _context| {
info.register_operation_trait::<arith::Constant, dyn HirLowering>();
info.register_operation_trait::<arith::Add, dyn HirLowering>();
Expand Down
91 changes: 90 additions & 1 deletion codegen/masm/src/lower/component.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use alloc::{collections::BTreeSet, sync::Arc};
use alloc::{collections::BTreeSet, sync::Arc, vec::Vec};

use miden_assembly::{LibraryPath, ast::InvocationTarget};
use miden_assembly_syntax::parser::WordValue;
use miden_core::DebugVarLocation;
use miden_mast_package::ProcedureName;
use midenc_hir::{
CallConv, FunctionIdent, Op, SourceSpan, Span, Symbol, ValueRef, diagnostics::IntoDiagnostic,
Expand Down Expand Up @@ -619,10 +620,98 @@ impl MasmFunctionBuilder {
num_locals,
} = self;

// Compute total WASM locals count for FMP offset calculation.
// WASM locals = params (in felts) + local variables (in felts).
// This is needed because DWARF's WasmLocal(idx) uses WASM indexing where
// params come first, while num_locals only counts HIR spilled values.
let num_params_in_felts: u16 = function
.signature()
.params
.iter()
.map(|p| p.ty.size_in_felts() as u16)
.sum();
let num_wasm_locals = num_params_in_felts + num_locals;

// Patch DebugVar Local locations to compute FMP offset.
// During lowering, Local(idx) stores the raw WASM local index.
// Now convert to FMP offset: idx - num_wasm_locals
patch_debug_var_locals_in_block(&mut body, num_wasm_locals);

// Strip DebugVar-only procedure bodies.
// The Miden assembler rejects procedures whose bodies contain only decorators
// (like DebugVar) and no real instructions, because decorators don't affect
// MAST digests — two empty procedures with different decorators would be
// indistinguishable. If there are no real instructions, the debug info is
// meaningless anyway, so just drop it.
if !block_has_real_instructions(&body) {
body = masm::Block::new(body.span(), vec![]);
}

let mut procedure = masm::Procedure::new(span, visibility, name, num_locals, body);
procedure.set_signature(signature);
procedure.extend_invoked(invoked);

Ok(procedure)
}
}

/// Returns true if the block contains at least one real (non-decorator) instruction.
///
/// DebugVar instructions are decorator-only and don't produce MAST nodes. If a procedure
/// body contains only DebugVar ops, the assembler will reject it.
fn block_has_real_instructions(block: &masm::Block) -> bool {
block.iter().any(|op| match op {
masm::Op::Inst(inst) => inst.has_textual_representation(),
masm::Op::If {
then_blk, else_blk, ..
} => block_has_real_instructions(then_blk) || block_has_real_instructions(else_blk),
masm::Op::While { body, .. } => block_has_real_instructions(body),
masm::Op::Repeat { body, .. } => block_has_real_instructions(body),
})
}

/// Recursively patch DebugVar Local locations in a block.
///
/// Converts `Local(idx)` where idx is the raw WASM local index to `Local(offset)`
/// where offset = idx - num_locals (the FMP offset, typically negative).
fn patch_debug_var_locals_in_block(block: &mut masm::Block, num_locals: u16) {
for op in block.iter_mut() {
match op {
masm::Op::Inst(span_inst) => {
// Use DerefMut to get mutable access to the inner Instruction
if let masm::Instruction::DebugVar(info) = &mut **span_inst {
if let DebugVarLocation::Local(idx) = info.value_location() {
// Convert raw WASM local index to FMP offset
let fmp_offset = *idx - (num_locals as i16);

// Create new info with patched location, preserving all fields
let mut new_info = miden_core::DebugVarInfo::new(
info.name(),
DebugVarLocation::Local(fmp_offset),
);
if let Some(type_id) = info.type_id() {
new_info.set_type_id(type_id);
}
if let Some(arg_index) = info.arg_index() {
new_info.set_arg_index(arg_index.get());
}
if let Some(loc) = info.location() {
new_info.set_location(loc.clone());
}
*info = new_info;
}
}
}
masm::Op::If { then_blk, else_blk, .. } => {
patch_debug_var_locals_in_block(then_blk, num_locals);
patch_debug_var_locals_in_block(else_blk, num_locals);
}
masm::Op::While { body: while_body, .. } => {
patch_debug_var_locals_in_block(while_body, num_locals);
}
masm::Op::Repeat { body: repeat_body, .. } => {
patch_debug_var_locals_in_block(repeat_body, num_locals);
}
}
}
}
111 changes: 111 additions & 0 deletions codegen/masm/src/lower/lowering.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use midenc_dialect_arith as arith;
use midenc_dialect_cf as cf;
use midenc_dialect_debuginfo as debuginfo;
use midenc_dialect_hir as hir;
use midenc_dialect_scf as scf;
use midenc_dialect_ub as ub;
Expand Down Expand Up @@ -1229,6 +1230,116 @@ impl HirLowering for arith::Split {
}
}

impl HirLowering for debuginfo::DebugValue {
fn schedule_operands(&self, _emitter: &mut BlockEmitter<'_>) -> Result<(), Report> {
// Debug value operations are purely observational — they do not consume their
// operand from the stack. Skip operand scheduling entirely; the emit() method
// will look up the value's current stack position (if any) on its own.
Ok(())
}

fn required_operands(&self) -> ValueRange<'_, 4> {
// No operands need to be scheduled on the stack for debug ops.
ValueRange::Empty
}

fn emit(&self, emitter: &mut BlockEmitter<'_>) -> Result<(), Report> {
use miden_core::{DebugVarInfo, DebugVarLocation, Felt};
use midenc_hir::DIExpressionOp;

// Get the variable info
let var = self.variable();

// Build the DebugVarLocation from DIExpression
let expr = self.expression();
let value = self.value().as_value_ref();

// If the value is not on the stack and there's no expression info,
// skip emitting this debug info (the value has been optimized away)
let has_location_expr = expr.operations.first().is_some_and(|op| {
matches!(
op,
DIExpressionOp::WasmStack(_)
| DIExpressionOp::WasmLocal(_)
| DIExpressionOp::ConstU64(_)
| DIExpressionOp::ConstS64(_)
)
});
if !has_location_expr && emitter.stack.find(&value).is_none() {
// Value has been dropped and we have no other location info, skip
return Ok(());
}
let value_location = if let Some(first_op) = expr.operations.first() {
match first_op {
DIExpressionOp::WasmStack(offset) => DebugVarLocation::Stack(*offset as u8),
DIExpressionOp::WasmLocal(idx) => {
// First check if the value is on the Miden operand stack.
// WASM locals might stay on the stack in Miden if not spilled.
if let Some(pos) = emitter.stack.find(&value) {
DebugVarLocation::Stack(pos as u8)
} else {
// Value is not on stack, assume it's in local memory.
// Store raw WASM local index temporarily. The FMP offset will be
// computed later in MasmFunctionBuilder::build() when num_locals is known.
DebugVarLocation::Local(*idx as i16)
}
}
DIExpressionOp::WasmGlobal(_) | DIExpressionOp::Deref => {
// For global or dereference, check the stack position of the value
if let Some(pos) = emitter.stack.find(&value) {
DebugVarLocation::Stack(pos as u8)
} else {
DebugVarLocation::Expression(vec![])
}
}
DIExpressionOp::ConstU64(val) => DebugVarLocation::Const(Felt::new(*val)),
DIExpressionOp::ConstS64(val) => DebugVarLocation::Const(Felt::new(*val as u64)),
_ => {
// For other operations, try to find the value on the stack
if let Some(pos) = emitter.stack.find(&value) {
DebugVarLocation::Stack(pos as u8)
} else {
DebugVarLocation::Expression(vec![])
}
}
}
} else {
// No expression, try to find the value on the stack
if let Some(pos) = emitter.stack.find(&value) {
DebugVarLocation::Stack(pos as u8)
} else {
// Value not found, use expression
DebugVarLocation::Expression(vec![])
}
};

let mut debug_var = DebugVarInfo::new(var.name.to_string(), value_location);

// Set arg_index if this is a parameter
if let Some(arg_index) = var.arg_index {
debug_var.set_arg_index(arg_index + 1); // Convert to 1-based
}

// Set source location
if let Some(line) = core::num::NonZeroU32::new(var.line) {
use miden_assembly::debuginfo::{ColumnNumber, FileLineCol, LineNumber, Uri};
let uri = Uri::new(var.file.as_str());
let file_line_col = FileLineCol::new(
uri,
LineNumber::new(line.get()).unwrap_or_default(),
var.column.and_then(ColumnNumber::new).unwrap_or_default(),
);
debug_var.set_location(file_line_col);
}

// Emit the instruction
let inst = masm::Instruction::DebugVar(debug_var);
emitter.emit_op(masm::Op::Inst(Span::new(self.span(), inst)));

Ok(())
}
}

impl HirLowering for builtin::GlobalSymbol {
fn emit(&self, emitter: &mut BlockEmitter<'_>) -> Result<(), Report> {
let context = self.as_operation().context();
Expand Down
21 changes: 21 additions & 0 deletions dialects/debuginfo/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "midenc-dialect-debuginfo"
description = "Miden IR Debug Info Dialect"
version.workspace = true
rust-version.workspace = true
authors.workspace = true
repository.workspace = true
categories.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
edition.workspace = true

[features]
default = ["std"]
std = ["midenc-hir/std"]

[dependencies]
midenc-hir.workspace = true
paste.workspace = true
log.workspace = true
Loading