Skip to content

Commit a3d6e40

Browse files
authored
Cranelift: add debug tag infrastructure. (#11768)
* Cranelift: add debug tag infrastructure. This PR adds *debug tags*, a kind of metadata that can attach to CLIF instructions and be lowered to VCode instructions and as metadata on the produced compiled code. It also adds opaque descriptor blobs carried with stackslots. Together, these two features allow decorating IR with first-class debug instrumentation that is properly preserved by the compiler, including across optimizations and inlining. (Wasmtime's use of these features will come in followup PRs.) The key idea of a "debug tag" is to allow the Cranelift embedder to express whatever information it needs to, in a format that is opaque to Cranelift itself, except for the parts that need translation during lowering. In particular, the `DebugTag::StackSlot` variant gets translated to a physical offset into the stackframe in the compiled metadata output. So, for example, the embedder can emit a tag referring to a stackslot, and another describing an offset in that stackslot. The debug tags exist as a *sequence* on any given instruction; the meaning of the sequence is known only to the embedder, *except* that during inlining, the tags for the inlining call instruction are prepended to the tags of inlined instructions. In this way, a canonical use-case of tags as describing original source-language frames can preserve the source-language view even when multiple functions are inlined into one. The descriptor on a stackslot may look a little odd at first, but its purpose is to allow serializing some description of stackslot-contained runtime user-program data, in a way that is firmly attached to the stackslot. In particular, in the face of inlining, this descriptor is copied into the inlining (parent) function from the inlined function when the stackslot entity is copied; no other metadata outside Cranelift needs to track the identity of stackslots and know about that motion. This fits nicely with the ability of tags to refer to stackslots; together, the embedder can annotate instructions as having certain state in stackslots, and describe the format of that state per stackslot. This infrastructure is tested with some compile-tests now; testing of the interpretation of the metadata output will come with end-to-end debug instrumentation tests in a followup PR. * Review feedback: add back sequence points and enforce tags only on sequence points or calls. * Use Vecs for debug metadata in MachBuffer to avoid SmallVec size penalty in not-used case. * Review feedback: switch from inlined stackslot descriptor blobs to u64 keys.
1 parent 0ac154b commit a3d6e40

File tree

47 files changed

+1020
-114
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1020
-114
lines changed

cranelift/codegen/meta/src/shared/instructions.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3902,4 +3902,20 @@ pub(crate) fn define(
39023902
Operand::new("a", &TxN.dynamic_to_vector()).with_doc("New fixed vector"),
39033903
]),
39043904
);
3905+
3906+
ig.push(
3907+
Inst::new(
3908+
"sequence_point",
3909+
r#"
3910+
A compiler barrier that acts as an immovable marker from IR input to machine-code output.
3911+
3912+
This "sequence point" can have debug tags attached to it, and these tags will be
3913+
noted in the output `MachBuffer`.
3914+
3915+
It prevents motion of any other side-effects across this boundary.
3916+
"#,
3917+
&formats.nullary,
3918+
)
3919+
.other_side_effects(),
3920+
);
39053921
}

cranelift/codegen/src/inline.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
//! Cranelift the body of the callee that is to be inlined.
2121
2222
use crate::cursor::{Cursor as _, FuncCursor};
23-
use crate::ir::{self, ExceptionTableData, ExceptionTableItem, InstBuilder as _};
23+
use crate::ir::{self, DebugTag, ExceptionTableData, ExceptionTableItem, InstBuilder as _};
2424
use crate::result::CodegenResult;
2525
use crate::trace;
2626
use crate::traversals::Dfs;
@@ -366,6 +366,13 @@ fn inline_one(
366366
// callee.
367367
let mut last_inlined_block = inline_block_layout(func, call_block, callee, &entity_map);
368368

369+
// Get a copy of debug tags on the call instruction; these are
370+
// prepended to debug tags on inlined instructions. Remove them
371+
// from the call itself as it will be rewritten to a jump (which
372+
// cannot have tags).
373+
let call_debug_tags = func.debug_tags.get(call_inst).to_vec();
374+
func.debug_tags.set(call_inst, []);
375+
369376
// Translate each instruction from the callee into the caller,
370377
// appending them to their associated block in the caller.
371378
//
@@ -403,6 +410,29 @@ fn inline_one(
403410
let inlined_inst = func.dfg.make_inst(inlined_inst_data);
404411
func.layout.append_inst(inlined_inst, inlined_block);
405412

413+
// Copy over debug tags, translating referenced entities
414+
// as appropriate.
415+
let debug_tags = callee.debug_tags.get(callee_inst);
416+
// If there are tags on the inlined instruction, we always
417+
// add tags, and we prepend any tags from the call
418+
// instruction; but we don't add tags if only the callsite
419+
// had them (this would otherwise mean that every single
420+
// instruction in an inlined function body would get
421+
// tags).
422+
if !debug_tags.is_empty() {
423+
let tags = call_debug_tags
424+
.iter()
425+
.cloned()
426+
.chain(debug_tags.iter().map(|tag| match *tag {
427+
DebugTag::User(value) => DebugTag::User(value),
428+
DebugTag::StackSlot(slot) => {
429+
DebugTag::StackSlot(entity_map.inlined_stack_slot(slot))
430+
}
431+
}))
432+
.collect::<SmallVec<[_; 4]>>();
433+
func.debug_tags.set(inlined_inst, tags);
434+
}
435+
406436
let opcode = callee.dfg.insts[callee_inst].opcode();
407437
if opcode.is_return() {
408438
// Instructions that return do not define any values, so we

cranelift/codegen/src/inst_predicates.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ pub fn has_memory_fence_semantics(op: Opcode) -> bool {
147147
| Opcode::AtomicLoad
148148
| Opcode::AtomicStore
149149
| Opcode::Fence
150-
| Opcode::Debugtrap => true,
150+
| Opcode::Debugtrap
151+
| Opcode::SequencePoint => true,
151152
Opcode::Call | Opcode::CallIndirect | Opcode::TryCall | Opcode::TryCallIndirect => true,
152153
op if op.can_trap() => true,
153154
_ => false,
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//! Debug tag storage.
2+
//!
3+
//! Cranelift permits the embedder to place "debug tags" on
4+
//! instructions in CLIF. These tags are sequences of items of various
5+
//! kinds, with no other meaning imposed by Cranelift. They are passed
6+
//! through to metadata provided alongside the compilation result.
7+
//!
8+
//! When Cranelift inlines a function, it will prepend any tags from
9+
//! the call instruction at the inlining callsite to tags on all
10+
//! inlined instructions.
11+
//!
12+
//! These tags can be used, for example, to identify stackslots that
13+
//! store user state, or to denote positions in user source. In
14+
//! general, the intent is to allow perfect reconstruction of original
15+
//! (source-level) program state in an instrumentation-based
16+
//! debug-info scheme, as long as the instruction(s) on which these
17+
//! tags are attached are preserved. This will be the case for any
18+
//! instructions with side-effects.
19+
//!
20+
//! A few answers to design questions that lead to this design:
21+
//!
22+
//! - Why not use the SourceLoc mechanism? Debug tags are richer than
23+
//! that infrastructure because they preserve inlining location and
24+
//! are interleaved properly with any other tags describing the
25+
//! frame.
26+
//! - Why not attach debug tags only to special sequence-point
27+
//! instructions? This is driven by inlining: we should have the
28+
//! semantic information about a callsite attached directly to the
29+
//! call and observe it there, not have a magic "look backward to
30+
//! find a sequence point" behavior in the inliner.
31+
//!
32+
//! In other words, the needs of preserving "virtual" frames across an
33+
//! inlining transform drive this design.
34+
35+
use crate::ir::{Inst, StackSlot};
36+
use alloc::collections::BTreeMap;
37+
use alloc::vec::Vec;
38+
use core::ops::Range;
39+
40+
/// Debug tags for instructions.
41+
#[derive(Clone, PartialEq, Hash, Default)]
42+
#[cfg_attr(
43+
feature = "enable-serde",
44+
derive(serde_derive::Serialize, serde_derive::Deserialize)
45+
)]
46+
pub struct DebugTags {
47+
/// Pool of tags, referred to by `insts` below.
48+
tags: Vec<DebugTag>,
49+
50+
/// Per-instruction range for its list of tags in the tag pool (if
51+
/// any).
52+
///
53+
/// Note: we don't use `PackedOption` and `EntityList` here
54+
/// because the values that we are storing are not entities.
55+
insts: BTreeMap<Inst, Range<u32>>,
56+
}
57+
58+
/// One debug tag.
59+
#[derive(Clone, Debug, PartialEq, Hash)]
60+
#[cfg_attr(
61+
feature = "enable-serde",
62+
derive(serde_derive::Serialize, serde_derive::Deserialize)
63+
)]
64+
pub enum DebugTag {
65+
/// User-specified `u32` value, opaque to Cranelift.
66+
User(u32),
67+
68+
/// A stack slot reference.
69+
StackSlot(StackSlot),
70+
}
71+
72+
impl DebugTags {
73+
/// Set the tags on an instruction, overwriting existing tag list.
74+
///
75+
/// Tags can only be set on call instructions (those for which
76+
/// [`crate::Opcode::is_call()`] returns `true`) and on
77+
/// `sequence_point` instructions. This property is checked by the
78+
/// CLIF verifier.
79+
pub fn set(&mut self, inst: Inst, tags: impl IntoIterator<Item = DebugTag>) {
80+
let start = u32::try_from(self.tags.len()).unwrap();
81+
self.tags.extend(tags);
82+
let end = u32::try_from(self.tags.len()).unwrap();
83+
if end > start {
84+
self.insts.insert(inst, start..end);
85+
} else {
86+
self.insts.remove(&inst);
87+
}
88+
}
89+
90+
/// Get the tags associated with an instruction.
91+
pub fn get(&self, inst: Inst) -> &[DebugTag] {
92+
if let Some(range) = self.insts.get(&inst) {
93+
let start = usize::try_from(range.start).unwrap();
94+
let end = usize::try_from(range.end).unwrap();
95+
&self.tags[start..end]
96+
} else {
97+
&[]
98+
}
99+
}
100+
101+
/// Does the given instruction have any tags?
102+
pub fn has(&self, inst: Inst) -> bool {
103+
// We rely on the invariant that an entry in the map is
104+
// present only if the list range is non-empty.
105+
self.insts.contains_key(&inst)
106+
}
107+
108+
/// Clone the tags from one instruction to another.
109+
///
110+
/// This clone is cheap (references the same underlying storage)
111+
/// because the tag lists are immutable.
112+
pub fn clone_tags(&mut self, from: Inst, to: Inst) {
113+
if let Some(range) = self.insts.get(&from).cloned() {
114+
self.insts.insert(to, range);
115+
} else {
116+
self.insts.remove(&to);
117+
}
118+
}
119+
120+
/// Are any debug tags present?
121+
///
122+
/// This is used for adjusting margins when pretty-printing CLIF.
123+
pub fn is_empty(&self) -> bool {
124+
self.insts.is_empty()
125+
}
126+
127+
/// Clear all tags.
128+
pub fn clear(&mut self) {
129+
self.insts.clear();
130+
self.tags.clear();
131+
}
132+
}
133+
134+
impl core::fmt::Display for DebugTag {
135+
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
136+
match self {
137+
DebugTag::User(value) => write!(f, "{value}"),
138+
DebugTag::StackSlot(slot) => write!(f, "{slot}"),
139+
}
140+
}
141+
}

cranelift/codegen/src/ir/function.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
use crate::HashMap;
77
use crate::entity::{PrimaryMap, SecondaryMap};
8+
use crate::ir::DebugTags;
89
use crate::ir::{
910
self, Block, DataFlowGraph, DynamicStackSlot, DynamicStackSlotData, DynamicStackSlots,
1011
DynamicType, ExtFuncData, FuncRef, GlobalValue, GlobalValueData, Inst, JumpTable,
@@ -190,6 +191,22 @@ pub struct FunctionStencil {
190191
/// interpreted by Cranelift, only preserved.
191192
pub srclocs: SourceLocs,
192193

194+
/// Opaque debug-info tags on sequence-point and call
195+
/// instructions.
196+
///
197+
/// These tags are not interpreted by Cranelift, and are passed
198+
/// through to compilation-result metadata. The only semantic
199+
/// structure that Cranelift imposes is that when inlining, it
200+
/// prepends the callsite call instruction's tags to the tags on
201+
/// inlined instructions.
202+
///
203+
/// In order to ensure clarity around guaranteed compiler
204+
/// behavior, tags are only permitted on instructions whose
205+
/// presence and sequence will remain the same in the compiled
206+
/// output: namely, `sequence_point` instructions and ordinary
207+
/// call instructions.
208+
pub debug_tags: DebugTags,
209+
193210
/// An optional global value which represents an expression evaluating to
194211
/// the stack limit for this function. This `GlobalValue` will be
195212
/// interpreted in the prologue, if necessary, to insert a stack check to
@@ -209,6 +226,7 @@ impl FunctionStencil {
209226
self.dfg.clear();
210227
self.layout.clear();
211228
self.srclocs.clear();
229+
self.debug_tags.clear();
212230
self.stack_limit = None;
213231
}
214232

@@ -408,6 +426,7 @@ impl Function {
408426
layout: Layout::new(),
409427
srclocs: SecondaryMap::new(),
410428
stack_limit: None,
429+
debug_tags: DebugTags::default(),
411430
},
412431
params: FunctionParameters::new(),
413432
}

cranelift/codegen/src/ir/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod atomic_rmw_op;
44
mod builder;
55
pub mod condcodes;
66
pub mod constant;
7+
mod debug_tags;
78
pub mod dfg;
89
pub mod dynamic_type;
910
pub mod entities;
@@ -36,6 +37,7 @@ pub use crate::ir::builder::{
3637
InsertBuilder, InstBuilder, InstBuilderBase, InstInserterBase, ReplaceBuilder,
3738
};
3839
pub use crate::ir::constant::{ConstantData, ConstantPool};
40+
pub use crate::ir::debug_tags::{DebugTag, DebugTags};
3941
pub use crate::ir::dfg::{BlockData, DataFlowGraph, ValueDef};
4042
pub use crate::ir::dynamic_type::{DynamicTypeData, DynamicTypes, dynamic_to_fixed};
4143
pub use crate::ir::entities::{
@@ -64,7 +66,7 @@ pub use crate::ir::progpoint::ProgramPoint;
6466
pub use crate::ir::sourceloc::RelSourceLoc;
6567
pub use crate::ir::sourceloc::SourceLoc;
6668
pub use crate::ir::stackslot::{
67-
DynamicStackSlotData, DynamicStackSlots, StackSlotData, StackSlotKind, StackSlots,
69+
DynamicStackSlotData, DynamicStackSlots, StackSlotData, StackSlotKey, StackSlotKind, StackSlots,
6870
};
6971
pub use crate::ir::trapcode::TrapCode;
7072
pub use crate::ir::types::Type;

cranelift/codegen/src/ir/stackslot.rs

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,37 @@ pub struct StackSlotData {
6969
/// be aligned according to other considerations, such as minimum
7070
/// stack slot size or machine word size, as well.
7171
pub align_shift: u8,
72+
73+
/// Opaque stackslot metadata handle, passed through to
74+
/// compilation result metadata describing stackslot location.
75+
///
76+
/// In the face of compiler transforms like inlining that may move
77+
/// stackslots between functions, when an embedder wants to
78+
/// externally observe stackslots, it needs a first-class way for
79+
/// the identity of stackslots to be carried along with the IR
80+
/// entities. This opaque `StackSlotKey` allows the embedder to do
81+
/// so.
82+
pub key: Option<StackSlotKey>,
83+
}
84+
85+
/// An opaque key uniquely identifying a stack slot.
86+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
87+
#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))]
88+
pub struct StackSlotKey(u64);
89+
impl StackSlotKey {
90+
/// Construct a [`StackSlotKey`] from raw bits.
91+
///
92+
/// An embedder can use any 64-bit value to describe a stack slot;
93+
/// there are no restrictions, and the value does not mean
94+
/// anything to Cranelift itself.
95+
pub fn new(value: u64) -> StackSlotKey {
96+
StackSlotKey(value)
97+
}
98+
99+
/// Get the raw bits from the [`StackSlotKey`].
100+
pub fn bits(&self) -> u64 {
101+
self.0
102+
}
72103
}
73104

74105
impl StackSlotData {
@@ -78,23 +109,40 @@ impl StackSlotData {
78109
kind,
79110
size,
80111
align_shift,
112+
key: None,
113+
}
114+
}
115+
116+
/// Create a stack slot with the specified byte size and alignment
117+
/// and the given user-defined key.
118+
pub fn new_with_key(
119+
kind: StackSlotKind,
120+
size: StackSize,
121+
align_shift: u8,
122+
key: StackSlotKey,
123+
) -> Self {
124+
Self {
125+
kind,
126+
size,
127+
align_shift,
128+
key: Some(key),
81129
}
82130
}
83131
}
84132

85133
impl fmt::Display for StackSlotData {
86134
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
87-
if self.align_shift != 0 {
88-
write!(
89-
f,
90-
"{} {}, align = {}",
91-
self.kind,
92-
self.size,
93-
1u32 << self.align_shift
94-
)
135+
let align_shift = if self.align_shift != 0 {
136+
format!(", align = {}", 1u32 << self.align_shift)
95137
} else {
96-
write!(f, "{} {}", self.kind, self.size)
97-
}
138+
"".into()
139+
};
140+
let key = match self.key {
141+
Some(value) => format!(", key = {}", value.bits()),
142+
None => "".into(),
143+
};
144+
145+
write!(f, "{} {}{align_shift}{key}", self.kind, self.size)
98146
}
99147
}
100148

0 commit comments

Comments
 (0)