rucc_pp/trace.rs
1//! The expansion trace: which macros a token came out of, and where each was written.
2//!
3//! Design: `spec/05-preprocessor.md` section 5.5, which asks that every token produced by
4//! expansion carry its spelling location, its expansion location, and a pointer into a trace,
5//! so that a diagnostic can print the chain from the error site up through three nested macros
6//! to the call the user wrote. `spec/03-architecture.md` section 3.4 calls that chain the
7//! single most useful thing a C compiler can do.
8//!
9//! A token already carried two spans. The one it did not carry is the middle: with only the
10//! spelling and the outermost invocation, an error in a macro three deep says where the user
11//! typed and where the text lives and nothing about how one became the other, which is the
12//! part that is hard to work out by hand.
13//!
14//! # Shape
15//!
16//! One step per macro traversed, each pointing at the step outside it, so the chain is a linked
17//! list running inwards out. Expansion goes the other way, outermost macro first, so a step is
18//! built pointing at the one already there. Steps are interned on their contents, and that is
19//! what makes this affordable: every token of one replacement list has the same name, the same
20//! invocation and the same chain above it, so they all intern to one step. A hundred token
21//! replacement list adds one node, not a hundred.
22//!
23//! There is one table per translation unit, so a [`TraceId`] from one is meaningless in
24//! another, the same rule the hide sets follow.
25
26use std::collections::HashMap;
27
28use rucc_base::Symbol;
29use rucc_diag::Span;
30
31/// A pointer into a [`Traces`] table, or [`TraceId::NONE`] for a token the user wrote.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub struct TraceId(u32);
34
35impl TraceId {
36 /// No expansion, which is index zero in every table.
37 ///
38 /// A constant rather than a table lookup because a token straight from the lexer has one
39 /// and building it should not need the table to exist yet.
40 pub const NONE: TraceId = TraceId(0);
41
42 /// Whether this token came out of no macro at all.
43 #[inline]
44 pub const fn is_none(self) -> bool {
45 self.0 == 0
46 }
47
48 /// The underlying index, for packing a trace into a token.
49 #[inline]
50 pub const fn raw(self) -> u32 {
51 self.0
52 }
53}
54
55/// One macro traversed on the way from the user's text to a token.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub struct Step {
58 /// The macro that was expanded.
59 pub macro_name: Symbol,
60 /// Where its invocation was written. Inside another macro's body for every step but the
61 /// outermost, and in a file the user wrote for that one.
62 pub at: Span,
63 /// The step this one sits inside, or [`TraceId::NONE`] if this is the outermost.
64 pub outer: TraceId,
65}
66
67/// The interning table for expansion traces.
68#[derive(Debug, Default)]
69pub struct Traces {
70 /// Every distinct step. A [`TraceId`] of `n` is `steps[n - 1]`, so that zero can mean no
71 /// expansion without a placeholder entry that has to be built out of a `Symbol` nobody
72 /// has yet.
73 steps: Vec<Step>,
74 map: HashMap<Step, TraceId>,
75}
76
77impl Traces {
78 /// An empty table.
79 pub fn new() -> Traces {
80 Traces::default()
81 }
82
83 /// Records that `macro_name`, invoked at `at`, was reached from `outer`.
84 ///
85 /// The result is the trace the tokens it produces should carry. Expansion works outermost
86 /// macro first, so the chain above a step is always already interned by the time the step
87 /// is, and the list grows inwards.
88 pub fn push(&mut self, macro_name: Symbol, at: Span, outer: TraceId) -> TraceId {
89 let step = Step { macro_name, at, outer };
90 if let Some(&found) = self.map.get(&step) {
91 return found;
92 }
93 // Saturating rather than wrapping. A translation unit with four billion distinct
94 // expansion steps has stopped being a translation unit, and reusing an index would
95 // point a diagnostic at a macro that has nothing to do with it.
96 let Ok(next) = u32::try_from(self.steps.len() + 1) else {
97 return outer;
98 };
99 let id = TraceId(next);
100 self.steps.push(step);
101 self.map.insert(step, id);
102 id
103 }
104
105 /// One step, or `None` for [`TraceId::NONE`].
106 #[must_use]
107 pub fn step(&self, id: TraceId) -> Option<Step> {
108 self.steps.get((id.0 as usize).checked_sub(1)?).copied()
109 }
110
111 /// The chain from the outermost macro inwards, which is the order a reader wants it.
112 ///
113 /// The list points the other way, because a step can only be interned once the chain above
114 /// it exists, so this walks it and reverses. Chains are a handful of steps long in the
115 /// worst real case, so the allocation is not worth avoiding.
116 #[must_use]
117 pub fn chain(&self, id: TraceId) -> Vec<Step> {
118 let mut out = Vec::new();
119 let mut at = id;
120 // A cycle cannot happen, because `push` only ever points a new step at an existing
121 // one, but a bound costs nothing and a compiler that hangs is worse than one that is
122 // wrong in a way you can see.
123 while let Some(step) = self.step(at) {
124 out.push(step);
125 at = step.outer;
126 if out.len() > 256 {
127 break;
128 }
129 }
130 out.reverse();
131 out
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use rucc_base::Interner;
138
139 use super::*;
140
141 /// Two names, since there is no way to make a `Symbol` without an interner and no reason
142 /// to want one.
143 fn names() -> (Interner, Symbol, Symbol) {
144 let mut interner = Interner::new();
145 let cat = interner.intern("CAT");
146 let outer = interner.intern("OUTER");
147 (interner, cat, outer)
148 }
149
150 fn span(lo: u32) -> Span {
151 Span::new(lo, lo + 1)
152 }
153
154 #[test]
155 fn a_token_the_user_wrote_has_no_chain() {
156 let traces = Traces::new();
157 assert!(TraceId::NONE.is_none());
158 assert_eq!(traces.step(TraceId::NONE), None);
159 assert!(traces.chain(TraceId::NONE).is_empty());
160 }
161
162 #[test]
163 fn the_chain_reads_from_the_outermost_macro_inwards() {
164 // What `#define CAT(a,b) a##b` used by `#define OUTER(y) CAT(y,+)` builds: `OUTER` is
165 // expanded first, then `CAT` is found in what it produced.
166 let (_interner, cat_name, outer_name) = names();
167 let mut traces = Traces::new();
168 let outer = traces.push(outer_name, span(40), TraceId::NONE);
169 let cat = traces.push(cat_name, span(20), outer);
170 let chain = traces.chain(cat);
171 assert_eq!(chain.len(), 2);
172 assert_eq!(chain[0].macro_name, outer_name);
173 assert_eq!(chain[0].at, span(40));
174 assert_eq!(chain[1].macro_name, cat_name);
175 assert_eq!(chain[1].at, span(20));
176 }
177
178 #[test]
179 fn the_same_step_twice_is_stored_once() {
180 // The reason this is affordable. Every token of one replacement list arrives here with
181 // the same name, the same invocation and the same chain above it.
182 let (_interner, cat_name, _) = names();
183 let mut traces = Traces::new();
184 let first = traces.push(cat_name, span(20), TraceId::NONE);
185 let second = traces.push(cat_name, span(20), TraceId::NONE);
186 assert_eq!(first, second);
187 assert_eq!(traces.steps.len(), 1);
188 // A different chain above is a different step, even for the same macro at the same
189 // place, because the same header can be reached two ways.
190 let third = traces.push(cat_name, span(20), first);
191 assert_ne!(third, first);
192 assert_eq!(traces.chain(third).len(), 2);
193 }
194
195 #[test]
196 fn two_macros_of_the_same_name_at_different_places_are_different_steps() {
197 let (_interner, cat_name, _) = names();
198 let mut traces = Traces::new();
199 let here = traces.push(cat_name, span(20), TraceId::NONE);
200 let there = traces.push(cat_name, span(90), TraceId::NONE);
201 assert_ne!(here, there);
202 }
203}