Skip to content

Commit 000ec78

Browse files
authored
Replace Attr::SpvDebugLine with a better basic debug source location attribute. (#9)
Partially modeled on a subset of the [`NonSemantic.Shader.DebugInfo.100`](https://htmlpreview.github.io/?https://github.com/KhronosGroup/SPIRV-Registry/blob/main/nonsemantic/NonSemantic.Shader.DebugInfo.100.html) SPIR-V extended instruction set, but more with Rust-GPU's needs in mind, this extends the existing `file:line:col` debuginfo to support both: - ranges (within a single file - comparable to rustc's internal `Span`) - "inlined at" debuginfo (call site debug location + callee name) - more expressive than `NonSemantic.Shader.DebugInfo.100` in one key way: for some reason, that extension only seems to allow distinguishing call sites *by line number* which seems borderline incorrect (wrt DWARF) There is nothing in the `spirt` library itself that can create such extended debuginfo (even though in theory e.g. `NonSemantic.Shader.DebugInfo.100` support could do that), and instead Rust-GPU is expected to produce it by converting its own custom instructions. The Rust-GPU side of this already exists since it's part of the polaris/lodestar demo branches (IIRC this specifically happened for recursion emulation, where `FuncCall` being outside `Block`s meant). --- Example from a relatively messy inlining stack: ![image](https://github.com/user-attachments/assets/a69718d6-65a1-4749-bea6-27842417daa5) Ideally large amounts of deduplication could be done by having a "debug context tracker" in `print` that only needs to show *changes* (i.e. entering/leaving a group of inlined instructions, and individual location debuginfo that could still differ between instructions), but that's not done in this PR. The status quo on the Rust-GPU side was already quite verbose, and arguably worse than this at times (or at least more "syntaxful"), so this will do for the time being.
2 parents 3cabf18 + f956ecc commit 000ec78

File tree

6 files changed

+260
-86
lines changed

6 files changed

+260
-86
lines changed

src/lib.rs

Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ pub use context::AttrSet;
287287
/// Definition for an [`AttrSet`]: a set of [`Attr`]s.
288288
#[derive(Default, PartialEq, Eq, Hash)]
289289
pub struct AttrSetDef {
290+
// FIXME(eddyb) consider "persistent datastructures" (e.g. the `im` crate).
290291
// FIXME(eddyb) use `BTreeMap<Attr, AttrValue>` and split some of the params
291292
// between the `Attr` and `AttrValue` based on specified uniquness.
292293
// FIXME(eddyb) don't put debuginfo in here, but rather at use sites
@@ -298,7 +299,34 @@ pub struct AttrSetDef {
298299
}
299300

300301
impl AttrSetDef {
301-
pub fn push_diag(&mut self, diag: Diag) {
302+
pub fn dbg_src_loc(&self) -> Option<DbgSrcLoc> {
303+
// FIXME(eddyb) seriously consider moving to `BTreeMap` (see above).
304+
// HACK(eddyb) this assumes `Attr::DbgSrcLoc` is the first of `Attr`!
305+
match self.attrs.first() {
306+
Some(&Attr::DbgSrcLoc(OrdAssertEq(dbg_src_loc))) => Some(dbg_src_loc),
307+
_ => None,
308+
}
309+
}
310+
311+
pub fn set_dbg_src_loc(&mut self, dbg_src_loc: DbgSrcLoc) {
312+
// FIXME(eddyb) seriously consider moving to `BTreeMap` (see above).
313+
// HACK(eddyb) this assumes `Attr::DbgSrcLoc` is the first of `Attr`!
314+
if let Some(Attr::DbgSrcLoc(_)) = self.attrs.first() {
315+
self.attrs.pop_first().unwrap();
316+
}
317+
self.attrs.insert(Attr::DbgSrcLoc(OrdAssertEq(dbg_src_loc)));
318+
}
319+
320+
pub fn diags(&self) -> &[Diag] {
321+
// FIXME(eddyb) seriously consider moving to `BTreeMap` (see above).
322+
// HACK(eddyb) this assumes `Attr::Diagnostics` is the last of `Attr`!
323+
match self.attrs.last() {
324+
Some(Attr::Diagnostics(OrdAssertEq(diags))) => diags,
325+
_ => &[],
326+
}
327+
}
328+
329+
pub fn mutate_diags(&mut self, f: impl FnOnce(&mut Vec<Diag>)) {
302330
// FIXME(eddyb) seriously consider moving to `BTreeMap` (see above).
303331
// HACK(eddyb) this assumes `Attr::Diagnostics` is the last of `Attr`!
304332
let mut attr = if let Some(Attr::Diagnostics(_)) = self.attrs.last() {
@@ -307,30 +335,54 @@ impl AttrSetDef {
307335
Attr::Diagnostics(OrdAssertEq(vec![]))
308336
};
309337
match &mut attr {
310-
Attr::Diagnostics(OrdAssertEq(diags)) => diags.push(diag),
338+
Attr::Diagnostics(OrdAssertEq(diags)) => f(diags),
311339
_ => unreachable!(),
312340
}
313341
self.attrs.insert(attr);
314342
}
315343

316-
// FIXME(eddyb) should this be hidden in favor of `AttrSet::append_diag`?
317-
pub fn append_diag(&self, diag: Diag) -> Self {
318-
let mut new_attrs = Self { attrs: self.attrs.clone() };
319-
new_attrs.push_diag(diag);
320-
new_attrs
344+
// HACK(eddyb) these only exist to avoid changing code working with `AttrSetDef`s.
345+
pub fn push_diags(&mut self, new_diags: impl IntoIterator<Item = Diag>) {
346+
self.mutate_diags(|diags| diags.extend(new_diags));
347+
}
348+
pub fn push_diag(&mut self, diag: Diag) {
349+
self.push_diags([diag]);
321350
}
322351
}
323352

324353
// FIXME(eddyb) should these methods be elsewhere?
325354
impl AttrSet {
326-
// FIXME(eddyb) should this be hidden in favor of `push_diag`?
327-
// FIXME(eddyb) should these methods always take multiple values?
328-
pub fn append_diag(self, cx: &Context, diag: Diag) -> Self {
329-
cx.intern(cx[self].append_diag(diag))
355+
// FIXME(eddyb) could these two methods have a better name?
356+
pub fn reintern_with(self, cx: &Context, f: impl FnOnce(&mut AttrSetDef)) -> Self {
357+
let mut new_attrs = AttrSetDef { attrs: cx[self].attrs.clone() };
358+
f(&mut new_attrs);
359+
cx.intern(new_attrs)
360+
}
361+
pub fn mutate(&mut self, cx: &Context, f: impl FnOnce(&mut AttrSetDef)) {
362+
*self = self.reintern_with(cx, f);
363+
}
364+
365+
pub fn dbg_src_loc(self, cx: &Context) -> Option<DbgSrcLoc> {
366+
if self == AttrSet::default() {
367+
return None;
368+
}
369+
cx[self].dbg_src_loc()
370+
}
371+
372+
pub fn set_dbg_src_loc(&mut self, cx: &Context, dbg_src_loc: DbgSrcLoc) {
373+
self.mutate(cx, |attrs| attrs.set_dbg_src_loc(dbg_src_loc));
374+
}
375+
376+
pub fn diags(self, cx: &Context) -> &[Diag] {
377+
cx[self].diags()
378+
}
379+
380+
pub fn push_diags(&mut self, cx: &Context, diags: impl IntoIterator<Item = Diag>) {
381+
self.mutate(cx, |attrs| attrs.push_diags(diags));
330382
}
331383

332384
pub fn push_diag(&mut self, cx: &Context, diag: Diag) {
333-
*self = self.append_diag(cx, diag);
385+
self.push_diags(cx, [diag]);
334386
}
335387
}
336388

@@ -342,18 +394,16 @@ impl AttrSet {
342394
// FIXME(eddyb) consider interning individual attrs, not just `AttrSet`s.
343395
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, derive_more::From)]
344396
pub enum Attr {
397+
// HACK(eddyb) this must be the first variant of `Attr` for the correctness
398+
// of `AttrSetDef::{dbg_src_loc,set_dbg_src_loc}`.
399+
DbgSrcLoc(OrdAssertEq<DbgSrcLoc>),
400+
345401
/// `QPtr`-specific attributes (see [`qptr::QPtrAttr`]).
346402
#[from]
347403
QPtr(qptr::QPtrAttr),
348404

349405
SpvAnnotation(spv::Inst),
350406

351-
SpvDebugLine {
352-
file_path: OrdAssertEq<InternedStr>,
353-
line: u32,
354-
col: u32,
355-
},
356-
357407
/// Some SPIR-V instructions, like `OpFunction`, take a bitflags operand
358408
/// that is effectively an optimization over using `OpDecorate`.
359409
//
@@ -363,11 +413,29 @@ pub enum Attr {
363413
/// Can be used anywhere to record [`Diag`]nostics produced during a pass,
364414
/// while allowing the pass to continue (and its output to be pretty-printed).
365415
//
366-
// HACK(eddyb) this is the last variant to control printing order, but also
367-
// to make `push_diag`/`append_diag` above work correctly!
416+
// HACK(eddyb) this must be the last variant of `Attr` for the correctness
417+
// of`AttrSetDef::{diags,mutate_diags}` (this also helps with printing order).
368418
Diagnostics(OrdAssertEq<Vec<Diag>>),
369419
}
370420

421+
/// Simple `file:line:column`-style debuginfo, similar to SPIR-V `OpLine`,
422+
/// but also supporting `(line, column)` ranges, and inlined locations.
423+
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
424+
pub struct DbgSrcLoc {
425+
pub file_path: InternedStr,
426+
427+
// FIXME(eddyb) `Range` might make sense here but these are inclusive,
428+
// and `range::RangeInclusive` (the non-`Iterator` version of `a..=b`)
429+
// isn't stable (nor the type of `a..=b` expressions), yet.
430+
pub start_line_col: (u32, u32),
431+
pub end_line_col: (u32, u32),
432+
433+
/// To describe locations originally in the callee of a call that was inlined,
434+
/// the name of the callee and attributes describing the callsite are used,
435+
/// where callsite attributes are expected to contain an [`Attr::DbgSrcLoc`].
436+
pub inlined_callee_name_and_call_site: Option<(InternedStr, AttrSet)>,
437+
}
438+
371439
/// Diagnostics produced by SPIR-T passes, and recorded in [`Attr::Diagnostics`].
372440
#[derive(Clone, PartialEq, Eq, Hash)]
373441
pub struct Diag {

src/print/mod.rs

Lines changed: 94 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::qptr::{self, QPtrAttr, QPtrMemUsage, QPtrMemUsageKind, QPtrOp, QPtrUs
2525
use crate::visit::{InnerVisit, Visit, Visitor};
2626
use crate::{
2727
AddrSpace, Attr, AttrSet, AttrSetDef, Const, ConstDef, ConstKind, Context, DataInst,
28-
DataInstDef, DataInstForm, DataInstFormDef, DataInstKind, DeclDef, Diag, DiagLevel,
28+
DataInstDef, DataInstForm, DataInstFormDef, DataInstKind, DbgSrcLoc, DeclDef, Diag, DiagLevel,
2929
DiagMsgPart, EntityListIter, ExportKey, Exportee, Func, FuncDecl, FuncParam, FxIndexMap,
3030
FxIndexSet, GlobalVar, GlobalVarDecl, GlobalVarDefBody, Import, Module, ModuleDebugInfo,
3131
ModuleDialect, Node, NodeDef, NodeKind, NodeOutputDecl, OrdAssertEq, Region, RegionDef,
@@ -2111,6 +2111,99 @@ impl Print for Attr {
21112111
type Output = pretty::Fragment;
21122112
fn print(&self, printer: &Printer<'_>) -> pretty::Fragment {
21132113
let non_comment_attr = match self {
2114+
// FIXME(eddyb) move from repeating the same backtrace-like comments
2115+
// (potentially many times in a row) to a more "stateful" approach,
2116+
// which only mentions every inlined callsite once per sequence of
2117+
// e.g. `DataInst`s that are all nested in it.
2118+
&Attr::DbgSrcLoc(OrdAssertEq(dbg_src_loc)) => {
2119+
let mut comments = SmallVec::<[_; 8]>::new();
2120+
let mut next_dbg_src_loc = Some(dbg_src_loc);
2121+
while let Some(dbg_src_loc) = next_dbg_src_loc {
2122+
let DbgSrcLoc {
2123+
file_path,
2124+
start_line_col: (start_line, mut start_col),
2125+
end_line_col: (end_line, mut end_col),
2126+
inlined_callee_name_and_call_site,
2127+
} = dbg_src_loc;
2128+
2129+
// HACK(eddyb) Rust-GPU's column numbers seem
2130+
// off-by-one wrt what e.g. VSCode expects
2131+
// for `:line:col` syntax, but it's hard to
2132+
// tell from the spec and `glslang` doesn't
2133+
// even emit column numbers at all!
2134+
start_col += 1;
2135+
end_col += 1;
2136+
2137+
let mut s = String::new();
2138+
if comments.is_empty() {
2139+
s += "// at ";
2140+
} else {
2141+
s += "// … at ";
2142+
}
2143+
2144+
// HACK(eddyb) only skip string-quoting and escaping for
2145+
// well-behaved file paths.
2146+
let file_path = &printer.cx[file_path];
2147+
if file_path.chars().all(|c| c.is_ascii_graphic() && c != ':') {
2148+
s += file_path;
2149+
} else {
2150+
write!(s, "{file_path:?}").unwrap();
2151+
}
2152+
2153+
// HACK(eddyb) the syntaxes used are taken from VSCode, i.e.:
2154+
// https://github.com/microsoft/vscode/blob/6b924c5/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts#L75-L91
2155+
// (using the most boring syntax possible for every situation).
2156+
let is_quoted = s.ends_with('"');
2157+
let is_range = (start_line, start_col) != (end_line, end_col);
2158+
write!(
2159+
s,
2160+
"{}{start_line}{}{start_col}",
2161+
if is_quoted { ',' } else { ':' },
2162+
if is_quoted && is_range { '.' } else { ':' }
2163+
)
2164+
.unwrap();
2165+
if is_range {
2166+
s += "-";
2167+
if start_line != end_line {
2168+
write!(s, "{end_line}.").unwrap();
2169+
}
2170+
write!(s, "{end_col}").unwrap();
2171+
}
2172+
2173+
// Chain inlined locations by putting the most important
2174+
// details (`file:line:col`) at the start of each comment,
2175+
// and the less important ones (callee name) at the end.
2176+
next_dbg_src_loc =
2177+
inlined_callee_name_and_call_site.map(|(callee_name, call_site_attrs)| {
2178+
s += ", in `";
2179+
2180+
// HACK(eddyb) not trusting non-trivial strings to behave.
2181+
let callee_name = &printer.cx[callee_name];
2182+
if callee_name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
2183+
s += callee_name;
2184+
} else {
2185+
s.extend(callee_name.escape_debug().flat_map(|c| {
2186+
(c == '`').then_some('\\').into_iter().chain([c])
2187+
}));
2188+
}
2189+
2190+
s += "`, inlined …";
2191+
2192+
call_site_attrs.dbg_src_loc(printer.cx).unwrap_or_else(|| DbgSrcLoc {
2193+
file_path: printer.cx.intern("<unknown callee location>"),
2194+
start_line_col: (0, 0),
2195+
end_line_col: (0, 0),
2196+
inlined_callee_name_and_call_site: None,
2197+
})
2198+
});
2199+
2200+
if !comments.is_empty() {
2201+
comments.push(pretty::Node::ForceLineSeparation);
2202+
}
2203+
comments.push(printer.comment_style().apply(s));
2204+
}
2205+
return pretty::Fragment::new(comments);
2206+
}
21142207
Attr::Diagnostics(diags) => {
21152208
return pretty::Fragment::new(
21162209
diags
@@ -2250,24 +2343,6 @@ impl Print for Attr {
22502343
printer.pretty_spv_inst(printer.attr_style(), *opcode, imms, [None])
22512344
}
22522345
}
2253-
&Attr::SpvDebugLine { file_path, line, col } => {
2254-
// HACK(eddyb) Rust-GPU's column numbers seem
2255-
// off-by-one wrt what e.g. VSCode expects
2256-
// for `:line:col` syntax, but it's hard to
2257-
// tell from the spec and `glslang` doesn't
2258-
// even emit column numbers at all!
2259-
let col = col + 1;
2260-
2261-
// HACK(eddyb) only use skip string quoting
2262-
// and escaping for well-behaved file paths.
2263-
let file_path = &printer.cx[file_path.0];
2264-
let comment = if file_path.chars().all(|c| c.is_ascii_graphic() && c != ':') {
2265-
format!("// at {file_path}:{line}:{col}")
2266-
} else {
2267-
format!("// at {file_path:?}:{line}:{col}")
2268-
};
2269-
return printer.comment_style().apply(comment).into();
2270-
}
22712346
&Attr::SpvBitflagsOperand(imm) => printer.pretty_spv_operand_from_imms([imm]),
22722347
};
22732348
pretty::Fragment::new([

src/spv/lift.rs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ use crate::spv::{self, spec};
55
use crate::visit::{InnerVisit, Visitor};
66
use crate::{
77
AddrSpace, Attr, AttrSet, Const, ConstDef, ConstKind, Context, DataInst, DataInstDef,
8-
DataInstForm, DataInstFormDef, DataInstKind, DeclDef, EntityList, ExportKey, Exportee, Func,
9-
FuncDecl, FuncParam, FxIndexMap, FxIndexSet, GlobalVar, GlobalVarDefBody, Import, Module,
10-
ModuleDebugInfo, ModuleDialect, Node, NodeKind, NodeOutputDecl, Region, RegionInputDecl,
11-
SelectionKind, Type, TypeDef, TypeKind, TypeOrConst, Value, cfg,
8+
DataInstForm, DataInstFormDef, DataInstKind, DbgSrcLoc, DeclDef, EntityList, ExportKey,
9+
Exportee, Func, FuncDecl, FuncParam, FxIndexMap, FxIndexSet, GlobalVar, GlobalVarDefBody,
10+
Import, Module, ModuleDebugInfo, ModuleDialect, Node, NodeKind, NodeOutputDecl, OrdAssertEq,
11+
Region, RegionInputDecl, SelectionKind, Type, TypeDef, TypeKind, TypeOrConst, Value, cfg,
1212
};
1313
use rustc_hash::FxHashMap;
1414
use smallvec::SmallVec;
@@ -207,8 +207,8 @@ impl Visitor<'_> for NeedsIdsCollector<'_> {
207207
| Attr::QPtr(_)
208208
| Attr::SpvAnnotation { .. }
209209
| Attr::SpvBitflagsOperand(_) => {}
210-
Attr::SpvDebugLine { file_path, .. } => {
211-
self.debug_strings.insert(&self.cx[file_path.0]);
210+
Attr::DbgSrcLoc(OrdAssertEq(DbgSrcLoc { file_path, .. })) => {
211+
self.debug_strings.insert(&self.cx[file_path]);
212212
}
213213
}
214214
attr.inner_visit_with(self);
@@ -1508,9 +1508,9 @@ impl Module {
15081508

15091509
for attr in cx[attrs].attrs.iter() {
15101510
match attr {
1511-
Attr::Diagnostics(_)
1511+
Attr::DbgSrcLoc(_)
1512+
| Attr::Diagnostics(_)
15121513
| Attr::QPtr(_)
1513-
| Attr::SpvDebugLine { .. }
15141514
| Attr::SpvBitflagsOperand(_) => {}
15151515
Attr::SpvAnnotation(inst @ spv::Inst { opcode, .. }) => {
15161516
let target_id = result_id.expect(
@@ -1718,15 +1718,12 @@ impl Module {
17181718
// in order to end up with the expected line debuginfo.
17191719
// FIXME(eddyb) make this less of a search and more of a
17201720
// lookup by splitting attrs into key and value parts.
1721-
let new_debug_line = cx[attrs].attrs.iter().find_map(|attr| match *attr {
1722-
Attr::SpvDebugLine { file_path, line, col } => {
1723-
Some((ids.debug_strings[&cx[file_path.0]], line, col))
1724-
}
1725-
_ => None,
1721+
let new_debug_line = attrs.dbg_src_loc(cx).map(|dbg_src_loc| {
1722+
(ids.debug_strings[&cx[dbg_src_loc.file_path]], dbg_src_loc.start_line_col)
17261723
});
17271724
if current_debug_line != new_debug_line {
17281725
let (opcode, imms, ids) = match new_debug_line {
1729-
Some((file_path_id, line, col)) => (
1726+
Some((file_path_id, (line, col))) => (
17301727
wk.OpLine,
17311728
[
17321729
spv::Imm::Short(wk.LiteralInteger, line),

0 commit comments

Comments
 (0)