Skip to main content

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}