diff --git a/cutile-rs/CHANGELOG.md b/cutile-rs/CHANGELOG.md index 43b02fe498..870417aaf7 100644 --- a/cutile-rs/CHANGELOG.md +++ b/cutile-rs/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ### Fixed +- Generated kernel entry setup and assumptions now use the user function's + signature as their source location, preserving the inlined body's own + statement locations and helper scopes. - Generated constants without captured source spans use their containing function definition, retaining exact user-expression locations and nested cross-file inline caller chains. diff --git a/cutile-rs/cutile-compiler/src/kernel_entry_generator.rs b/cutile-rs/cutile-compiler/src/kernel_entry_generator.rs index 33ce9dce04..7de337b9d2 100644 --- a/cutile-rs/cutile-compiler/src/kernel_entry_generator.rs +++ b/cutile-rs/cutile-compiler/src/kernel_entry_generator.rs @@ -22,6 +22,7 @@ use proc_macro2::Span; use quote::ToTokens; use std::collections::HashMap; use syn::punctuated::Punctuated; +use syn::spanned::Spanned; use syn::visit_mut::VisitMut; use syn::{Expr, FnArg, GenericArgument, ItemFn, ItemImpl, Lit, Stmt, Token}; @@ -657,8 +658,10 @@ pub fn generate_entry_point( statements.push(final_stmnt); fn_entry.block.stmts = statements; - // This source code is not visible. - let mut visitor = SpanSetter::new(Span::call_site()); + // Generated entry setup has no user-written body. Attribute it, including + // assumptions and the call into the kernel, to the user's signature. + // The original kernel body keeps its own spans when it is inlined. + let mut visitor = SpanSetter::new(fn_item.sig.span()); visitor.visit_item_fn_mut(&mut fn_entry); Ok(( diff --git a/cutile-rs/cutile/tests/debug_info.rs b/cutile-rs/cutile/tests/debug_info.rs index 5e3c15002c..fece010f92 100644 --- a/cutile-rs/cutile/tests/debug_info.rs +++ b/cutile-rs/cutile/tests/debug_info.rs @@ -123,6 +123,121 @@ fn helper_instructions_keep_exact_lines_and_call_sites() { }); } +#[test] +fn entry_wrapper_and_assumptions_use_the_user_signature() { + common::with_test_stack(|| { + use cutile_ir::bytecode::Opcode; + + let signature_line = source_line( + include_str!("debug_info.rs"), + "fn source_kernel(out: &mut Tensor, x: i32) {", + ); + for level in [DebugInfoLevel::Line, DebugInfoLevel::Full] { + let artifacts = compile(level); + let mut wrapper_ops = 0; + let mut wrapper_assumes = 0; + let mut inline_ops = 0; + visit_ops( + artifacts.module(), + &artifacts.module().functions, + &mut |op| { + let mut location = &op.location; + while let Location::CallSite { caller, .. } = location { + inline_ops += 1; + location = caller; + } + // Constants built from synthesized types have a separate source + // span path. This test covers generated syntax and inline callers. + if op.opcode == Opcode::Constant + && !matches!(op.location, Location::CallSite { .. }) + { + return; + } + if let Location::FileLineCol { filename, line, .. } = location { + assert!(filename.ends_with("debug_info.rs"), "{location:?}"); + assert_eq!(*line, signature_line, "{:?}: {location:?}", op.opcode); + if !matches!(op.location, Location::CallSite { .. }) { + wrapper_ops += 1; + wrapper_assumes += usize::from(op.opcode == Opcode::Assume); + } + } else { + panic!("missing wrapper origin: {location:?}"); + } + }, + ); + assert!(wrapper_ops > 1); + assert!(wrapper_assumes > 0); + assert!(inline_ops > 0); + + // The wrapper origin must not replace the nested helper's own scope + // or either call site, including when the helper is in another file. + let helpers = include_str!("fixtures/debug_helpers.rs"); + let offset_line = source_line(helpers, "x + 7"); + let nested_line = source_line(helpers, "offset(x) * 2"); + let kernel_line = source_line(include_str!("debug_info.rs"), "let value = nested(x);"); + let mut nested_chain = false; + visit_ops( + artifacts.module(), + &artifacts.module().functions, + &mut |op| { + let Location::CallSite { callee, caller } = &op.location else { + return; + }; + let Location::DebugInfo(info) = &**callee else { + return; + }; + let DebugScope::Subprogram(scope) = &info.scope else { + return; + }; + if scope.name != "offset" || info.line != offset_line { + return; + } + assert!(info.filename.ends_with("fixtures/debug_helpers.rs")); + let mut caller = &**caller; + for (name, file, line) in [ + ("nested", "fixtures/debug_helpers.rs", nested_line), + ("source_kernel", "tests/debug_info.rs", kernel_line), + ] { + let Location::CallSite { + callee, + caller: next, + } = caller + else { + panic!("missing {name}: {caller:?}"); + }; + let Location::DebugInfo(info) = &**callee else { + panic!("missing scope: {callee:?}"); + }; + let DebugScope::Subprogram(scope) = &info.scope else { + panic!("missing subprogram: {info:?}"); + }; + assert_eq!(scope.name, name); + assert!(info.filename.ends_with(file), "{info:?}"); + assert_eq!(info.line, line); + caller = next; + } + assert!( + matches!(caller, Location::FileLineCol { line, .. } if *line == signature_line) + ); + nested_chain = true; + }, + ); + assert!( + nested_chain, + "missing exact nested helper chain in {level:?}" + ); + + let dump = cutile_ir::decode_bytecode(&artifacts.bytecode().unwrap()).unwrap(); + assert!( + dump.contains(&format!( + "line={signature_line}, name=\"source_kernel\", linkage=\"source_kernel_entry\"" + )), + "{dump}" + ); + } + }); +} + #[test] fn explicit_debug_modes_override_build_default_and_change_cache_keys() { use cutile_compiler::cuda_tile_runtime_utils::TileirasOptions; @@ -221,7 +336,12 @@ fn cubin_has_rust_lines_and_inline_frames() { continue; } assert!(sections.contains(".debug_line"), "{sections}"); - let sass = output(Command::new("nvdisasm").arg("--print-line-info").arg(&path)); + let sass = output( + Command::new("nvdisasm") + .arg("--print-code") + .arg("--print-line-info") + .arg(&path), + ); assert!(sass.contains("debug_helpers.rs"), "{sass}"); assert!(sass.contains("debug_info.rs"), "{sass}"); assert!(!sass.contains("_core.rs"), "{sass}"); @@ -236,6 +356,57 @@ fn cubin_has_rust_lines_and_inline_frames() { }); } +#[test] +#[ignore = "requires tileiras and nvdisasm"] +fn entry_setup_cubin_uses_signature_in_line_and_full_modes() { + common::with_test_stack(|| { + use cutile_compiler::cuda_tile_runtime_utils::{run_tileiras, TileirasOptions}; + let signature = source_line( + include_str!("debug_info.rs"), + "fn source_kernel(out: &mut Tensor, x: i32) {", + ); + let directory = CubinDirectory::new(); + for level in [DebugInfoLevel::Line, DebugInfoLevel::Full] { + let artifacts = compile(level); + let flags = + TileirasOptions::from_compile_options(&CompileOptions::new().debug_info(level)); + let cubin = run_tileiras(&artifacts.bytecode().unwrap(), "sm_120", &flags).unwrap(); + let path = directory.0.join(format!("entry-{level:?}.cubin")); + std::fs::write(&path, cubin).unwrap(); + let sass = output( + Command::new("nvdisasm") + .arg("--print-code") + .arg("--print-line-info") + .arg(&path), + ); + let mut location = None; + let mut entry_found = false; + for line in sass.lines() { + if let Some(row) = line.trim().strip_prefix("//## File \"") { + let (file, line) = row.split_once("\", line ").unwrap(); + location = Some((file, line.trim().parse::().unwrap())); + } + if line.trim_start().starts_with("/*0000*/") { + let (file, line) = location.unwrap_or_else(|| { + panic!("{level:?}: entry instruction must have a source location\n{sass}") + }); + assert!( + file.ends_with("tests/debug_info.rs"), + "{level:?}: {file}\n{sass}" + ); + assert_eq!( + line, signature, + "{level:?}: entry setup must use the signature\n{sass}" + ); + entry_found = true; + break; + } + } + assert!(entry_found, "{level:?}: no entry instruction\n{sass}"); + } + }); +} + fn output(command: &mut Command) -> String { let result = command .output() diff --git a/cutile-rs/scripts/run_cpu_tests.sh b/cutile-rs/scripts/run_cpu_tests.sh index f4a3380983..621250f0e2 100755 --- a/cutile-rs/scripts/run_cpu_tests.sh +++ b/cutile-rs/scripts/run_cpu_tests.sh @@ -88,6 +88,10 @@ run_step \ "cutile debug provenance through the assembler" \ cargo test -p cutile --test debug_info nested_helper_cubin_lines_stay_inside_their_functions -- --ignored --nocapture +run_step \ + "cutile entry setup locations through the assembler" \ + cargo test -p cutile --test debug_info entry_setup_cubin_uses_signature_in_line_and_full_modes -- --ignored + run_step \ "cutile generated source locations through the assembler" \ cargo test -p cutile --test generated_source_locations generated_locations_survive_assembly -- --ignored