Skip to main content

celox_sir_opt/
policy.rs

1use crate::HashSet;
2
3// ── OptLevel / SirPass / OptimizeOptions ────────────────────────────
4
5/// Optimization level presets, analogous to GCC's `-O` flags.
6///
7/// Each level sets defaults for SIR passes, Cranelift backend options,
8/// and dead store elimination policy. Individual passes can be overridden
9/// via [`OptimizeOptions::enable`] / [`OptimizeOptions::disable`].
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
11pub enum OptLevel {
12    /// No semantic SIR optimizations. The compatibility
13    /// [`SirPass::TailCallSplit`] selector is consumed only by Cranelift.
14    /// Cranelift: `fast_compile()`. DSE: Off.
15    O0,
16    /// Production-default SIR optimizations enabled, including conservative
17    /// two-state control-flow recovery for profitable Mux regions.
18    /// Cranelift: Speed / Backtracking. DSE: Off.
19    #[default]
20    O1,
21    /// Production-default SIR optimizations + DSE(`PreserveTopPorts`),
22    /// including conservative two-state control-flow recovery for profitable
23    /// Mux regions.
24    /// Cranelift: Speed / Backtracking.
25    O2,
26}
27
28impl OptLevel {
29    /// Returns whether a given SIR pass is enabled by default at this level.
30    pub fn default_enabled(self, pass: SirPass) -> bool {
31        match self {
32            OptLevel::O0 => matches!(pass, SirPass::TailCallSplit),
33            OptLevel::O1 | OptLevel::O2 => true,
34        }
35    }
36
37    /// Parse from string (for NAPI/CLI).
38    pub fn parse(s: &str) -> Option<Self> {
39        match s {
40            "O0" | "o0" => Some(Self::O0),
41            "O1" | "o1" => Some(Self::O1),
42            "O2" | "o2" => Some(Self::O2),
43            _ => None,
44        }
45    }
46
47    /// String representation.
48    pub fn as_str(self) -> &'static str {
49        match self {
50            OptLevel::O0 => "O0",
51            OptLevel::O1 => "O1",
52            OptLevel::O2 => "O2",
53        }
54    }
55}
56
57// Keep the public selector, its serialized spelling, and `ALL` in one source
58// of truth. A pass added here is automatically visible to CLI/NAPI parsing.
59macro_rules! define_sir_passes {
60    ($( $(#[$meta:meta])* $variant:ident => $name:literal ),+ $(,)?) => {
61        /// Individual SIR optimization passes that can be toggled.
62        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63        pub enum SirPass {
64            $( $(#[$meta])* $variant, )+
65        }
66
67        impl SirPass {
68            /// All pass variants in definition order.
69            pub const ALL: &'static [Self] = &[$(Self::$variant),+];
70
71            /// Snake_case string representation (for NAPI/TS serialization).
72            pub fn as_str(self) -> &'static str {
73                match self {
74                    $(Self::$variant => $name),+
75                }
76            }
77
78            /// Parse from snake_case string (for NAPI/CLI).
79            pub fn parse(value: &str) -> Option<Self> {
80                match value {
81                    $($name => Some(Self::$variant),)+
82                    _ => None,
83                }
84            }
85        }
86    };
87}
88
89define_sir_passes! {
90    StoreLoadForwarding => "store_load_forwarding",
91    ControlFlowSimplify => "control_flow_simplify",
92    HoistCommonBranchLoads => "hoist_common_branch_loads",
93    BitExtractPeephole => "bit_extract_peephole",
94    OptimizeBlocks => "optimize_blocks",
95    SplitWideCommits => "split_wide_commits",
96    CommitSinking => "commit_sinking",
97    InlineCommitForwarding => "inline_commit_forwarding",
98    EliminateDeadWorkingStores => "eliminate_dead_working_stores",
99    Reschedule => "reschedule",
100    CoalesceStores => "coalesce_stores",
101    Gvn => "gvn",
102    ConcatFolding => "concat_folding",
103    XorChainFolding => "xor_chain_folding",
104    VectorizeConcat => "vectorize_concat",
105    MaskedArrayAny => "masked_array_any",
106    CircularPriority => "circular_priority",
107    IndexedStoreRecovery => "indexed_store_recovery",
108    GuardedRegionSinking => "guarded_region_sinking",
109    LoopIdiom => "loop_idiom",
110    PackedScatterStore => "packed_scatter_store",
111    SparseCaseDispatch => "sparse_case_dispatch",
112    BranchifyMux => "branchify_mux",
113    SplitCoalescedStores => "split_coalesced_stores",
114    PartialForward => "partial_forward",
115    IdentityStoreBypass => "identity_store_bypass",
116    /// Compatibility selector for Cranelift oversized-function planning.
117    /// This is not a SIR transform and is consumed at the backend boundary.
118    TailCallSplit => "tail_call_split",
119}
120/// Diagnostics and additional verification for the SIR optimizer.
121///
122/// These switches are explicit so optimizer behavior is independent of the
123/// process environment and multiple compilations can use different settings.
124#[derive(Debug, Clone, Default, PartialEq, Eq)]
125pub struct SirDiagnostics {
126    pub pass_timing: bool,
127    pub branchify_stats: bool,
128    pub mux_chain_stats: bool,
129    pub verify_boundaries: bool,
130    pub verify_passes: bool,
131    pub branchify_verify: bool,
132    pub branchify_trace_reg: Option<usize>,
133    pub effect_case_dispatch: bool,
134}
135
136/// Controls which SIR optimization passes are enabled.
137///
138/// Built from an [`OptLevel`] preset, with optional per-pass overrides.
139///
140/// # Examples
141///
142/// ```
143/// use celox_sir_opt::{OptLevel, SirPass, OptimizeOptions};
144///
145/// // Production defaults enabled, including BranchifyMux.
146/// let opts = OptimizeOptions::default();
147/// assert!(opts.is_enabled(SirPass::Gvn));
148/// assert!(opts.is_enabled(SirPass::BranchifyMux));
149///
150/// // O0 with one pass selectively enabled
151/// let opts = OptimizeOptions::new(OptLevel::O0)
152///     .enable(SirPass::Gvn);
153/// assert!(opts.is_enabled(SirPass::Gvn));
154/// assert!(!opts.is_enabled(SirPass::Reschedule));
155/// ```
156#[derive(Debug, Clone)]
157pub struct OptimizeOptions {
158    opt_level: OptLevel,
159    enabled: HashSet<SirPass>,
160    disabled: HashSet<SirPass>,
161    max_native_memory_width: usize,
162    pub diagnostics: SirDiagnostics,
163}
164
165impl Default for OptimizeOptions {
166    fn default() -> Self {
167        Self::new(OptLevel::default())
168    }
169}
170
171impl OptimizeOptions {
172    /// Create options from an optimization level preset.
173    pub fn new(level: OptLevel) -> Self {
174        Self {
175            opt_level: level,
176            enabled: HashSet::default(),
177            disabled: HashSet::default(),
178            max_native_memory_width: if cfg!(feature = "wide-native-memory") {
179                128
180            } else {
181                64
182            },
183            diagnostics: SirDiagnostics::default(),
184        }
185    }
186
187    /// All production-default passes enabled (equivalent to `OptLevel::O1`).
188    pub fn all() -> Self {
189        Self::new(OptLevel::O1)
190    }
191
192    /// All passes disabled except TailCallSplit (equivalent to `OptLevel::O0`).
193    pub fn none() -> Self {
194        Self::new(OptLevel::O0)
195    }
196
197    /// Enable a pass regardless of the OptLevel default.
198    pub fn enable(mut self, pass: SirPass) -> Self {
199        self.disabled.remove(&pass);
200        self.enabled.insert(pass);
201        self
202    }
203
204    /// Disable a pass regardless of the OptLevel default.
205    pub fn disable(mut self, pass: SirPass) -> Self {
206        self.enabled.remove(&pass);
207        self.disabled.insert(pass);
208        self
209    }
210
211    /// Set the largest contiguous Store represented as one SIR value.
212    ///
213    /// 64 preserves scalar word-sized placement. 128 exposes one x86 vector
214    /// to target SLP while wider stores are still split before lowering.
215    pub fn with_max_native_memory_width(mut self, width: usize) -> Self {
216        assert!(
217            matches!(width, 64 | 128),
218            "coalesced Store width must be 64 or 128 bits"
219        );
220        self.max_native_memory_width = width;
221        self
222    }
223
224    pub fn max_native_memory_width(&self) -> usize {
225        self.max_native_memory_width
226    }
227
228    /// Query whether a specific pass is active.
229    pub fn is_enabled(&self, pass: SirPass) -> bool {
230        if self.enabled.contains(&pass) {
231            return true;
232        }
233        if self.disabled.contains(&pass) {
234            return false;
235        }
236        self.opt_level.default_enabled(pass)
237    }
238
239    /// Returns true if any pass other than TailCallSplit is enabled.
240    pub fn any_enabled(&self) -> bool {
241        SirPass::ALL
242            .iter()
243            .any(|&p| p != SirPass::TailCallSplit && self.is_enabled(p))
244    }
245
246    /// The base optimization level.
247    pub fn opt_level(&self) -> OptLevel {
248        self.opt_level
249    }
250}
251
252#[derive(Debug, Clone)]
253pub struct PassOptions {
254    pub max_inflight_loads: usize,
255    pub four_state: bool,
256    pub optimize_options: OptimizeOptions,
257    /// Preserve source array element boundaries for a backend layout that
258    /// stores each element in its own naturally sized scalar slot.
259    pub preserve_element_storage_layout: bool,
260}
261
262impl Default for PassOptions {
263    fn default() -> Self {
264        Self {
265            max_inflight_loads: 8,
266            four_state: false,
267            optimize_options: OptimizeOptions::default(),
268            preserve_element_storage_layout: false,
269        }
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::{OptLevel, OptimizeOptions, SirPass};
276    use crate::HashSet;
277
278    #[test]
279    fn every_pass_name_round_trips_and_is_unique() {
280        let names = SirPass::ALL
281            .iter()
282            .map(|&pass| {
283                let name = pass.as_str();
284                assert_eq!(SirPass::parse(name), Some(pass));
285                name
286            })
287            .collect::<HashSet<_>>();
288
289        assert_eq!(names.len(), SirPass::ALL.len());
290    }
291
292    #[test]
293    fn o0_only_enables_the_backend_compatibility_selector() {
294        let options = OptimizeOptions::new(OptLevel::O0);
295        for &pass in SirPass::ALL {
296            assert_eq!(options.is_enabled(pass), pass == SirPass::TailCallSplit);
297        }
298        assert!(!options.any_enabled());
299    }
300
301    #[test]
302    fn explicit_overrides_take_precedence_over_the_level() {
303        let options = OptimizeOptions::new(OptLevel::O0)
304            .enable(SirPass::Gvn)
305            .disable(SirPass::TailCallSplit);
306        assert!(options.is_enabled(SirPass::Gvn));
307        assert!(!options.is_enabled(SirPass::TailCallSplit));
308        assert!(options.any_enabled());
309    }
310
311    #[test]
312    fn branchify_is_enabled_by_optimization_presets() {
313        assert!(OptimizeOptions::new(OptLevel::O1).is_enabled(SirPass::BranchifyMux));
314        assert!(OptimizeOptions::new(OptLevel::O2).is_enabled(SirPass::BranchifyMux));
315        assert!(OptimizeOptions::all().is_enabled(SirPass::BranchifyMux));
316    }
317
318    #[test]
319    fn control_flow_simplify_is_a_production_default() {
320        assert!(OptimizeOptions::new(OptLevel::O1).is_enabled(SirPass::ControlFlowSimplify));
321        assert!(OptimizeOptions::new(OptLevel::O2).is_enabled(SirPass::ControlFlowSimplify));
322        assert!(!OptimizeOptions::new(OptLevel::O0).is_enabled(SirPass::ControlFlowSimplify));
323    }
324
325    #[test]
326    fn masked_array_any_is_cli_addressable_and_a_production_default() {
327        assert_eq!(
328            SirPass::parse("masked_array_any"),
329            Some(SirPass::MaskedArrayAny)
330        );
331        assert_eq!(SirPass::MaskedArrayAny.as_str(), "masked_array_any");
332        assert!(OptimizeOptions::new(OptLevel::O1).is_enabled(SirPass::MaskedArrayAny));
333        assert!(OptimizeOptions::new(OptLevel::O2).is_enabled(SirPass::MaskedArrayAny));
334        assert!(!OptimizeOptions::new(OptLevel::O0).is_enabled(SirPass::MaskedArrayAny));
335    }
336
337    #[test]
338    fn circular_priority_is_cli_addressable_and_a_production_default() {
339        assert_eq!(
340            SirPass::parse("circular_priority"),
341            Some(SirPass::CircularPriority)
342        );
343        assert_eq!(SirPass::CircularPriority.as_str(), "circular_priority");
344        assert!(OptimizeOptions::new(OptLevel::O1).is_enabled(SirPass::CircularPriority));
345        assert!(OptimizeOptions::new(OptLevel::O2).is_enabled(SirPass::CircularPriority));
346        assert!(!OptimizeOptions::new(OptLevel::O0).is_enabled(SirPass::CircularPriority));
347    }
348
349    #[test]
350    fn indexed_store_recovery_is_cli_addressable_and_a_production_default() {
351        assert_eq!(
352            SirPass::parse("indexed_store_recovery"),
353            Some(SirPass::IndexedStoreRecovery)
354        );
355        assert_eq!(
356            SirPass::IndexedStoreRecovery.as_str(),
357            "indexed_store_recovery"
358        );
359        assert!(OptimizeOptions::new(OptLevel::O1).is_enabled(SirPass::IndexedStoreRecovery));
360        assert!(OptimizeOptions::new(OptLevel::O2).is_enabled(SirPass::IndexedStoreRecovery));
361        assert!(!OptimizeOptions::new(OptLevel::O0).is_enabled(SirPass::IndexedStoreRecovery));
362        assert!(
363            !OptimizeOptions::new(OptLevel::O2)
364                .disable(SirPass::IndexedStoreRecovery)
365                .is_enabled(SirPass::IndexedStoreRecovery)
366        );
367    }
368}