diff --git a/crates/llvm-export/src/export/config.rs b/crates/llvm-export/src/export/config.rs index 79e93d5e44..aec5f408eb 100644 --- a/crates/llvm-export/src/export/config.rs +++ b/crates/llvm-export/src/export/config.rs @@ -23,7 +23,7 @@ pub enum DebugKind { Off, /// Emit enough metadata for source-line breakpoints and stepping. LineTables, - /// Reserved for variable/type debug info. For now this emits line tables. + /// Emit line tables plus the supported local-variable/type metadata. Full, } @@ -31,6 +31,10 @@ impl DebugKind { pub fn line_tables_enabled(self) -> bool { !matches!(self, Self::Off) } + + pub fn variables_enabled(self) -> bool { + matches!(self, Self::Full) + } } /// Configuration trait for export backends (PTX, LTOIR, etc.). diff --git a/crates/llvm-export/src/export/debug.rs b/crates/llvm-export/src/export/debug.rs index 2a79664127..6b24c49175 100644 --- a/crates/llvm-export/src/export/debug.rs +++ b/crates/llvm-export/src/export/debug.rs @@ -3,11 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -//! Line-table debug metadata emission. +//! Debug metadata emission. //! -//! Stage 2 deliberately emits only the debug-info pieces needed to map machine -//! instructions back to source lines. Locals, argument variables, lexical -//! blocks, and Rust types need more MIR/type plumbing and belong to later stages. +//! Line-table mode emits just enough metadata to map machine instructions back +//! to source lines. Full mode builds on that with the first variable/type slice: +//! simple source locals are described with `llvm.dbg.declare` and compact DWARF +//! type nodes. use std::{ fmt::Write, @@ -20,6 +21,8 @@ use pliron::{ uniqued_any, }; +use crate::ops::{DebugLocalTypeKind, DebugLocalVariableInfo}; + use super::state::ModuleExportState; impl<'a> ModuleExportState<'a> { @@ -95,6 +98,16 @@ impl<'a> ModuleExportState<'a> { } } + pub(super) fn emit_debug_intrinsic_declarations(&self, output: &mut String) { + if self.debug_declare_used { + writeln!( + output, + "declare void @llvm.dbg.declare(metadata, metadata, metadata)" + ) + .unwrap(); + } + } + pub(super) fn emit_debug_metadata(&mut self, output: &mut String) { let Some(cu_id) = self.debug_compile_unit else { return; @@ -125,6 +138,47 @@ impl<'a> ModuleExportState<'a> { } } + pub(super) fn debug_local_variable_for_scope( + &mut self, + scope: usize, + loc: &Location, + info: &DebugLocalVariableInfo, + ) -> Option<(usize, usize)> { + if !self.debug_kind.variables_enabled() { + return None; + } + + let (path, pos) = self.source_position_from_location(loc)?; + if self + .debug_subprogram_files + .get(&scope) + .is_some_and(|scope_path| scope_path.as_path() != path) + { + return None; + } + + let file_id = self.ensure_debug_file(&path); + let type_id = self.ensure_debug_type(&info.ty); + let location_id = self.debug_location_for_scope(scope, loc)?; + let name = escape_debug_string(&info.name); + let arg = info + .argument_index + .map(|idx| format!("arg: {idx}, ")) + .unwrap_or_default(); + let id = self.alloc_metadata_id(); + + self.debug_nodes.push(( + id, + format!( + "!DILocalVariable(name: \"{name}\", {arg}scope: !{scope}, file: !{file_id}, \ + line: {}, type: !{type_id})", + pos.line + ), + )); + + Some((id, location_id)) + } + fn ensure_debug_compile_unit(&mut self, path: &Path) -> usize { if let Some(id) = self.debug_compile_unit { return id; @@ -132,12 +186,22 @@ impl<'a> ModuleExportState<'a> { let file_id = self.ensure_debug_file(path); let id = self.alloc_metadata_id(); + let is_optimized = if self.debug_kind.variables_enabled() { + "false" + } else { + "true" + }; + let emission_kind = if self.debug_kind.variables_enabled() { + "FullDebug" + } else { + "LineTablesOnly" + }; self.debug_nodes.push(( id, format!( "distinct !DICompileUnit(language: DW_LANG_Rust, file: !{file_id}, \ - producer: \"cuda-oxide\", isOptimized: true, runtimeVersion: 0, \ - emissionKind: LineTablesOnly)" + producer: \"cuda-oxide\", isOptimized: {is_optimized}, runtimeVersion: 0, \ + emissionKind: {emission_kind})" ), )); self.debug_compile_unit = Some(id); @@ -176,6 +240,36 @@ impl<'a> ModuleExportState<'a> { id } + fn ensure_debug_type(&mut self, ty: &DebugLocalTypeKind) -> usize { + if let Some(id) = self.debug_types.get(ty).copied() { + return id; + } + + let node = match ty { + DebugLocalTypeKind::Basic { + name, + size_bits, + encoding, + } => { + let name = escape_debug_string(name); + format!("!DIBasicType(name: \"{name}\", size: {size_bits}, encoding: {encoding})") + } + DebugLocalTypeKind::Pointer { name, size_bits } => { + let name = escape_debug_string(name); + format!( + "!DIDerivedType(tag: DW_TAG_pointer_type, name: \"{name}\", \ + baseType: null, size: {size_bits})" + ) + } + }; + + let id = self.alloc_metadata_id(); + self.debug_nodes.push((id, node)); + self.debug_types.insert(ty.clone(), id); + + id + } + fn debug_location_for_scope(&mut self, scope: usize, loc: &Location) -> Option { if !self.debug_kind.line_tables_enabled() { return None; diff --git a/crates/llvm-export/src/export/module.rs b/crates/llvm-export/src/export/module.rs index 7eff71d5b4..1bb63ce0d7 100644 --- a/crates/llvm-export/src/export/module.rs +++ b/crates/llvm-export/src/export/module.rs @@ -165,26 +165,32 @@ pub(super) fn export_module_with_externs_impl( } } - // 5. Emit attribute groups for convergent intrinsics used by module functions + // 5. Debug intrinsic declarations used by full-debug local variables. + if state.debug_declare_used { + writeln!(&mut output).unwrap(); + state.emit_debug_intrinsic_declarations(&mut output); + } + + // 6. Emit attribute groups for convergent intrinsics used by module functions // Note: Device extern declarations no longer get attribute groups - see section 2 comment. if state.convergent_used { writeln!(&mut output).unwrap(); writeln!(&mut output, "attributes #0 = {{ convergent }}").unwrap(); } - // 6. nvvm.annotations metadata + // 7. nvvm.annotations metadata if needs_nvvm_annotations(&state, emit_all_annotations) { writeln!(&mut output).unwrap(); emit_nvvm_annotations(&mut output, &mut state, emit_all_annotations); } - // 7. nvvmir.version metadata (if backend requires) + // 8. nvvmir.version metadata (if backend requires) if config.emit_nvvmir_version() { writeln!(&mut output).unwrap(); emit_nvvmir_version(&mut output, &mut state, config.nvvmir_version()); } - // 8. DWARF line-table metadata (if requested and source locations exist) + // 9. DWARF metadata (if requested and source locations exist) if state.has_debug_metadata() { writeln!(&mut output).unwrap(); state.emit_debug_metadata(&mut output); @@ -302,6 +308,12 @@ pub(super) fn export_module_to_string_with_config( } } + // Emit debug intrinsic declarations used by full-debug local variables. + if state.debug_declare_used { + writeln!(&mut output).unwrap(); + state.emit_debug_intrinsic_declarations(&mut output); + } + // Emit attributes section if convergent operations were used if state.convergent_used { writeln!(&mut output).unwrap(); diff --git a/crates/llvm-export/src/export/ops.rs b/crates/llvm-export/src/export/ops.rs index 79b9e6de27..f161a5cd0b 100644 --- a/crates/llvm-export/src/export/ops.rs +++ b/crates/llvm-export/src/export/ops.rs @@ -244,6 +244,10 @@ impl<'a> ModuleExportState<'a> { let op_loc = op_ref.loc(); let op_obj = Operation::get_op_dyn(op, self.ctx); let llvm_op = LlvmOp::try_from(op_obj.as_ref()).ok(); + let debug_alloca = llvm_op.as_ref().and_then(|llvm_op| match llvm_op { + LlvmOp::Alloca(op) => Some(*op), + _ => None, + }); let should_attach_debug = llvm_op.as_ref().is_some_and(LlvmOp::emits_real_instruction); let allow_scope_debug_fallback = llvm_op .as_ref() @@ -403,6 +407,9 @@ impl<'a> ModuleExportState<'a> { allow_scope_debug_fallback, ); } + if let Some(alloca) = debug_alloca { + self.emit_debug_declare_for_alloca(alloca, value_names, debug_scope, &op_loc, output)?; + } Ok(()) } @@ -530,6 +537,43 @@ impl<'a> ModuleExportState<'a> { Ok(()) } + fn emit_debug_declare_for_alloca( + &mut self, + op: &ops::AllocaOp, + value_names: &HashMap, + debug_scope: Option, + loc: &pliron::location::Location, + output: &mut String, + ) -> Result<(), String> { + if !self.debug_kind.variables_enabled() { + return Ok(()); + } + + let Some(scope) = debug_scope else { + return Ok(()); + }; + let Some(info) = crate::ops::debug_local_variable(self.ctx, op.get_operation()) else { + return Ok(()); + }; + let Some((var_id, loc_id)) = self.debug_local_variable_for_scope(scope, loc, &info) else { + return Ok(()); + }; + + let alloca_result = op.get_operation().deref(self.ctx).get_result(0); + let alloca_name = value_names + .get(&alloca_result) + .ok_or_else(|| "Missing alloca result name for debug declare".to_string())?; + + writeln!( + output, + " call void @llvm.dbg.declare(metadata ptr {alloca_name}, metadata !{var_id}, metadata !DIExpression()), !dbg !{loc_id}" + ) + .unwrap(); + self.debug_declare_used = true; + + Ok(()) + } + fn emit_gep( &mut self, op: &ops::GetElementPtrOp, diff --git a/crates/llvm-export/src/export/state.rs b/crates/llvm-export/src/export/state.rs index 5e8da45ead..f6087fe7fd 100644 --- a/crates/llvm-export/src/export/state.rs +++ b/crates/llvm-export/src/export/state.rs @@ -8,6 +8,8 @@ use pliron::{basic_block::BasicBlock, context::Ptr, value::Value}; use std::{collections::HashMap, path::PathBuf}; +use crate::ops::DebugLocalTypeKind; + use super::config::DebugKind; /// Map from block to its predecessors with the values passed to each predecessor. @@ -70,8 +72,12 @@ pub(super) struct ModuleExportState<'a> { pub(super) debug_subprogram_fallbacks: HashMap, /// `DILocation` nodes keyed by `(scope, line, column)`. pub(super) debug_locations: HashMap<(usize, i32, i32), usize>, + /// `DIType` nodes keyed by the simple debug type they describe. + pub(super) debug_types: HashMap, /// Numbered debug metadata definitions, in allocation order. pub(super) debug_nodes: Vec<(usize, String)>, + /// Whether any function emitted `llvm.dbg.declare`. + pub(super) debug_declare_used: bool, } impl<'a> ModuleExportState<'a> { @@ -98,7 +104,9 @@ impl<'a> ModuleExportState<'a> { debug_subprogram_files: HashMap::new(), debug_subprogram_fallbacks: HashMap::new(), debug_locations: HashMap::new(), + debug_types: HashMap::new(), debug_nodes: Vec::new(), + debug_declare_used: false, } } diff --git a/crates/llvm-export/src/lib.rs b/crates/llvm-export/src/lib.rs index d6f4aa21b6..46d557dd56 100644 --- a/crates/llvm-export/src/lib.rs +++ b/crates/llvm-export/src/lib.rs @@ -138,7 +138,7 @@ pub mod ops { pub use pliron::builtin::ops::ConstantOp; use pliron::{ - builtin::attributes::BoolAttr, + builtin::attributes::{BoolAttr, StringAttr}, context::{Context, Ptr}, identifier::Identifier, op::Op, @@ -190,6 +190,34 @@ pub mod ops { /// default for user-authored inline PTX. const INLINE_ASM_SIDEEFFECT_KEY: &str = "cuda_oxide_inline_asm_sideeffect"; + /// Debug type metadata for a local variable described by `llvm.dbg.declare`. + #[derive(Clone, Debug, Eq, Hash, PartialEq)] + pub enum DebugLocalTypeKind { + /// A scalar `DIBasicType`. + Basic { + name: String, + size_bits: u64, + encoding: &'static str, + }, + /// A pointer/reference `DIDerivedType`. + Pointer { name: String, size_bits: u64 }, + } + + /// Debug metadata attached to the alloca that stores a source local. + #[derive(Clone, Debug, Eq, PartialEq)] + pub struct DebugLocalVariableInfo { + pub name: String, + pub argument_index: Option, + pub ty: DebugLocalTypeKind, + } + + const DEBUG_LOCAL_NAME_KEY: &str = "cuda_oxide_debug_local_name"; + const DEBUG_LOCAL_ARG_KEY: &str = "cuda_oxide_debug_local_arg"; + const DEBUG_LOCAL_TYPE_KIND_KEY: &str = "cuda_oxide_debug_local_type_kind"; + const DEBUG_LOCAL_TYPE_NAME_KEY: &str = "cuda_oxide_debug_local_type_name"; + const DEBUG_LOCAL_TYPE_SIZE_KEY: &str = "cuda_oxide_debug_local_type_size_bits"; + const DEBUG_LOCAL_TYPE_ENCODING_KEY: &str = "cuda_oxide_debug_local_type_encoding"; + /// Stamp the ABI alignment (bytes) onto a memory op. pub fn set_op_alignment(ctx: &mut Context, op: Ptr, align: u32) { let key = Identifier::try_new(OP_ALIGNMENT_KEY.to_string()).expect("valid identifier"); @@ -225,6 +253,95 @@ pub mod ops { .unwrap_or(true) } + /// Attach source-local debug metadata to a memory slot op. + pub fn set_debug_local_variable( + ctx: &mut Context, + op: Ptr, + info: DebugLocalVariableInfo, + ) { + set_string_attr(ctx, op, DEBUG_LOCAL_NAME_KEY, info.name); + if let Some(arg) = info.argument_index { + set_string_attr(ctx, op, DEBUG_LOCAL_ARG_KEY, arg.to_string()); + } + + match info.ty { + DebugLocalTypeKind::Basic { + name, + size_bits, + encoding, + } => { + set_string_attr(ctx, op, DEBUG_LOCAL_TYPE_KIND_KEY, "basic".to_string()); + set_string_attr(ctx, op, DEBUG_LOCAL_TYPE_NAME_KEY, name); + set_string_attr(ctx, op, DEBUG_LOCAL_TYPE_SIZE_KEY, size_bits.to_string()); + set_string_attr(ctx, op, DEBUG_LOCAL_TYPE_ENCODING_KEY, encoding.to_string()); + } + DebugLocalTypeKind::Pointer { name, size_bits } => { + set_string_attr(ctx, op, DEBUG_LOCAL_TYPE_KIND_KEY, "pointer".to_string()); + set_string_attr(ctx, op, DEBUG_LOCAL_TYPE_NAME_KEY, name); + set_string_attr(ctx, op, DEBUG_LOCAL_TYPE_SIZE_KEY, size_bits.to_string()); + } + } + } + + /// Read source-local debug metadata from a memory slot op, if present. + pub fn debug_local_variable( + ctx: &Context, + op: Ptr, + ) -> Option { + let name = get_string_attr(ctx, op, DEBUG_LOCAL_NAME_KEY)?; + let argument_index = + get_string_attr(ctx, op, DEBUG_LOCAL_ARG_KEY).and_then(|arg| arg.parse::().ok()); + let kind = get_string_attr(ctx, op, DEBUG_LOCAL_TYPE_KIND_KEY)?; + let type_name = get_string_attr(ctx, op, DEBUG_LOCAL_TYPE_NAME_KEY)?; + let size_bits = get_string_attr(ctx, op, DEBUG_LOCAL_TYPE_SIZE_KEY)? + .parse() + .ok()?; + + let ty = match kind.as_str() { + "basic" => DebugLocalTypeKind::Basic { + name: type_name, + size_bits, + encoding: debug_type_encoding(ctx, op)?, + }, + "pointer" => DebugLocalTypeKind::Pointer { + name: type_name, + size_bits, + }, + _ => return None, + }; + + Some(DebugLocalVariableInfo { + name, + argument_index, + ty, + }) + } + + fn set_string_attr(ctx: &mut Context, op: Ptr, key: &str, value: String) { + let key = Identifier::try_new(key.to_string()).expect("valid identifier"); + op.deref_mut(ctx) + .attributes + .set(key, StringAttr::new(value)); + } + + fn get_string_attr(ctx: &Context, op: Ptr, key: &str) -> Option { + let key = Identifier::try_new(key.to_string()).expect("valid identifier"); + op.deref(ctx) + .attributes + .get::(&key) + .map(|a| String::from((*a).clone())) + } + + fn debug_type_encoding(ctx: &Context, op: Ptr) -> Option<&'static str> { + match get_string_attr(ctx, op, DEBUG_LOCAL_TYPE_ENCODING_KEY)?.as_str() { + "DW_ATE_boolean" => Some("DW_ATE_boolean"), + "DW_ATE_float" => Some("DW_ATE_float"), + "DW_ATE_signed" => Some("DW_ATE_signed"), + "DW_ATE_unsigned" => Some("DW_ATE_unsigned"), + _ => None, + } + } + /// Alignment helpers re-homed from the pre-migration local `GlobalOp`. /// Upstream `GlobalOp` carries type/linkage/addrspace but no alignment, so /// we keep the alignment in the op's generic attribute dictionary. Address diff --git a/crates/llvm-export/tests/export_test.rs b/crates/llvm-export/tests/export_test.rs index ef528d987e..fbadcca2cb 100644 --- a/crates/llvm-export/tests/export_test.rs +++ b/crates/llvm-export/tests/export_test.rs @@ -10,10 +10,10 @@ use llvm_export::{ export_module_to_string_with_config, }, ops::{ - AddressOfOp, BrOp, CallOp, FuncOp, GepIndex, GetElementPtrOp, GlobalOp, InlineAsmOp, - ReturnOp, + AddressOfOp, AllocaOp, BrOp, CallOp, ConstantOp, DebugLocalTypeKind, + DebugLocalVariableInfo, FuncOp, GepIndex, GetElementPtrOp, GlobalOp, InlineAsmOp, ReturnOp, }, - types::{FuncType, VoidType}, + types::{FuncType, PointerType, VoidType}, }; use pliron::{ basic_block::BasicBlock, @@ -438,6 +438,184 @@ fn debug_metadata_shares_allocator_with_nvvm_metadata() { ); } +#[test] +fn full_debug_metadata_emits_dbg_declare_for_tagged_allocas() { + let mut ctx = Context::new(); + + let module = ModuleOp::new(&mut ctx, "test_module".try_into().unwrap()); + let module_region = module.get_operation().deref(&ctx).get_region(0); + let module_block = { + let region = module_region.deref(&ctx); + region.iter(&ctx).next().unwrap() + }; + + let void_ty = VoidType::get(&ctx); + let func_ty = FuncType::get(&mut ctx, void_ty.to_ptr(), vec![], false); + let func = FuncOp::new(&mut ctx, "debug_kernel".try_into().unwrap(), func_ty); + let func_loc = src_location(&mut ctx, "/tmp/cuda-oxide/tests/kernel.rs", 30, 1); + func.get_operation().deref_mut(&ctx).set_loc(func_loc); + + let entry = func.get_or_create_entry_block(&mut ctx); + let i32_ty = IntegerType::get(&mut ctx, 32, Signedness::Signless); + let one_attr = IntegerAttr::new(i32_ty, APInt::from_u32(1, NonZero::new(32).unwrap())); + let one = ConstantOp::new(&mut ctx, one_attr.into()); + one.get_operation().insert_at_back(entry, &ctx); + let one_val = one.get_operation().deref(&ctx).get_result(0); + + let tid = AllocaOp::new(&mut ctx, i32_ty.into(), one_val); + let tid_loc = src_location(&mut ctx, "/tmp/cuda-oxide/tests/kernel.rs", 31, 9); + tid.get_operation().deref_mut(&ctx).set_loc(tid_loc); + llvm_export::ops::set_debug_local_variable( + &mut ctx, + tid.get_operation(), + DebugLocalVariableInfo { + name: "tid".to_string(), + argument_index: Some(1), + ty: DebugLocalTypeKind::Basic { + name: "u32".to_string(), + size_bits: 32, + encoding: "DW_ATE_unsigned", + }, + }, + ); + tid.get_operation().insert_at_back(entry, &ctx); + + let ptr_ty = PointerType::get(&mut ctx, 0); + let ptr = AllocaOp::new(&mut ctx, ptr_ty.into(), one_val); + let ptr_loc = src_location(&mut ctx, "/tmp/cuda-oxide/tests/kernel.rs", 32, 9); + ptr.get_operation().deref_mut(&ctx).set_loc(ptr_loc); + llvm_export::ops::set_debug_local_variable( + &mut ctx, + ptr.get_operation(), + DebugLocalVariableInfo { + name: "ptr".to_string(), + argument_index: None, + ty: DebugLocalTypeKind::Pointer { + name: "*mut f32".to_string(), + size_bits: 64, + }, + }, + ); + ptr.get_operation().insert_at_back(entry, &ctx); + + let ret = ReturnOp::new(&mut ctx, None); + let ret_loc = src_location(&mut ctx, "/tmp/cuda-oxide/tests/kernel.rs", 33, 1); + ret.get_operation().deref_mut(&ctx).set_loc(ret_loc); + ret.get_operation().insert_at_back(entry, &ctx); + + func.get_operation().insert_at_back(module_block, &ctx); + + let config = DebugConfig { + inner: PtxExportConfig, + debug_kind: DebugKind::Full, + }; + let ir = + export_module_to_string_with_config(&ctx, &module, &config).expect("debug export succeeds"); + + assert!( + ir.contains("emissionKind: FullDebug"), + "full debug should request full DWARF metadata:\n{ir}" + ); + assert!( + ir.contains("isOptimized: false"), + "full debug export should describe the unoptimized debug path:\n{ir}" + ); + assert!( + ir.contains("declare void @llvm.dbg.declare(metadata, metadata, metadata)"), + "full debug should declare the debug intrinsic it calls:\n{ir}" + ); + assert!( + ir.contains("call void @llvm.dbg.declare(metadata ptr %"), + "tagged allocas should be bound to variables with dbg.declare:\n{ir}" + ); + assert!( + ir.contains("!DILocalVariable(name: \"tid\", arg: 1, scope: !"), + "argument debug metadata should preserve the argument number:\n{ir}" + ); + assert!( + ir.contains("!DILocalVariable(name: \"ptr\", scope: !"), + "local debug metadata should omit the arg field:\n{ir}" + ); + assert!( + ir.contains("!DIBasicType(name: \"u32\", size: 32, encoding: DW_ATE_unsigned)"), + "basic integer variables should get DIBasicType metadata:\n{ir}" + ); + assert!( + ir.contains( + "!DIDerivedType(tag: DW_TAG_pointer_type, name: \"*mut f32\", baseType: null, size: 64)" + ), + "pointer variables should get a pointer DIType:\n{ir}" + ); +} + +#[test] +fn line_table_debug_metadata_ignores_tagged_alloca_variables() { + let mut ctx = Context::new(); + + let module = ModuleOp::new(&mut ctx, "test_module".try_into().unwrap()); + let module_region = module.get_operation().deref(&ctx).get_region(0); + let module_block = { + let region = module_region.deref(&ctx); + region.iter(&ctx).next().unwrap() + }; + + let void_ty = VoidType::get(&ctx); + let func_ty = FuncType::get(&mut ctx, void_ty.to_ptr(), vec![], false); + let func = FuncOp::new(&mut ctx, "debug_kernel".try_into().unwrap(), func_ty); + let func_loc = src_location(&mut ctx, "/tmp/cuda-oxide/tests/kernel.rs", 40, 1); + func.get_operation().deref_mut(&ctx).set_loc(func_loc); + + let entry = func.get_or_create_entry_block(&mut ctx); + let i32_ty = IntegerType::get(&mut ctx, 32, Signedness::Signless); + let one_attr = IntegerAttr::new(i32_ty, APInt::from_u32(1, NonZero::new(32).unwrap())); + let one = ConstantOp::new(&mut ctx, one_attr.into()); + one.get_operation().insert_at_back(entry, &ctx); + let one_val = one.get_operation().deref(&ctx).get_result(0); + + let local = AllocaOp::new(&mut ctx, i32_ty.into(), one_val); + let local_loc = src_location(&mut ctx, "/tmp/cuda-oxide/tests/kernel.rs", 41, 9); + local.get_operation().deref_mut(&ctx).set_loc(local_loc); + llvm_export::ops::set_debug_local_variable( + &mut ctx, + local.get_operation(), + DebugLocalVariableInfo { + name: "x".to_string(), + argument_index: None, + ty: DebugLocalTypeKind::Basic { + name: "i32".to_string(), + size_bits: 32, + encoding: "DW_ATE_signed", + }, + }, + ); + local.get_operation().insert_at_back(entry, &ctx); + + ReturnOp::new(&mut ctx, None) + .get_operation() + .insert_at_back(entry, &ctx); + func.get_operation().insert_at_back(module_block, &ctx); + + let config = DebugConfig { + inner: PtxExportConfig, + debug_kind: DebugKind::LineTables, + }; + let ir = + export_module_to_string_with_config(&ctx, &module, &config).expect("debug export succeeds"); + + assert!( + ir.contains("emissionKind: LineTablesOnly"), + "line-table mode should stay line-table-only:\n{ir}" + ); + assert!( + !ir.contains("llvm.dbg.declare"), + "line-table mode should not emit variable bindings:\n{ir}" + ); + assert!( + !ir.contains("DILocalVariable"), + "line-table mode should not emit local-variable metadata:\n{ir}" + ); +} + #[test] fn line_table_debug_metadata_adds_fallback_locations_to_calls() { let mut ctx = Context::new(); diff --git a/crates/mir-importer/src/pipeline.rs b/crates/mir-importer/src/pipeline.rs index 0a4f5e55cc..b9bfb68a17 100644 --- a/crates/mir-importer/src/pipeline.rs +++ b/crates/mir-importer/src/pipeline.rs @@ -174,8 +174,7 @@ pub struct PipelineConfig { /// Currently supports NVVM 20 dialect (Blackwell+). Architecture is /// controlled by `--arch` flag. pub emit_nvvm_ir: bool, - /// Device debug metadata tier. Full variable/type debug is not implemented - /// yet, so `Full` currently emits line tables. + /// Device debug metadata tier. pub debug_kind: DebugKind, } @@ -267,6 +266,7 @@ pub fn run_pipeline( func.is_kernel, Some(&func.export_name), &mut legaliser, + config.debug_kind, ) .map_err(|e| { // Use .disp(&ctx) for rich error formatting with location and backtrace @@ -306,32 +306,39 @@ pub fn run_pipeline( } // Step 4.5: Run mem2reg (promote `mir.alloca` + `mir.load`/`mir.store` - // chains back to SSA values). This erases every promotable alloca and - // replaces each load with the reaching definition, leaving the subsequent - // `dialect-mir` → LLVM dialect lowering to handle only genuinely - // address-taken locals. - if config.verbose { - eprintln!("\n=== Running mem2reg ==="); - } - // pliron's pass infra now threads an AnalysisManager through mem2reg - // (caches dominator trees etc.); we run it standalone, so a fresh empty - // manager suffices. The returned IRStatus (Changed/Unchanged) is discarded. - let mut analyses = pliron::pass_manager::AnalysisManager::default(); - pliron::opts::mem2reg::mem2reg(module_op_ptr, &mut ctx, &mut analyses).map_err(|e| { - PipelineError::Verification { - name: "mem2reg".to_string(), - message: e.disp(&ctx).to_string(), - operation: None, + // chains back to SSA values). Full debug deliberately skips this first: + // the initial variable-info implementation emits `dbg.declare` against + // local slots, so erasing those slots would make the debug locations false. + // A later optimized-debug stage can teach Pliron mem2reg to rewrite those + // locations into `dbg.value`, like LLVM's mem2reg does. + if config.debug_kind.variables_enabled() { + if config.verbose { + eprintln!("\n=== Skipping mem2reg (full device debug preserves local slots) ==="); } - })?; - if config.verbose { - eprintln!("mem2reg successful ✓"); - } - if config.show_mir_dialect { - eprintln!("\n=== dialect-mir module (after mem2reg) ==="); - eprintln!("{}", module_op_ptr.deref(&ctx).disp(&ctx)); + } else { + if config.verbose { + eprintln!("\n=== Running mem2reg ==="); + } + // pliron's pass infra now threads an AnalysisManager through mem2reg + // (caches dominator trees etc.); we run it standalone, so a fresh empty + // manager suffices. The returned IRStatus (Changed/Unchanged) is discarded. + let mut analyses = pliron::pass_manager::AnalysisManager::default(); + pliron::opts::mem2reg::mem2reg(module_op_ptr, &mut ctx, &mut analyses).map_err(|e| { + PipelineError::Verification { + name: "mem2reg".to_string(), + message: e.disp(&ctx).to_string(), + operation: None, + } + })?; + if config.verbose { + eprintln!("mem2reg successful ✓"); + } + if config.show_mir_dialect { + eprintln!("\n=== dialect-mir module (after mem2reg) ==="); + eprintln!("{}", module_op_ptr.deref(&ctx).disp(&ctx)); + } + verify_operation(&ctx, module_op_ptr, "module post-mem2reg")?; } - verify_operation(&ctx, module_op_ptr, "module post-mem2reg")?; // Step 5: Lower dialect-mir → LLVM dialect. if config.verbose { @@ -439,7 +446,7 @@ pub fn run_pipeline( let ptx_path = config .output_dir .join(format!("{}.ptx", config.output_name)); - let target = generate_ptx(&ll_path, &ptx_path)?; + let target = generate_ptx(&ll_path, &ptx_path, config.debug_kind)?; if config.verbose { eprintln!( "✓ PTX written to {} (target: {})", @@ -969,7 +976,11 @@ fn optimize_ll(ll_path: &Path, toolchain: &LlvmToolchain, verbose: bool) -> Opti /// `CUDA_OXIDE_TARGET` override, else the detected-GPU hint /// (`CUDA_OXIDE_DEVICE_ARCH`) when that GPU can run the kernel, else the minimum /// arch the IR's features require (`select_target`). -fn generate_ptx(ll_path: &Path, ptx_path: &Path) -> Result { +fn generate_ptx( + ll_path: &Path, + ptx_path: &Path, + debug_kind: DebugKind, +) -> Result { // Explicit, hard override: `--arch` or a parent-set `CUDA_OXIDE_TARGET`. let explicit_override = std::env::var("CUDA_OXIDE_TARGET").ok(); // Advisory hint: the arch of the GPU in this machine, forwarded by @@ -1047,9 +1058,13 @@ fn generate_ptx(ll_path: &Path, ptx_path: &Path) -> Result Result Ok(target.to_string()), + Ok(output) if output.status.success() => { + if matches!(debug_kind, DebugKind::LineTables) { + strip_target_debug_from_ptx(ptx_path)?; + if verbose { + eprintln!( + "line-table debug: stripped PTX target debug flag; source line tables remain" + ); + } + } + Ok(target.to_string()) + } Ok(output) => Err(PipelineError::PtxGeneration(format!( "{} failed:\n{}", llc_desc, @@ -1093,6 +1118,70 @@ fn generate_ptx(ll_path: &Path, ptx_path: &Path) -> Result Result<(), PipelineError> { + let ptx = std::fs::read_to_string(ptx_path).map_err(|e| { + PipelineError::PtxGeneration(format!( + "failed to read PTX for line-table debug cleanup ({}): {e}", + ptx_path.display() + )) + })?; + let stripped = strip_target_debug_from_ptx_text(&ptx); + if stripped != ptx { + std::fs::write(ptx_path, stripped).map_err(|e| { + PipelineError::PtxGeneration(format!( + "failed to write PTX after line-table debug cleanup ({}): {e}", + ptx_path.display() + )) + })?; + } + Ok(()) +} + +fn strip_target_debug_from_ptx_text(ptx: &str) -> String { + let mut out = String::with_capacity(ptx.len()); + for line in ptx.split_inclusive('\n') { + let (line_body, newline) = line + .strip_suffix('\n') + .map_or((line, ""), |without_newline| (without_newline, "\n")); + out.push_str(&strip_target_debug_from_ptx_line(line_body)); + out.push_str(newline); + } + out +} + +fn strip_target_debug_from_ptx_line(line: &str) -> String { + let indent_len = line.len() - line.trim_start().len(); + let indent = &line[..indent_len]; + let body = &line[indent_len..]; + let Some(rest) = body.strip_prefix(".target") else { + return line.to_string(); + }; + + let mut parts = rest.split(','); + let Some(arch) = parts.next() else { + return line.to_string(); + }; + + let options: Vec<&str> = parts + .map(str::trim) + .filter(|option| *option != "debug") + .collect(); + if !rest + .split(',') + .skip(1) + .any(|option| option.trim() == "debug") + { + return line.to_string(); + } + + let mut stripped = format!("{indent}.target{arch}"); + for option in options { + stripped.push_str(", "); + stripped.push_str(option); + } + stripped +} + /// Recursively finds the innermost operation that failed verification. /// /// Helps produce better error messages by pointing to the specific failing @@ -1198,6 +1287,38 @@ mod tests { assert_eq!(config.debug_kind, DebugKind::Off); } + #[test] + fn line_table_ptx_cleanup_strips_only_target_debug_flag() { + let ptx = "\ +.version 8.9 +.target sm_120a, debug +.address_size 64 + +.section .debug_info +\t.b8 1; +"; + + let stripped = strip_target_debug_from_ptx_text(ptx); + + assert!( + stripped.contains(".target sm_120a\n"), + "line-table mode should not ask the driver for debug compilation:\n{stripped}" + ); + assert!( + stripped.contains(".section .debug_info"), + "line-table mode must keep the emitted DWARF sections:\n{stripped}" + ); + } + + #[test] + fn line_table_ptx_cleanup_preserves_other_target_options() { + let ptx = ".target sm_90a, texmode_independent, debug\n"; + + let stripped = strip_target_debug_from_ptx_text(ptx); + + assert_eq!(stripped, ".target sm_90a, texmode_independent\n"); + } + #[test] fn test_device_extern_decl_converts_to_export_decl() { let decl = DeviceExternDecl { diff --git a/crates/mir-importer/src/translator/body.rs b/crates/mir-importer/src/translator/body.rs index 70e26bc31c..0c7283056e 100644 --- a/crates/mir-importer/src/translator/body.rs +++ b/crates/mir-importer/src/translator/body.rs @@ -26,6 +26,8 @@ use crate::translator::location::span_to_location; use crate::translator::values::{self, SlotAddrSpaceMap, ValueMap}; use dialect_mir::ops::MirFuncOp; use dialect_mir::types::address_space; +use llvm_export::export::DebugKind; +use llvm_export::ops::{DebugLocalTypeKind, DebugLocalVariableInfo}; use pliron::basic_block::BasicBlock; use pliron::builtin::op_interfaces::SymbolOpInterface; use pliron::context::{Context, Ptr}; @@ -39,7 +41,8 @@ use pliron::operation::Operation; use rustc_public::CrateDef; use rustc_public::mir; use rustc_public::mir::mono; -use rustc_public::ty::{ConstantKind, RigidTy, TyKind}; +use rustc_public::ty::{ConstantKind, FloatTy, IntTy, RigidTy, Ty, TyKind, UintTy}; +use std::collections::HashMap; /// Cluster dimensions extracted from `#[cluster(x,y,z)]` attribute. /// @@ -204,6 +207,171 @@ fn compute_reachable_blocks(body: &mir::Body) -> std::collections::BTreeSet _3 +/// ``` +/// +/// Fragments/projections need `DIExpression(DW_OP_LLVM_fragment, ...)` and more +/// value-location tracking, so they are intentionally skipped until the basic +/// local/argument path is solid. +fn collect_debug_locals( + ctx: &mut Context, + body: &mir::Body, +) -> HashMap { + let mut locals = HashMap::new(); + + for info in &body.var_debug_info { + if info.composite.is_some() { + continue; + } + + let Some(local) = info.local() else { + continue; + }; + let local_idx: usize = local; + if local_idx == 0 { + continue; + } + + let Some(decl) = body.local_decl(local) else { + continue; + }; + let Some(ty) = debug_type_for_ty(&decl.ty) else { + continue; + }; + + let name = info.name.to_string(); + if name.is_empty() { + continue; + } + + locals.entry(local).or_insert_with(|| LocalDebugInfo { + variable: DebugLocalVariableInfo { + name, + argument_index: info.argument_index, + ty, + }, + loc: span_to_location(ctx, info.source_info.span), + }); + } + + locals +} + +fn debug_type_for_ty(ty: &Ty) -> Option { + match ty.kind() { + TyKind::RigidTy(RigidTy::Bool) => Some(DebugLocalTypeKind::Basic { + name: "bool".to_string(), + size_bits: 8, + encoding: "DW_ATE_boolean", + }), + TyKind::RigidTy(RigidTy::Int(int_ty)) => Some(DebugLocalTypeKind::Basic { + name: int_name(int_ty).to_string(), + size_bits: (int_ty.num_bytes() * 8) as u64, + encoding: "DW_ATE_signed", + }), + TyKind::RigidTy(RigidTy::Uint(uint_ty)) => Some(DebugLocalTypeKind::Basic { + name: uint_name(uint_ty).to_string(), + size_bits: (uint_ty.num_bytes() * 8) as u64, + encoding: "DW_ATE_unsigned", + }), + TyKind::RigidTy(RigidTy::Float(float_ty)) => Some(DebugLocalTypeKind::Basic { + name: float_name(float_ty).to_string(), + size_bits: float_size_bits(float_ty), + encoding: "DW_ATE_float", + }), + TyKind::RigidTy(RigidTy::RawPtr(pointee, mutability)) => { + Some(DebugLocalTypeKind::Pointer { + name: raw_pointer_name(pointee, mutability), + size_bits: 64, + }) + } + TyKind::RigidTy(RigidTy::Ref(_, pointee, mutability)) => { + Some(DebugLocalTypeKind::Pointer { + name: reference_name(pointee, mutability), + size_bits: 64, + }) + } + _ => None, + } +} + +fn int_name(ty: IntTy) -> &'static str { + match ty { + IntTy::Isize => "isize", + IntTy::I8 => "i8", + IntTy::I16 => "i16", + IntTy::I32 => "i32", + IntTy::I64 => "i64", + IntTy::I128 => "i128", + } +} + +fn uint_name(ty: UintTy) -> &'static str { + match ty { + UintTy::Usize => "usize", + UintTy::U8 => "u8", + UintTy::U16 => "u16", + UintTy::U32 => "u32", + UintTy::U64 => "u64", + UintTy::U128 => "u128", + } +} + +fn float_name(ty: FloatTy) -> &'static str { + match ty { + FloatTy::F16 => "f16", + FloatTy::F32 => "f32", + FloatTy::F64 => "f64", + FloatTy::F128 => "f128", + } +} + +fn float_size_bits(ty: FloatTy) -> u64 { + match ty { + FloatTy::F16 => 16, + FloatTy::F32 => 32, + FloatTy::F64 => 64, + FloatTy::F128 => 128, + } +} + +fn raw_pointer_name(pointee: Ty, mutability: mir::Mutability) -> String { + let mutability = match mutability { + mir::Mutability::Mut => "mut ", + mir::Mutability::Not => "const ", + }; + format!("*{mutability}{}", simple_type_name(&pointee)) +} + +fn reference_name(pointee: Ty, mutability: mir::Mutability) -> String { + let mutability = match mutability { + mir::Mutability::Mut => "mut ", + mir::Mutability::Not => "", + }; + format!("&{mutability}{}", simple_type_name(&pointee)) +} + +fn simple_type_name(ty: &Ty) -> &'static str { + match ty.kind() { + TyKind::RigidTy(RigidTy::Bool) => "bool", + TyKind::RigidTy(RigidTy::Int(int_ty)) => int_name(int_ty), + TyKind::RigidTy(RigidTy::Uint(uint_ty)) => uint_name(uint_ty), + TyKind::RigidTy(RigidTy::Float(float_ty)) => float_name(float_ty), + _ => "_", + } +} + /// Emit one `mir.alloca` per non-ZST MIR local at the top of the entry block, /// then store each function argument into its backing slot. /// @@ -238,8 +406,14 @@ fn emit_entry_allocas( entry_block: Ptr, num_args: usize, value_map: &mut ValueMap, + debug_kind: DebugKind, ) -> Option> { let mut prev_op: Option> = None; + let debug_locals = if debug_kind.variables_enabled() { + collect_debug_locals(ctx, body) + } else { + HashMap::new() + }; // Pre-scan the body once: for each local whose translated slot type is a // pointer, infer the address space from the *writes* into it rather than @@ -267,6 +441,10 @@ fn emit_entry_allocas( let mir_ty = values::align_pointer_addr_space(ctx, mir_ty, target); let (op, slot) = ValueMap::emit_alloca(ctx, mir_ty, entry_block, prev_op); + if let Some(info) = debug_locals.get(&local) { + llvm_export::ops::set_debug_local_variable(ctx, op, info.variable.clone()); + op.deref_mut(ctx).set_loc(info.loc.clone()); + } prev_op = Some(op); value_map.set_slot(local, slot); } @@ -309,6 +487,7 @@ pub fn translate_body( is_kernel: bool, override_name: Option<&str>, legaliser: &mut Legaliser, + debug_kind: DebugKind, ) -> TranslationResult> { // Create a value map to track MIR locals -> pliron IR values let num_locals = body.locals().len(); @@ -570,7 +749,14 @@ pub fn translate_body( // // The `mem2reg` pass in `pipeline.rs` promotes the scalar slots back into // SSA before LLVM lowering. - let entry_last_op = emit_entry_allocas(ctx, body, block_map[0], num_args, &mut value_map); + let entry_last_op = emit_entry_allocas( + ctx, + body, + block_map[0], + num_args, + &mut value_map, + debug_kind, + ); // ------------------------------------------------------------------------- // PHASE 2: Translate reachable blocks diff --git a/crates/mir-importer/src/translator/mod.rs b/crates/mir-importer/src/translator/mod.rs index f32a2b3533..744166a561 100644 --- a/crates/mir-importer/src/translator/mod.rs +++ b/crates/mir-importer/src/translator/mod.rs @@ -54,6 +54,7 @@ pub mod types; pub mod values; use crate::error::{TranslationErr, TranslationResult}; +use llvm_export::export::DebugKind; use pliron::context::{Context, Ptr}; use pliron::identifier::Legaliser; use pliron::input_error_noloc; @@ -96,7 +97,15 @@ pub fn translate_function( register_dialects(ctx); // Translate the function body - let func_op = body::translate_body(ctx, body, instance, is_kernel, None, legaliser)?; + let func_op = body::translate_body( + ctx, + body, + instance, + is_kernel, + None, + legaliser, + DebugKind::Off, + )?; // Create a builtin.module operation using ModuleOp::new let module_name = instance.name(); diff --git a/crates/mir-lower/src/convert/ops/memory.rs b/crates/mir-lower/src/convert/ops/memory.rs index b7cf42512b..0a004cabfd 100644 --- a/crates/mir-lower/src/convert/ops/memory.rs +++ b/crates/mir-lower/src/convert/ops/memory.rs @@ -111,6 +111,12 @@ fn copy_alignment(ctx: &mut Context, mir_op: Ptr, llvm_op: Ptr, llvm_op: Ptr) { + if let Some(info) = llvm_export::ops::debug_local_variable(ctx, mir_op) { + llvm_export::ops::set_debug_local_variable(ctx, llvm_op, info); + } +} + /// Convert `mir.load` to `llvm.load`. /// /// Takes a single pointer operand and returns the loaded value. @@ -171,6 +177,7 @@ pub(crate) fn convert_alloca( let alloca = llvm::AllocaOp::new(ctx, llvm_pointee, one_val); copy_alignment(ctx, op, alloca.get_operation()); + copy_debug_local_variable(ctx, op, alloca.get_operation()); rewriter.insert_operation(ctx, alloca.get_operation()); rewriter.replace_operation(ctx, op, alloca.get_operation()); @@ -757,6 +764,57 @@ mod tests { assert!(elem_ty.deref(&ctx).is::()); } + #[test] + fn convert_alloca_preserves_debug_local_metadata() { + let mut ctx = make_ctx(); + let i32_ty: Ptr = IntegerType::get(&mut ctx, 32, Signedness::Signless).into(); + let mir_ptr_ty = MirPtrType::get_generic(&mut ctx, i32_ty, true); + + let (module_ptr, block) = build_kernel(&mut ctx, vec![], vec![]); + + let alloca_op = Operation::new( + &mut ctx, + mir::MirAllocaOp::get_concrete_op_info(), + vec![mir_ptr_ty.into()], + vec![], + vec![], + 0, + ); + llvm::set_debug_local_variable( + &mut ctx, + alloca_op, + llvm::DebugLocalVariableInfo { + name: "x".to_string(), + argument_index: Some(1), + ty: llvm::DebugLocalTypeKind::Basic { + name: "i32".to_string(), + size_bits: 32, + encoding: "DW_ATE_signed", + }, + }, + ); + alloca_op.insert_at_back(block, &ctx); + append_mir_return(&mut ctx, block, vec![]); + + crate::lower_mir_to_llvm(&mut ctx, module_ptr).expect("lowering failed"); + + let body = kernel_blocks(&ctx, module_ptr); + let alloca = find_first::(&ctx, &body).unwrap(); + let info = llvm::debug_local_variable(&ctx, alloca.get_operation()) + .expect("debug local metadata should survive lowering"); + + assert_eq!(info.name, "x"); + assert_eq!(info.argument_index, Some(1)); + assert_eq!( + info.ty, + llvm::DebugLocalTypeKind::Basic { + name: "i32".to_string(), + size_bits: 32, + encoding: "DW_ATE_signed", + } + ); + } + #[test] fn convert_store_lowers_to_llvm_store() { let mut ctx = make_ctx();