qcode/pass_scope.rs
1//! Thread-local identity of the currently-running analysis pass.
2//!
3//! The pipeline driver brackets every pass invocation in a [`PassScope`] guard.
4//! While the guard is alive, [`current_pass`] names the pass, so the
5//! assumption API ([`Context::assume_true`](crate::context::Context::assume_true)
6//! and friends), the [`pass_log!`](crate::pass_log) macro, and the
7//! [`stat!`](crate::stat) counters attribute their records to the right pass
8//! without any plumbing through call signatures — free helper functions get
9//! the attribution too.
10//!
11//! Outside any scope, [`current_pass`] reports `"?"` so stray records remain
12//! visible (and greppable) rather than silently unattributed.
13
14use std::cell::{Cell, RefCell};
15use std::collections::BTreeMap;
16
17thread_local! {
18 static CURRENT_PASS: Cell<&'static str> = const { Cell::new(UNATTRIBUTED) };
19 static STATS: RefCell<BTreeMap<(&'static str, &'static str), u64>> =
20 const { RefCell::new(BTreeMap::new()) };
21}
22
23/// The name reported outside any [`PassScope`].
24pub const UNATTRIBUTED: &str = "?";
25
26/// The name of the pass currently running on this thread (or
27/// [`UNATTRIBUTED`]).
28pub fn current_pass() -> &'static str {
29 CURRENT_PASS.get()
30}
31
32/// RAII guard naming the current pass; restores the previous name on drop, so
33/// scopes nest (a driver phase can wrap individual passes).
34pub struct PassScope {
35 prev: &'static str,
36}
37
38/// Enter a pass scope. Hold the returned guard for the duration of the pass.
39pub fn enter(name: &'static str) -> PassScope {
40 PassScope {
41 prev: CURRENT_PASS.replace(name),
42 }
43}
44
45impl Drop for PassScope {
46 fn drop(&mut self) {
47 CURRENT_PASS.set(self.prev);
48 }
49}
50
51/// Add `n` to the counter `key` of the current pass. Prefer the
52/// [`stat!`](crate::stat) macro.
53pub fn record_stat(key: &'static str, n: u64) {
54 STATS.with_borrow_mut(|stats| {
55 *stats.entry((current_pass(), key)).or_insert(0) += n;
56 });
57}
58
59/// Drain all counters accumulated on this thread since the last drain, as
60/// `((pass, key), total)` in sorted order. The pipeline driver calls this once
61/// per round to log a statistics table.
62pub fn drain_stats() -> Vec<((&'static str, &'static str), u64)> {
63 STATS.with_borrow_mut(std::mem::take).into_iter().collect()
64}
65
66/// Fold `entries` (as produced by [`drain_stats`]) back into this thread's
67/// counters. The parallel function-pass driver drains each worker thread's
68/// counters before the thread exits (thread-local state would otherwise be lost)
69/// and re-absorbs them on the master thread, so the per-round statistics table is
70/// identical to a sequential run.
71pub fn absorb_stats(entries: impl IntoIterator<Item = ((&'static str, &'static str), u64)>) {
72 STATS.with_borrow_mut(|stats| {
73 for (key, n) in entries {
74 *stats.entry(key).or_insert(0) += n;
75 }
76 });
77}
78
79/// Log a message attributed to the current pass: the `log` target is the pass
80/// name (so `RUST_LOG=mem2reg=debug` filters per pass) and the message is
81/// prefixed with it.
82///
83/// Level conventions: `debug` = what the pass did, `trace` = per-instruction
84/// detail, `warn` = suspicious but continuing; `info` is reserved for the
85/// pipeline driver.
86///
87/// ```ignore
88/// pass_log!(debug, "promoted {} stack slots", n);
89/// ```
90#[macro_export]
91macro_rules! pass_log {
92 ($lvl:ident, $($arg:tt)+) => {{
93 let __pass = $crate::pass_scope::current_pass();
94 $crate::__log::$lvl!(target: __pass, "[{}] {}", __pass, format_args!($($arg)+));
95 }};
96}
97
98/// Add to a named counter of the current pass; the driver aggregates and logs
99/// them per round. `stat!("slots_promoted")` increments by 1.
100///
101/// ```ignore
102/// stat!("slots_promoted", promoted.len() as u64);
103/// ```
104#[macro_export]
105macro_rules! stat {
106 ($key:expr) => {
107 $crate::pass_scope::record_stat($key, 1)
108 };
109 ($key:expr, $n:expr) => {
110 $crate::pass_scope::record_stat($key, $n)
111 };
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn scopes_nest_and_restore() {
120 assert_eq!(current_pass(), UNATTRIBUTED);
121 {
122 let _outer = enter("outer");
123 assert_eq!(current_pass(), "outer");
124 {
125 let _inner = enter("inner");
126 assert_eq!(current_pass(), "inner");
127 }
128 assert_eq!(current_pass(), "outer");
129 }
130 assert_eq!(current_pass(), UNATTRIBUTED);
131 }
132
133 #[test]
134 fn stats_accumulate_per_pass_and_drain() {
135 let _ = drain_stats();
136 {
137 let _s = enter("p1");
138 record_stat("k", 2);
139 record_stat("k", 3);
140 }
141 {
142 let _s = enter("p2");
143 record_stat("k", 1);
144 }
145 let drained = drain_stats();
146 assert_eq!(drained, vec![(("p1", "k"), 5), (("p2", "k"), 1)]);
147 assert!(drain_stats().is_empty());
148 }
149}