Skip to main content

analyssa/scheduling/
pass.rs

1//! [`SsaPass`] trait and supporting types ([`ModificationScope`],
2//! [`SsaPassHost`]) for declaring and implementing SSA transformation
3//! passes.
4//!
5//! A pass is a target-agnostic transformation that runs on a single
6//! method's SSA (or globally across all methods) and reports whether
7//! it made changes. The scheduler in [`super::scheduler`] orchestrates
8//! execution: layer assignment from capability dependencies, per-layer
9//! fixpoint, parallel per-method dispatch via rayon, and
10//! modification-scope-driven repair after each pass.
11
12use crate::{
13    error::Result,
14    events::EventListener,
15    host::{DirtySet, SsaStore},
16    ir::function::SsaFunction,
17    pointer::PointerSize,
18    target::Target,
19    world::World,
20};
21
22/// Combined host surface that the scheduler exposes to passes.
23///
24/// Scheduler hosts implement [`World<T>`], [`SsaStore<T>`], and
25/// [`DirtySet<T>`] separately, plus this trait to surface the event
26/// sink and pointer size. The scheduler hands passes a
27/// `&dyn SsaPassHost<T>` so they can read the call graph, look up peer
28/// methods' SSA, mark methods dirty, and record events without knowing
29/// the concrete host type.
30pub trait SsaPassHost<T: Target>: World<T> + SsaStore<T> + DirtySet<T> + Sync {
31    /// Returns the event sink for transformation events recorded by passes.
32    ///
33    /// A trait object rather than a concrete [`EventLog<T>`], so a host can
34    /// choose its sink. [`EventLog<T>`] is an append-only `boxcar::Vec` with no
35    /// bound and no way to drain it, so naming it concretely would force a host
36    /// that never consumes events to retain every one for the process lifetime.
37    /// [`NullListener`] discards them instead.
38    ///
39    /// [`EventLog<T>`]: crate::events::EventLog
40    /// [`NullListener`]: crate::events::NullListener
41    fn events(&self) -> &dyn EventListener<T>;
42
43    /// Returns the pointer width of the target's runtime.
44    ///
45    /// Used by passes that need to know the host's address size (e.g.,
46    /// predicate evaluation, reassociation, jump threading). Hosts that
47    /// do not care can accept the default of [`PointerSize::Bit64`].
48    fn ptr_size(&self) -> PointerSize {
49        PointerSize::Bit64
50    }
51}
52
53/// Describes the extent of modifications a pass makes to the SSA function.
54///
55/// The scheduler uses this to select the minimum repair necessary after a
56/// pass runs, avoiding expensive full SSA reconstruction when it is not
57/// needed. Passes should declare the **tightest** scope that covers all
58/// their modifications.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
60pub enum ModificationScope {
61    /// The pass only replaces uses of variables with other existing
62    /// variables.
63    ///
64    /// SSA invariants are preserved automatically — no repair is needed
65    /// after the pass. Example: GVN forwarding redundant uses to earlier
66    /// defs.
67    UsesOnly,
68
69    /// The pass replaces or removes instructions but does not change
70    /// the CFG.
71    ///
72    /// After this scope, the scheduler runs a lightweight repair to strip
73    /// `Nop`s, recompute variable metadata, and eliminate trivial phis.
74    /// No dominator or dominance-frontier recomputation is needed.
75    /// Examples: constant propagation, copy propagation, dead-code
76    /// elimination, algebraic simplification, strength reduction.
77    InstructionsOnly,
78
79    /// The pass may add or remove blocks, change predecessors or
80    /// successors, or otherwise modify the control-flow graph.
81    ///
82    /// After this scope, the scheduler runs full SSA reconstruction
83    /// (recompute dominators, place phis, rename variables). Examples:
84    /// control flow unflattening, jump threading, block merging, loop
85    /// canonicalization, inlining.
86    CfgModifying,
87}
88
89/// An SSA transformation pass.
90///
91/// Generic over both the target `T` and the host adapter `H`. The host
92/// type must implement [`SsaPassHost<T>`] and is fixed by the
93/// [`PassScheduler`](crate::scheduling::PassScheduler) instance — all
94/// passes registered with that scheduler see the same host type.
95///
96/// # Concurrency
97///
98/// Passes must be `Send + Sync` so the scheduler can run them in
99/// parallel across methods via rayon. Mutation of pass state happens
100/// only in [`initialize`](Self::initialize) /
101/// [`finalize`](Self::finalize) (single-threaded boundary calls);
102/// per-method work uses `&self` and must rely on interior mutability
103/// for any cross-method state.
104///
105/// # Implementing a pass
106///
107/// Most analyssa-side passes have a pure-function body in `crate::passes`
108/// and a one-line trait impl in the scheduling sub-module:
109///
110/// ```text
111/// impl<T: Target, H: SsaPassHost<T>> SsaPass<T, H> for MyPass {
112///     fn name(&self) -> &'static str { "my-pass" }
113///     fn run_on_method(&self, ssa: &mut SsaFunction<T>, method: &T::MethodRef, host: &H) -> Result<bool> {
114///         Ok(passes::my_pass::run(ssa, method, host.events()))
115///     }
116/// }
117/// ```
118///
119/// Hosts that ship target-specific passes (e.g., CIL inlining) write
120/// impls bounded on a host extension trait, giving the impl access to
121/// host-specific methods while still being storable in a
122/// `Box<dyn SsaPass<CilTarget, ConcreteCilHost>>`.
123pub trait SsaPass<T: Target, H: SsaPassHost<T>>: Send + Sync {
124    /// Returns a unique short name for logging and debugging.
125    fn name(&self) -> &'static str;
126
127    /// Returns a human-readable description of the pass.
128    fn description(&self) -> &'static str {
129        "No description available"
130    }
131
132    /// Determines whether this pass should run on a specific method.
133    ///
134    /// Called before [`run_on_method`](Self::run_on_method). Override to
135    /// skip methods that do not need this pass (e.g., already processed,
136    /// too small to be interesting). Default implementation returns `true`.
137    fn should_run(&self, _method: &T::MethodRef, _host: &H) -> bool {
138        true
139    }
140
141    /// Run the pass on a single method's SSA.
142    ///
143    /// Events are recorded directly to `host.events()`.
144    ///
145    /// # Arguments
146    ///
147    /// * `ssa` — The SSA function to transform in place.
148    /// * `method` — Opaque reference identifying the method being processed.
149    /// * `host` — The host adapter for reading the call graph, looking up
150    ///   peer SSA, marking methods dirty, and recording events.
151    ///
152    /// # Returns
153    ///
154    /// `Ok(true)` if any changes were made, `Ok(false)` if no changes.
155    ///
156    /// # Errors
157    ///
158    /// Returns `Err` if the pass fails to process the method (e.g.,
159    /// malformed SSA, unexpected opcode).
160    fn run_on_method(
161        &self,
162        ssa: &mut SsaFunction<T>,
163        method: &T::MethodRef,
164        host: &H,
165    ) -> Result<bool>;
166
167    /// Run the pass on the entire program (interprocedural passes).
168    ///
169    /// Override for passes that need to see all methods simultaneously,
170    /// such as dead-method elimination or whole-program constant
171    /// propagation.
172    ///
173    /// # Arguments
174    ///
175    /// * `host` — The host adapter providing program-wide state.
176    ///
177    /// # Returns
178    ///
179    /// `Ok(true)` if any changes were made, `Ok(false)` if no changes.
180    ///
181    /// # Errors
182    ///
183    /// Returns `Err` if the pass fails.
184    fn run_global(&self, _host: &H) -> Result<bool> {
185        Ok(false)
186    }
187
188    /// Returns whether this pass operates globally (across all methods).
189    ///
190    /// When `true`, the scheduler invokes [`run_global`](Self::run_global)
191    /// instead of iterating per-method with
192    /// [`run_on_method`](Self::run_on_method).
193    fn is_global(&self) -> bool {
194        false
195    }
196
197    /// Called once before the pass runs in a phase.
198    ///
199    /// Use this to initialize pass-specific state or caches. The default
200    /// implementation is a no-op.
201    ///
202    /// # Errors
203    ///
204    /// Returns an error if initialization fails.
205    fn initialize(&mut self, _host: &H) -> Result<()> {
206        Ok(())
207    }
208
209    /// Called once after the pass completes in a phase.
210    ///
211    /// Use this to clean up pass-specific state. The default
212    /// implementation is a no-op.
213    ///
214    /// # Errors
215    ///
216    /// Returns an error if finalization fails.
217    fn finalize(&mut self, _host: &H) -> Result<()> {
218        Ok(())
219    }
220
221    /// Declares the extent of modifications this pass makes.
222    ///
223    /// Default is [`ModificationScope::CfgModifying`] (conservative).
224    /// Override for passes that do not modify the CFG so the scheduler
225    /// can apply a lighter-weight repair.
226    fn modification_scope(&self) -> ModificationScope {
227        ModificationScope::CfgModifying
228    }
229
230    /// Returns whether the pass performs its own SSA boundary repair.
231    ///
232    /// Passes that wrap their mutation body in
233    /// [`SsaFunction::edit`](crate::ir::function::SsaFunction::edit) should
234    /// return `true` so the scheduler does not run a second repair step on
235    /// the same method. The pass must still report an accurate
236    /// [`modification_scope`](Self::modification_scope) for ordering,
237    /// documentation, and hosts that inspect pass metadata.
238    fn repairs_ssa(&self) -> bool {
239        false
240    }
241
242    /// Returns the capabilities this pass provides after execution.
243    ///
244    /// The scheduler ensures that consumers of a capability are placed in
245    /// later layers than providers.
246    fn provides(&self) -> &[T::Capability] {
247        &[]
248    }
249
250    /// Returns the capabilities this pass requires before it can run.
251    ///
252    /// Unsatisfied requirements (no provider registered) fall back to
253    /// phase-based ordering rather than blocking the pass.
254    fn requires(&self) -> &[T::Capability] {
255        &[]
256    }
257
258    /// Returns whether this pass reads other methods' SSA during
259    /// [`run_on_method`](Self::run_on_method).
260    ///
261    /// During parallel execution, the scheduler must keep each method's
262    /// SSA visible in the store so other threads can read it. When this
263    /// returns `true`, the scheduler clones the SSA before processing
264    /// (instead of removing it) so concurrent visibility is preserved.
265    /// Passes that only modify their own method should keep the default
266    /// `false` to avoid the clone overhead.
267    fn reads_peer_ssa(&self) -> bool {
268        false
269    }
270
271    /// Returns whether this pass requires a full scan of all methods
272    /// every iteration.
273    ///
274    /// If `true`, the scheduler dispatches to every method with SSA,
275    /// regardless of dirty-tracking state. If `false` (default), only
276    /// dirty methods are processed.
277    fn requires_full_scan(&self) -> bool {
278        false
279    }
280
281    /// Returns the fallback execution layer for this pass.
282    ///
283    /// Used when capability dependencies do not constrain the pass's
284    /// position. Hosts assign meaningful numbers (e.g., CIL uses
285    /// Structure=0, Value=1, Simplify=2, Inline=3 by convention).
286    ///
287    /// Normalize-style passes (DCE, GVN, constant propagation) typically
288    /// use layer 0 and are registered via the scheduler's `add_normalize`
289    /// API, which takes them out of the layered execution and runs them
290    /// between every layer's fixpoint iterations.
291    fn fallback_layer(&self) -> usize {
292        0
293    }
294}