use std::fmt;
use crate::error::{CudaError, CudaResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum DebugLevel {
Off,
Error,
Warn,
#[default]
Info,
Debug,
Trace,
}
impl fmt::Display for DebugLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Off => write!(f, "OFF"),
Self::Error => write!(f, "ERROR"),
Self::Warn => write!(f, "WARN"),
Self::Info => write!(f, "INFO"),
Self::Debug => write!(f, "DEBUG"),
Self::Trace => write!(f, "TRACE"),
}
}
}
#[derive(Debug, Clone)]
pub struct KernelDebugConfig {
pub debug_level: DebugLevel,
pub enable_bounds_check: bool,
pub enable_nan_check: bool,
pub enable_inf_check: bool,
pub enable_race_detection: bool,
pub print_buffer_size: usize,
pub max_print_per_thread: usize,
}
impl Default for KernelDebugConfig {
fn default() -> Self {
Self {
debug_level: DebugLevel::Info,
enable_bounds_check: true,
enable_nan_check: true,
enable_inf_check: true,
enable_race_detection: false,
print_buffer_size: 1024 * 1024, max_print_per_thread: 32,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum DebugEventType {
OutOfBounds {
address: u64,
size: usize,
},
NanDetected {
register: String,
value: f64,
},
InfDetected {
register: String,
},
RaceCondition {
address: u64,
},
Assertion {
condition: String,
file: String,
line: u32,
},
Printf {
format: String,
},
Breakpoint {
id: u32,
},
}
impl DebugEventType {
fn tag(&self) -> &'static str {
match self {
Self::OutOfBounds { .. } => "OOB",
Self::NanDetected { .. } => "NaN",
Self::InfDetected { .. } => "Inf",
Self::RaceCondition { .. } => "RACE",
Self::Assertion { .. } => "ASSERT",
Self::Printf { .. } => "PRINTF",
Self::Breakpoint { .. } => "BP",
}
}
fn same_kind(&self, other: &Self) -> bool {
std::mem::discriminant(self) == std::mem::discriminant(other)
}
}
#[derive(Debug, Clone)]
pub struct DebugEvent {
pub event_type: DebugEventType,
pub thread_id: (u32, u32, u32),
pub block_id: (u32, u32, u32),
pub timestamp_ns: u64,
pub message: String,
}
impl fmt::Display for DebugEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{tag}] block({bx},{by},{bz}) thread({tx},{ty},{tz}) @{ts}ns: {msg}",
tag = self.event_type.tag(),
bx = self.block_id.0,
by = self.block_id.1,
bz = self.block_id.2,
tx = self.thread_id.0,
ty = self.thread_id.1,
tz = self.thread_id.2,
ts = self.timestamp_ns,
msg = self.message,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WatchType {
Read,
Write,
ReadWrite,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct Breakpoint {
id: u32,
line: u32,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct Watchpoint {
id: u32,
address: u64,
size: usize,
watch_type: WatchType,
}
#[derive(Debug)]
pub struct KernelDebugger {
config: KernelDebugConfig,
breakpoints: Vec<Breakpoint>,
watchpoints: Vec<Watchpoint>,
next_bp_id: u32,
next_wp_id: u32,
}
impl KernelDebugger {
pub fn new(config: KernelDebugConfig) -> Self {
Self {
config,
breakpoints: Vec::new(),
watchpoints: Vec::new(),
next_bp_id: 1,
next_wp_id: 1,
}
}
pub fn attach(&mut self, kernel_name: &str) -> CudaResult<DebugSession> {
if kernel_name.is_empty() {
return Err(CudaError::InvalidValue);
}
Ok(DebugSession {
kernel_name: kernel_name.to_owned(),
events: Vec::new(),
config: self.config.clone(),
})
}
pub fn set_breakpoint(&mut self, line: u32) -> u32 {
let id = self.next_bp_id;
self.next_bp_id = self.next_bp_id.saturating_add(1);
self.breakpoints.push(Breakpoint { id, line });
id
}
pub fn remove_breakpoint(&mut self, bp_id: u32) -> bool {
let before = self.breakpoints.len();
self.breakpoints.retain(|bp| bp.id != bp_id);
self.breakpoints.len() < before
}
pub fn watchpoint(&mut self, address: u64, size: usize, watch_type: WatchType) -> u32 {
let id = self.next_wp_id;
self.next_wp_id = self.next_wp_id.saturating_add(1);
self.watchpoints.push(Watchpoint {
id,
address,
size,
watch_type,
});
id
}
pub fn config(&self) -> &KernelDebugConfig {
&self.config
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DebugSummary {
pub total_events: usize,
pub errors: usize,
pub warnings: usize,
pub nan_count: usize,
pub inf_count: usize,
pub oob_count: usize,
pub race_count: usize,
}
#[derive(Debug)]
pub struct DebugSession {
kernel_name: String,
events: Vec<DebugEvent>,
#[allow(dead_code)]
config: KernelDebugConfig,
}
impl DebugSession {
pub fn kernel_name(&self) -> &str {
&self.kernel_name
}
pub fn events(&self) -> &[DebugEvent] {
&self.events
}
pub fn add_event(&mut self, event: DebugEvent) {
self.events.push(event);
}
pub fn filter_events(&self, event_type: &DebugEventType) -> Vec<&DebugEvent> {
self.events
.iter()
.filter(|e| e.event_type.same_kind(event_type))
.collect()
}
pub fn summary(&self) -> DebugSummary {
let mut s = DebugSummary {
total_events: self.events.len(),
..DebugSummary::default()
};
for ev in &self.events {
match &ev.event_type {
DebugEventType::OutOfBounds { .. } => {
s.oob_count += 1;
s.errors += 1;
}
DebugEventType::NanDetected { .. } => {
s.nan_count += 1;
s.warnings += 1;
}
DebugEventType::InfDetected { .. } => {
s.inf_count += 1;
s.warnings += 1;
}
DebugEventType::RaceCondition { .. } => {
s.race_count += 1;
s.errors += 1;
}
DebugEventType::Assertion { .. } => {
s.errors += 1;
}
DebugEventType::Printf { .. } | DebugEventType::Breakpoint { .. } => {}
}
}
s
}
pub fn format_report(&self) -> String {
let summary = self.summary();
let mut out = String::with_capacity(512);
out.push_str(&format!("=== Debug Report: {} ===\n", self.kernel_name));
out.push_str(&format!(
"Total events: {} (errors: {}, warnings: {})\n",
summary.total_events, summary.errors, summary.warnings
));
if summary.oob_count > 0 {
out.push_str(&format!(" Out-of-bounds: {}\n", summary.oob_count));
}
if summary.nan_count > 0 {
out.push_str(&format!(" NaN detected: {}\n", summary.nan_count));
}
if summary.inf_count > 0 {
out.push_str(&format!(" Inf detected: {}\n", summary.inf_count));
}
if summary.race_count > 0 {
out.push_str(&format!(" Race cond.: {}\n", summary.race_count));
}
out.push_str("--- Events ---\n");
for ev in &self.events {
out.push_str(&format!(" {ev}\n"));
}
out.push_str("=== End Report ===\n");
out
}
}
#[derive(Debug, Clone)]
pub struct MemoryRegion {
pub base_address: u64,
pub size: usize,
pub name: String,
pub is_readonly: bool,
}
#[derive(Debug)]
pub struct MemoryChecker {
allocations: Vec<MemoryRegion>,
}
impl MemoryChecker {
pub fn new(allocations: Vec<MemoryRegion>) -> Self {
Self { allocations }
}
pub fn check_access(&self, address: u64, size: usize, is_write: bool) -> Option<DebugEvent> {
let region = self.allocations.iter().find(|r| {
address >= r.base_address
&& address
.checked_add(size as u64)
.is_some_and(|end| end <= r.base_address + r.size as u64)
});
match region {
Some(r) if is_write && r.is_readonly => Some(DebugEvent {
event_type: DebugEventType::OutOfBounds { address, size },
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 0,
message: format!("Write to read-only region '{}' at {:#x}", r.name, address),
}),
Some(_) => None,
None => Some(DebugEvent {
event_type: DebugEventType::OutOfBounds { address, size },
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 0,
message: format!(
"Access at {:#x} (size {}) does not fall within any known allocation",
address, size
),
}),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct NanInfLocation {
pub index: usize,
pub value: f64,
pub is_nan: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct NanInfChecker;
impl NanInfChecker {
pub fn check_f32(data: &[f32]) -> Vec<NanInfLocation> {
data.iter()
.enumerate()
.filter_map(|(i, &v)| {
if v.is_nan() {
Some(NanInfLocation {
index: i,
value: f64::from(v),
is_nan: true,
})
} else if v.is_infinite() {
Some(NanInfLocation {
index: i,
value: f64::from(v),
is_nan: false,
})
} else {
None
}
})
.collect()
}
pub fn check_f64(data: &[f64]) -> Vec<NanInfLocation> {
data.iter()
.enumerate()
.filter_map(|(i, &v)| {
if v.is_nan() {
Some(NanInfLocation {
index: i,
value: v,
is_nan: true,
})
} else if v.is_infinite() {
Some(NanInfLocation {
index: i,
value: v,
is_nan: false,
})
} else {
None
}
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PrintfArg {
Int(i64),
Float(f64),
String(String),
}
#[derive(Debug, Clone)]
pub struct PrintfEntry {
pub thread_id: (u32, u32, u32),
pub block_id: (u32, u32, u32),
pub format_string: String,
pub args: Vec<PrintfArg>,
}
#[derive(Debug)]
pub struct PrintfBuffer {
buffer_size: usize,
}
impl PrintfBuffer {
pub fn new(buffer_size: usize) -> Self {
Self { buffer_size }
}
pub fn buffer_size(&self) -> usize {
self.buffer_size
}
pub fn parse_entries(&self, raw: &[u8]) -> Vec<PrintfEntry> {
let mut entries = Vec::new();
let mut cursor = 0usize;
let entry_count = match Self::read_u32(raw, &mut cursor) {
Some(n) => n as usize,
None => return entries,
};
for _ in 0..entry_count {
let Some(entry) = self.parse_single_entry(raw, &mut cursor) else {
break;
};
entries.push(entry);
}
entries
}
fn parse_single_entry(&self, raw: &[u8], cursor: &mut usize) -> Option<PrintfEntry> {
let tx = Self::read_u32(raw, cursor)?;
let ty = Self::read_u32(raw, cursor)?;
let tz = Self::read_u32(raw, cursor)?;
let bx = Self::read_u32(raw, cursor)?;
let by = Self::read_u32(raw, cursor)?;
let bz = Self::read_u32(raw, cursor)?;
let fmt_len = Self::read_u32(raw, cursor)? as usize;
let fmt_bytes = Self::read_bytes(raw, cursor, fmt_len)?;
let format_string = String::from_utf8_lossy(fmt_bytes).into_owned();
let arg_count = Self::read_u32(raw, cursor)? as usize;
let mut args = Vec::with_capacity(arg_count);
for _ in 0..arg_count {
let tag = Self::read_u8(raw, cursor)?;
let arg = match tag {
0 => {
let val = Self::read_i64(raw, cursor)?;
PrintfArg::Int(val)
}
1 => {
let val = Self::read_f64(raw, cursor)?;
PrintfArg::Float(val)
}
2 => {
let slen = Self::read_u32(raw, cursor)? as usize;
let sbytes = Self::read_bytes(raw, cursor, slen)?;
PrintfArg::String(String::from_utf8_lossy(sbytes).into_owned())
}
_ => return None,
};
args.push(arg);
}
Some(PrintfEntry {
thread_id: (tx, ty, tz),
block_id: (bx, by, bz),
format_string,
args,
})
}
fn read_u8(raw: &[u8], cursor: &mut usize) -> Option<u8> {
if *cursor >= raw.len() {
return None;
}
let val = raw[*cursor];
*cursor += 1;
Some(val)
}
fn read_u32(raw: &[u8], cursor: &mut usize) -> Option<u32> {
if *cursor + 4 > raw.len() {
return None;
}
let bytes: [u8; 4] = raw[*cursor..*cursor + 4].try_into().ok()?;
*cursor += 4;
Some(u32::from_le_bytes(bytes))
}
fn read_i64(raw: &[u8], cursor: &mut usize) -> Option<i64> {
if *cursor + 8 > raw.len() {
return None;
}
let bytes: [u8; 8] = raw[*cursor..*cursor + 8].try_into().ok()?;
*cursor += 8;
Some(i64::from_le_bytes(bytes))
}
fn read_f64(raw: &[u8], cursor: &mut usize) -> Option<f64> {
if *cursor + 8 > raw.len() {
return None;
}
let bytes: [u8; 8] = raw[*cursor..*cursor + 8].try_into().ok()?;
*cursor += 8;
Some(f64::from_le_bytes(bytes))
}
fn read_bytes<'a>(raw: &'a [u8], cursor: &mut usize, len: usize) -> Option<&'a [u8]> {
if *cursor + len > raw.len() {
return None;
}
let slice = &raw[*cursor..*cursor + len];
*cursor += len;
Some(slice)
}
}
#[derive(Debug, Clone, Copy)]
pub struct KernelAssertions;
impl KernelAssertions {
pub fn assert_bounds(index: usize, len: usize, name: &str) -> Option<DebugEvent> {
if index < len {
return None;
}
Some(DebugEvent {
event_type: DebugEventType::Assertion {
condition: format!("{name}[{index}] < {len}"),
file: String::new(),
line: 0,
},
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 0,
message: format!("Bounds check failed: {name}[{index}] out of range (len={len})"),
})
}
pub fn assert_not_nan(value: f64, name: &str) -> Option<DebugEvent> {
if !value.is_nan() {
return None;
}
Some(DebugEvent {
event_type: DebugEventType::NanDetected {
register: name.to_owned(),
value,
},
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 0,
message: format!("NaN detected in '{name}'"),
})
}
pub fn assert_finite(value: f64, name: &str) -> Option<DebugEvent> {
if value.is_finite() {
return None;
}
if value.is_nan() {
return Some(DebugEvent {
event_type: DebugEventType::NanDetected {
register: name.to_owned(),
value,
},
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 0,
message: format!("Non-finite (NaN) value in '{name}'"),
});
}
Some(DebugEvent {
event_type: DebugEventType::InfDetected {
register: name.to_owned(),
},
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 0,
message: format!("Non-finite (Inf) value in '{name}'"),
})
}
pub fn assert_positive(value: f64, name: &str) -> Option<DebugEvent> {
if value > 0.0 {
return None;
}
if value.is_nan() {
return Some(DebugEvent {
event_type: DebugEventType::NanDetected {
register: name.to_owned(),
value,
},
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 0,
message: format!("Expected positive value for '{name}', got NaN"),
});
}
Some(DebugEvent {
event_type: DebugEventType::Assertion {
condition: format!("{name} > 0"),
file: String::new(),
line: 0,
},
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 0,
message: format!("Expected positive value for '{name}', got {value}"),
})
}
}
#[derive(Debug)]
pub struct DebugPtxInstrumenter {
enable_bounds_check: bool,
enable_nan_check: bool,
enable_printf: bool,
}
impl DebugPtxInstrumenter {
pub fn new(config: &KernelDebugConfig) -> Self {
Self {
enable_bounds_check: config.enable_bounds_check,
enable_nan_check: config.enable_nan_check,
enable_printf: config.print_buffer_size > 0,
}
}
pub fn instrument_bounds_checks(&self, ptx: &str) -> String {
if !self.enable_bounds_check {
return ptx.to_owned();
}
let mut output = String::with_capacity(ptx.len() + ptx.len() / 4);
let mut added_param = false;
for line in ptx.lines() {
let trimmed = line.trim();
if trimmed.starts_with(".entry") && !added_param {
output.push_str(line);
output.push('\n');
output.push_str(" // [oxicuda-debug] bounds-check instrumentation\n");
output.push_str(" .param .u64 __oxicuda_debug_buf;\n");
added_param = true;
continue;
}
if (trimmed.starts_with("ld.global") || trimmed.starts_with("st.global"))
&& !trimmed.starts_with("// [oxicuda-debug]")
{
output.push_str(line);
output.push('\n');
output.push_str(" // [oxicuda-debug] bounds check for above access\n");
output.push_str(" setp.ge.u64 %p_oob, %rd_addr, %rd_alloc_end;\n");
output.push_str(" @%p_oob trap;\n");
} else {
output.push_str(line);
output.push('\n');
}
}
output
}
pub fn instrument_nan_checks(&self, ptx: &str) -> String {
if !self.enable_nan_check {
return ptx.to_owned();
}
let fp_ops = [
"add.f32", "add.f64", "sub.f32", "sub.f64", "mul.f32", "mul.f64", "div.f32", "div.f64",
"fma.f32", "fma.f64",
];
let mut output = String::with_capacity(ptx.len() + ptx.len() / 4);
for line in ptx.lines() {
output.push_str(line);
output.push('\n');
let trimmed = line.trim();
if fp_ops.iter().any(|op| trimmed.starts_with(op)) {
if let Some(dest) = trimmed.split_whitespace().nth(1) {
let dest_clean = dest.trim_end_matches(',');
let width = if trimmed.contains(".f64") {
"f64"
} else {
"f32"
};
output.push_str(&format!(
" // [oxicuda-debug] NaN check for {dest_clean}\n"
));
output.push_str(&format!(" testp.nan.{width} %p_nan, {dest_clean};\n"));
output.push_str(" @%p_nan trap;\n");
}
}
}
output
}
pub fn instrument_printf(&self, ptx: &str) -> String {
if !self.enable_printf {
return ptx.to_owned();
}
let mut output = String::with_capacity(ptx.len() + ptx.len() / 4);
let mut added_param = false;
for line in ptx.lines() {
let trimmed = line.trim();
if trimmed.starts_with(".entry") && !added_param {
output.push_str(line);
output.push('\n');
output.push_str(" // [oxicuda-debug] printf buffer parameter\n");
output.push_str(" .param .u64 __oxicuda_printf_buf;\n");
added_param = true;
continue;
}
if trimmed.starts_with("// PRINTF") {
output.push_str(" // [oxicuda-debug] printf store sequence\n");
output.push_str(" ld.param.u64 %rd_pbuf, [__oxicuda_printf_buf];\n");
output.push_str(" atom.global.add.u32 %r_poff, [%rd_pbuf], 1;\n");
} else {
output.push_str(line);
output.push('\n');
}
}
output
}
pub fn strip_debug(&self, ptx: &str) -> String {
let mut output = String::with_capacity(ptx.len());
let mut skip_next = false;
for line in ptx.lines() {
if skip_next {
skip_next = false;
continue;
}
let trimmed = line.trim();
if trimmed.starts_with("// [oxicuda-debug]") {
skip_next = true;
continue;
}
if trimmed.contains("__oxicuda_debug_buf") || trimmed.contains("__oxicuda_printf_buf") {
continue;
}
output.push_str(line);
output.push('\n');
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_default_values() {
let cfg = KernelDebugConfig::default();
assert_eq!(cfg.debug_level, DebugLevel::Info);
assert!(cfg.enable_bounds_check);
assert!(cfg.enable_nan_check);
assert!(cfg.enable_inf_check);
assert!(!cfg.enable_race_detection);
assert_eq!(cfg.print_buffer_size, 1024 * 1024);
assert_eq!(cfg.max_print_per_thread, 32);
}
#[test]
fn debugger_creation_with_config() {
let cfg = KernelDebugConfig {
debug_level: DebugLevel::Trace,
enable_bounds_check: false,
..KernelDebugConfig::default()
};
let debugger = KernelDebugger::new(cfg);
assert_eq!(debugger.config().debug_level, DebugLevel::Trace);
assert!(!debugger.config().enable_bounds_check);
}
#[test]
fn debug_session_lifecycle() {
let cfg = KernelDebugConfig::default();
let mut debugger = KernelDebugger::new(cfg);
let session = debugger.attach("test_kernel");
assert!(session.is_ok());
let session = session.expect("session");
assert_eq!(session.kernel_name(), "test_kernel");
assert!(session.events().is_empty());
let err = debugger.attach("");
assert!(err.is_err());
}
#[test]
fn breakpoint_set_and_remove() {
let mut debugger = KernelDebugger::new(KernelDebugConfig::default());
let bp1 = debugger.set_breakpoint(42);
let bp2 = debugger.set_breakpoint(100);
assert_ne!(bp1, bp2);
assert!(debugger.remove_breakpoint(bp1));
assert!(!debugger.remove_breakpoint(bp1));
assert!(debugger.remove_breakpoint(bp2));
}
#[test]
fn memory_checker_valid_access() {
let checker = MemoryChecker::new(vec![MemoryRegion {
base_address: 0x1000,
size: 256,
name: "buf_a".into(),
is_readonly: false,
}]);
assert!(checker.check_access(0x1000, 16, false).is_none());
assert!(checker.check_access(0x1080, 32, true).is_none());
}
#[test]
fn memory_checker_out_of_bounds() {
let checker = MemoryChecker::new(vec![MemoryRegion {
base_address: 0x1000,
size: 256,
name: "buf_a".into(),
is_readonly: false,
}]);
let ev = checker.check_access(0x1100, 16, false);
assert!(ev.is_some());
let ev = ev.expect("oob event");
assert!(matches!(ev.event_type, DebugEventType::OutOfBounds { .. }));
let ev2 = checker.check_access(0x5000, 4, true);
assert!(ev2.is_some());
}
#[test]
fn nan_detection_f32() {
let data = [1.0_f32, f32::NAN, 3.0, f32::NAN];
let locs = NanInfChecker::check_f32(&data);
assert_eq!(locs.len(), 2);
assert_eq!(locs[0].index, 1);
assert!(locs[0].is_nan);
assert_eq!(locs[1].index, 3);
}
#[test]
fn inf_detection_f64() {
let data = [1.0_f64, f64::INFINITY, f64::NEG_INFINITY, 4.0];
let locs = NanInfChecker::check_f64(&data);
assert_eq!(locs.len(), 2);
assert!(!locs[0].is_nan);
assert_eq!(locs[0].index, 1);
assert!(!locs[1].is_nan);
assert_eq!(locs[1].index, 2);
}
#[test]
fn printf_buffer_parsing() {
let buf = PrintfBuffer::new(4096);
let mut raw = Vec::new();
raw.extend_from_slice(&1_u32.to_le_bytes());
raw.extend_from_slice(&1_u32.to_le_bytes());
raw.extend_from_slice(&0_u32.to_le_bytes());
raw.extend_from_slice(&0_u32.to_le_bytes());
raw.extend_from_slice(&0_u32.to_le_bytes());
raw.extend_from_slice(&0_u32.to_le_bytes());
raw.extend_from_slice(&0_u32.to_le_bytes());
let fmt = b"val=%d";
raw.extend_from_slice(&(fmt.len() as u32).to_le_bytes());
raw.extend_from_slice(fmt);
raw.extend_from_slice(&1_u32.to_le_bytes());
raw.push(0);
raw.extend_from_slice(&42_i64.to_le_bytes());
let entries = buf.parse_entries(&raw);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].thread_id, (1, 0, 0));
assert_eq!(entries[0].format_string, "val=%d");
assert_eq!(entries[0].args.len(), 1);
assert_eq!(entries[0].args[0], PrintfArg::Int(42));
}
#[test]
fn assertion_checks() {
assert!(KernelAssertions::assert_bounds(5, 10, "arr").is_none());
let ev = KernelAssertions::assert_bounds(10, 10, "arr");
assert!(ev.is_some());
assert!(KernelAssertions::assert_not_nan(1.0, "x").is_none());
assert!(KernelAssertions::assert_not_nan(f64::NAN, "x").is_some());
assert!(KernelAssertions::assert_finite(1.0, "x").is_none());
assert!(KernelAssertions::assert_finite(f64::INFINITY, "x").is_some());
assert!(KernelAssertions::assert_finite(f64::NAN, "x").is_some());
assert!(KernelAssertions::assert_positive(1.0, "x").is_none());
assert!(KernelAssertions::assert_positive(0.0, "x").is_some());
assert!(KernelAssertions::assert_positive(-1.0, "x").is_some());
assert!(KernelAssertions::assert_positive(f64::NAN, "x").is_some());
}
#[test]
fn debug_event_filtering() {
let cfg = KernelDebugConfig::default();
let mut debugger = KernelDebugger::new(cfg);
let mut session = debugger.attach("filter_test").expect("session");
session.add_event(DebugEvent {
event_type: DebugEventType::NanDetected {
register: "f0".into(),
value: f64::NAN,
},
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 100,
message: "nan".into(),
});
session.add_event(DebugEvent {
event_type: DebugEventType::OutOfBounds {
address: 0xDEAD,
size: 4,
},
thread_id: (1, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 200,
message: "oob".into(),
});
session.add_event(DebugEvent {
event_type: DebugEventType::NanDetected {
register: "f1".into(),
value: f64::NAN,
},
thread_id: (2, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 300,
message: "nan2".into(),
});
let nans = session.filter_events(&DebugEventType::NanDetected {
register: String::new(),
value: 0.0,
});
assert_eq!(nans.len(), 2);
let oobs = session.filter_events(&DebugEventType::OutOfBounds {
address: 0,
size: 0,
});
assert_eq!(oobs.len(), 1);
}
#[test]
fn summary_statistics() {
let cfg = KernelDebugConfig::default();
let mut debugger = KernelDebugger::new(cfg);
let mut session = debugger.attach("summary_test").expect("session");
session.add_event(DebugEvent {
event_type: DebugEventType::NanDetected {
register: "f0".into(),
value: f64::NAN,
},
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 0,
message: String::new(),
});
session.add_event(DebugEvent {
event_type: DebugEventType::InfDetected {
register: "f1".into(),
},
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 0,
message: String::new(),
});
session.add_event(DebugEvent {
event_type: DebugEventType::OutOfBounds {
address: 0x100,
size: 4,
},
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 0,
message: String::new(),
});
session.add_event(DebugEvent {
event_type: DebugEventType::RaceCondition { address: 0x200 },
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 0,
message: String::new(),
});
let s = session.summary();
assert_eq!(s.total_events, 4);
assert_eq!(s.errors, 2); assert_eq!(s.warnings, 2); assert_eq!(s.nan_count, 1);
assert_eq!(s.inf_count, 1);
assert_eq!(s.oob_count, 1);
assert_eq!(s.race_count, 1);
}
#[test]
fn format_report_output() {
let cfg = KernelDebugConfig::default();
let mut debugger = KernelDebugger::new(cfg);
let mut session = debugger.attach("report_test").expect("session");
session.add_event(DebugEvent {
event_type: DebugEventType::NanDetected {
register: "f0".into(),
value: f64::NAN,
},
thread_id: (0, 0, 0),
block_id: (0, 0, 0),
timestamp_ns: 42,
message: "NaN found".into(),
});
let report = session.format_report();
assert!(report.contains("report_test"));
assert!(report.contains("Total events: 1"));
assert!(report.contains("NaN detected: 1"));
assert!(report.contains("NaN found"));
assert!(report.contains("=== End Report ==="));
}
#[test]
fn ptx_instrumentation_bounds_checks() {
let cfg = KernelDebugConfig::default();
let inst = DebugPtxInstrumenter::new(&cfg);
let ptx = ".entry my_kernel {\n ld.global.f32 %f0, [%rd0];\n ret;\n}\n";
let result = inst.instrument_bounds_checks(ptx);
assert!(result.contains("__oxicuda_debug_buf"));
assert!(result.contains("setp.ge.u64"));
assert!(result.contains("@%p_oob trap"));
}
#[test]
fn ptx_strip_debug_roundtrip() {
let cfg = KernelDebugConfig::default();
let inst = DebugPtxInstrumenter::new(&cfg);
let original = ".entry kern {\n add.f32 %f0, %f1, %f2;\n ret;\n}\n";
let instrumented = inst.instrument_nan_checks(original);
assert!(instrumented.contains("[oxicuda-debug]"));
let stripped = inst.strip_debug(&instrumented);
assert!(!stripped.contains("[oxicuda-debug]"));
assert!(stripped.contains("add.f32"));
assert!(stripped.contains("ret;"));
}
}