Skip to content
Merged
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: 5 additions & 1 deletion crates/llvm-export/src/export/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,18 @@ 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,
}

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.).
Expand Down
106 changes: 100 additions & 6 deletions crates/llvm-export/src/export/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -20,6 +21,8 @@ use pliron::{
uniqued_any,
};

use crate::ops::{DebugLocalTypeKind, DebugLocalVariableInfo};

use super::state::ModuleExportState;

impl<'a> ModuleExportState<'a> {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -125,19 +138,70 @@ 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;
}

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);
Expand Down Expand Up @@ -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<usize> {
if !self.debug_kind.line_tables_enabled() {
return None;
Expand Down
20 changes: 16 additions & 4 deletions crates/llvm-export/src/export/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
44 changes: 44 additions & 0 deletions crates/llvm-export/src/export/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -530,6 +537,43 @@ impl<'a> ModuleExportState<'a> {
Ok(())
}

fn emit_debug_declare_for_alloca(
&mut self,
op: &ops::AllocaOp,
value_names: &HashMap<Value, String>,
debug_scope: Option<usize>,
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,
Expand Down
8 changes: 8 additions & 0 deletions crates/llvm-export/src/export/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -70,8 +72,12 @@ pub(super) struct ModuleExportState<'a> {
pub(super) debug_subprogram_fallbacks: HashMap<usize, (i32, i32)>,
/// `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<DebugLocalTypeKind, usize>,
/// 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> {
Expand All @@ -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,
}
}

Expand Down
Loading
Loading