Skip to main content

logicaffeine_language/
optimization.rs

1//! The single source of truth for compiler optimization toggles.
2//!
3//! Every optimization the compiler can perform has exactly one row in
4//! [`REGISTRY`], naming it, giving it a human label and a `## No <Keyword>`
5//! decorator keyword, recording which execution paths it touches, whether it
6//! emits `unsafe` Rust, how it trades memory for speed, and its dependencies
7//! and conflicts with other optimizations.
8//!
9//! A live choice of which optimizations are on is an [`OptimizationConfig`] —
10//! a single `u64` bitset, one bit per [`Opt`]. The default is "everything on"
11//! (tuned for speed); disabling everything yields plain, boring Rust.
12//!
13//! This lives in `logicaffeine_language` (not the compiler) because the parser
14//! maps `## No <Keyword>` decorators to [`Opt`]s, so the type must be visible
15//! here; the compiler and JIT see it transitively.
16
17use std::collections::BTreeMap;
18
19/// Execution-path tags (bitmask) recording where an optimization applies.
20pub mod path {
21    /// Ahead-of-time Rust codegen pipeline (`optimize_program`).
22    pub const AOT: u8 = 1 << 0;
23    /// Live run-path optimizer (`optimize_for_run`).
24    pub const RUN: u8 = 1 << 1;
25    /// Bytecode VM.
26    pub const VM: u8 = 1 << 2;
27    /// Forge JIT tier.
28    pub const JIT: u8 = 1 << 3;
29    /// Rust source emission (`codegen_program`).
30    pub const CODEGEN: u8 = 1 << 4;
31}
32
33/// How an optimization trades memory against speed.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum MemClass {
36    /// Neither clearly grows nor shrinks memory.
37    Neutral,
38    /// Spends memory (extra allocation, code growth, caches) to gain speed.
39    TradesMemForSpeed,
40    /// Shrinks memory (tighter representations, fewer allocations).
41    SavesMem,
42}
43
44/// How expensive an optimization pass is to RUN on the live path — the lever the
45/// tiered optimizer uses to decide WHEN (at which hotness tier) to pay for it.
46/// Orthogonal to [`MemClass`] (which is about the OUTPUT's memory cost). The derived
47/// `Ord` makes a tier gate a single comparison: `Cheap < Medium < Heavy` is exactly
48/// the tier-inclusion order T1 ⊂ T2 ⊂ T3.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
50pub enum OptCost {
51    /// A single structural sweep, no fixpoint (inline, DCE, TCO).
52    Cheap = 0,
53    /// A whole-function analysis or one bounded fixpoint (GVN, LICM, oracle,
54    /// fusion, the capped PE-light fixpoint).
55    Medium = 1,
56    /// A cloning / region-growing pass (the full PE fixpoint, unroll, scalarize,
57    /// recursion unfold, equality saturation).
58    Heavy = 2,
59}
60
61/// A hotness tier — how much optimization budget a unit has earned (HOTSWAP §4.1).
62/// Each tier pays for strictly more than the one below; `T3` with an all-on config
63/// reproduces today's whole-program `optimize_for_run`, bit-for-bit (the
64/// compatibility + soundness anchor). The mechanism that escalates a unit through
65/// the tiers (call/back-edge counters, thresholds) lives in the VM; this is just
66/// the policy the optimizer reads.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
68pub enum Tier {
69    /// Baseline: raw parse → bytecode, no optimization.
70    T0 = 0,
71    /// Warm: Cheap opts.
72    T1 = 1,
73    /// Hot: Cheap + Medium opts (PE-light).
74    T2 = 2,
75    /// Very-hot: every opt (PE-full, unroll, recursion unfold).
76    T3 = 3,
77}
78
79impl Tier {
80    /// The most expensive pass this tier will pay for; `None` ⇒ optimize nothing.
81    /// The inclusion order T1 ⊂ T2 ⊂ T3 falls straight out of [`OptCost`]'s `Ord`.
82    #[inline]
83    pub fn budget(self) -> Option<OptCost> {
84        match self {
85            Tier::T0 => None,
86            Tier::T1 => Some(OptCost::Cheap),
87            Tier::T2 => Some(OptCost::Medium),
88            Tier::T3 => Some(OptCost::Heavy),
89        }
90    }
91}
92
93/// Whether `opt` runs at `tier` under `cfg`: it must be enabled in the config AND
94/// cost no more than the tier's budget. The tier→opt-set mapping is therefore
95/// DERIVED from the registry, never hardcoded — adding an opt with a cost slots it
96/// into the right tier automatically (HOTSWAP §4.2).
97///
98/// This is pure policy. Run-path callers additionally AND in any
99/// `#[cfg(feature = "codegen")]` availability for the codegen-only passes
100/// (Affine/Unroll/Scalarize): those passes don't exist on targets without the
101/// feature, so tier-invariance is asserted per-platform, not across platforms.
102#[inline]
103pub fn admits(cfg: &OptimizationConfig, tier: Tier, opt: Opt) -> bool {
104    matches!(tier.budget(), Some(b) if opt.cost() <= b) && cfg.is_on(opt)
105}
106
107/// The number of [`Opt`] variants — the width of a [`PinSet`]. Derived from the
108/// last variant so it tracks the enum automatically (guarded by a test against
109/// `REGISTRY.len()`).
110pub const OPT_COUNT: usize = Opt::Supercompile as usize + 1;
111
112/// WHEN each allowed optimization runs, as opposed to WHICH are allowed
113/// ([`OptimizationConfig`]). The sibling policy of HOTSWAP §8: the bitset says what
114/// is permitted; this says at what hotness tier each permitted opt is paid for.
115/// Kept separate from `OptimizationConfig` (which is serialized / merged per-function
116/// / normalized) so tier state never pollutes the "which opts" bitset.
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum TierMode {
119    /// Units climb T0→T3 by hotness — the default.
120    Tiered,
121    /// Everything optimized upfront at T3 — today's behavior, the A/B baseline.
122    Eager,
123    /// Never optimize — every unit pinned at T0.
124    Baseline,
125}
126
127/// Call/back-edge counts at which a unit enters each tier (HOTSWAP §5). Defaults:
128/// don't optimize code that runs once (T1 at 8); amortize whole-function analyses
129/// only once recurring (T2 at 32); pay the cloning passes only on genuinely hot
130/// kernels (T3 at 100, aligned with the existing native-tier threshold).
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct TierThresholds {
133    pub t1: u32,
134    pub t2: u32,
135    pub t3: u32,
136}
137
138impl Default for TierThresholds {
139    fn default() -> Self {
140        Self { t1: 8, t2: 32, t3: 100 }
141    }
142}
143
144impl TierThresholds {
145    /// The tier a unit with `count` accumulated calls/back-edges has earned.
146    #[inline]
147    pub fn tier_for(self, count: u32) -> Tier {
148        if count >= self.t3 {
149            Tier::T3
150        } else if count >= self.t2 {
151            Tier::T2
152        } else if count >= self.t1 {
153            Tier::T1
154        } else {
155            Tier::T0
156        }
157    }
158
159    /// `LOGOS_TIER_THRESHOLDS="8,32,100"`, overridable per-rung by
160    /// `LOGOS_TIER_T1`/`T2`/`T3`.
161    pub fn from_env() -> Self {
162        Self::from_spec(
163            std::env::var("LOGOS_TIER_THRESHOLDS").ok().as_deref(),
164            std::env::var("LOGOS_TIER_T1").ok().as_deref(),
165            std::env::var("LOGOS_TIER_T2").ok().as_deref(),
166            std::env::var("LOGOS_TIER_T3").ok().as_deref(),
167        )
168    }
169
170    /// The pure, testable core of `from_env`.
171    pub fn from_spec(
172        combined: Option<&str>,
173        t1: Option<&str>,
174        t2: Option<&str>,
175        t3: Option<&str>,
176    ) -> Self {
177        let mut th = Self::default();
178        if let Some(c) = combined {
179            let parts: Vec<u32> = c.split(',').filter_map(|s| s.trim().parse().ok()).collect();
180            if parts.len() == 3 {
181                th = Self { t1: parts[0], t2: parts[1], t3: parts[2] };
182            }
183        }
184        if let Some(v) = t1.and_then(|s| s.trim().parse().ok()) {
185            th.t1 = v;
186        }
187        if let Some(v) = t2.and_then(|s| s.trim().parse().ok()) {
188            th.t2 = v;
189        }
190        if let Some(v) = t3.and_then(|s| s.trim().parse().ok()) {
191            th.t3 = v;
192        }
193        th
194    }
195}
196
197/// A per-opt tier override (HOTSWAP §8 `## Tier <kw> <eager|t1|t2|t3|never>`).
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub enum Pin {
200    /// Allowed by config but pinned off the tiering ladder — never run by tiering
201    /// (distinct from `## No <opt>`, which forbids it and cascades through normalize).
202    Never,
203    /// Run as soon as the unit is optimized at all (any tier ≥ T1), ignoring cost.
204    Eager,
205    /// Run once this tier is reached.
206    At(Tier),
207}
208
209/// Per-opt tier pins, indexed by [`Opt`] discriminant. `None` ⇒ use the cost-derived
210/// tier. `Copy` so [`HotswapConfig`] stays cheap to thread through the run path.
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub struct PinSet {
213    pins: [Option<Pin>; OPT_COUNT],
214}
215
216impl Default for PinSet {
217    fn default() -> Self {
218        Self::none()
219    }
220}
221
222impl PinSet {
223    /// No pins — every opt uses its cost-derived tier.
224    pub const fn none() -> Self {
225        Self { pins: [None; OPT_COUNT] }
226    }
227
228    #[inline]
229    pub fn get(&self, opt: Opt) -> Option<Pin> {
230        self.pins[opt as usize]
231    }
232
233    #[inline]
234    pub fn set(&mut self, opt: Opt, pin: Pin) {
235        self.pins[opt as usize] = Some(pin);
236    }
237
238    /// Copy every pin set in `other` over this set — `other` wins per-opt. Used to
239    /// overlay in-source `## Tier` decorators onto the ambient env pins (the
240    /// decorator, being explicit, takes precedence).
241    pub fn overlay(&mut self, other: &PinSet) {
242        for (slot, pin) in self.pins.iter_mut().zip(other.pins.iter()) {
243            if let Some(p) = pin {
244                *slot = Some(*p);
245            }
246        }
247    }
248}
249
250/// "*When* does each allowed opt run" — the sibling to [`OptimizationConfig`]
251/// (HOTSWAP §8). `mode` picks the escalation policy, `thresholds` the rungs,
252/// `force_tier` pins a fixed tier for deterministic tests, and `pins` overrides
253/// individual opts.
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub struct HotswapConfig {
256    pub mode: TierMode,
257    pub thresholds: TierThresholds,
258    /// Forces a fixed tier regardless of hotness — the determinism lever for
259    /// differential tests (`LOGOS_FORCE_TIER`). `None` ⇒ derive from mode + count.
260    pub force_tier: Option<Tier>,
261    pub pins: PinSet,
262}
263
264impl Default for HotswapConfig {
265    fn default() -> Self {
266        // Eager by default — the §12.2 ratchet (see `from_spec`). Tiered is the locked
267        // destination, reached once it is proven not to regress.
268        Self {
269            mode: TierMode::Eager,
270            thresholds: TierThresholds::default(),
271            force_tier: None,
272            pins: PinSet::none(),
273        }
274    }
275}
276
277impl HotswapConfig {
278    /// The tier a unit with `count` accumulated calls/back-edges should run at,
279    /// under this config's mode (and any `force_tier` override).
280    #[inline]
281    pub fn effective_tier(&self, count: u32) -> Tier {
282        if let Some(t) = self.force_tier {
283            return t;
284        }
285        match self.mode {
286            TierMode::Baseline => Tier::T0,
287            TierMode::Eager => Tier::T3,
288            TierMode::Tiered => self.thresholds.tier_for(count),
289        }
290    }
291
292    /// This config's pin for `opt`, if any.
293    #[inline]
294    pub fn pin(&self, opt: Opt) -> Option<Pin> {
295        self.pins.get(opt)
296    }
297
298    /// The tier at which the run path optimizes UPFRONT, before the program executes.
299    /// `Eager` pays the full optimizer (T3, today's behavior); `Tiered`/`Baseline`
300    /// start at the baseline (T0) and rely on the native tier — and, later, per-function
301    /// re-optimization — to escalate hot code during the run, reclaiming the upfront
302    /// optimizer cost (HOTSWAP §12.1). `force_tier` overrides everything (test
303    /// determinism). Distinct from `effective_tier`, which maps a per-unit hotness
304    /// COUNT to a tier; this maps the program-level MODE to the upfront tier.
305    #[inline]
306    pub fn run_tier(&self) -> Tier {
307        if let Some(t) = self.force_tier {
308            return t;
309        }
310        match self.mode {
311            TierMode::Eager => Tier::T3,
312            TierMode::Tiered | TierMode::Baseline => Tier::T0,
313        }
314    }
315
316    /// Build from the environment — the single external entry point (HOTSWAP §8).
317    /// `LOGOS_HOTSWAP=off` forces Eager; `LOGOS_TIER_PROFILE` picks the preset;
318    /// `LOGOS_FORCE_TIER` pins a fixed tier; `LOGOS_TIER_PIN` sets per-opt pins.
319    pub fn from_env() -> Self {
320        Self::from_spec(
321            std::env::var("LOGOS_HOTSWAP").ok().as_deref(),
322            std::env::var("LOGOS_TIER_PROFILE").ok().as_deref(),
323            std::env::var("LOGOS_FORCE_TIER").ok().as_deref(),
324            TierThresholds::from_env(),
325            std::env::var("LOGOS_TIER_PIN").ok().as_deref(),
326        )
327    }
328
329    /// The pure, testable core of `from_env`.
330    pub fn from_spec(
331        hotswap: Option<&str>,
332        profile: Option<&str>,
333        force_tier: Option<&str>,
334        thresholds: TierThresholds,
335        pin_spec: Option<&str>,
336    ) -> Self {
337        // The headline escape hatch: kill tiering, optimize everything upfront.
338        // DEFAULT = Eager (today's behavior). The locked product default is Tiered,
339        // but the §12.2 RATCHET keeps Eager until the tiered path (P9/P10 per-function
340        // Axis-1) is proven >= Eager on the benchmark A/B — so the served number can
341        // only improve. Tiered/Baseline are opt-in via LOGOS_TIER_PROFILE until then.
342        let mode = if hotswap == Some("off") {
343            TierMode::Eager
344        } else {
345            match profile {
346                Some("tiered") => TierMode::Tiered,
347                Some("baseline") => TierMode::Baseline,
348                _ => TierMode::Eager,
349            }
350        };
351        let force_tier = force_tier.and_then(parse_tier);
352        let mut pins = PinSet::none();
353        if let Some(spec) = pin_spec {
354            apply_pin_spec(&mut pins, spec);
355        }
356        HotswapConfig { mode, thresholds, force_tier, pins }
357    }
358}
359
360/// Parse a tier token: `0|1|2|3`, `t0..t3`, or `baseline|warm|hot|veryhot`
361/// (case-insensitive). `None` for anything else.
362fn parse_tier(s: &str) -> Option<Tier> {
363    match s.trim().to_ascii_lowercase().as_str() {
364        "0" | "t0" | "baseline" => Some(Tier::T0),
365        "1" | "t1" | "warm" => Some(Tier::T1),
366        "2" | "t2" | "hot" => Some(Tier::T2),
367        "3" | "t3" | "veryhot" | "very_hot" => Some(Tier::T3),
368        _ => None,
369    }
370}
371
372/// Parse one pin value: `eager`, `never`, or a tier token (see [`parse_tier`]). The
373/// surface the `## Tier <opt> <value>` parser and `LOGOS_TIER_PIN` both resolve.
374pub fn pin_from_str(s: &str) -> Option<Pin> {
375    match s.trim().to_ascii_lowercase().as_str() {
376        "eager" => Some(Pin::Eager),
377        "never" => Some(Pin::Never),
378        other => parse_tier(other).map(Pin::At),
379    }
380}
381
382/// Apply a pin spec — `"specialize:eager,fuse:t1,unfold:never"` — to `pins`. The same
383/// keyword list as `LOGOS_OPT_OFF`; unknown keywords / values are skipped.
384fn apply_pin_spec(pins: &mut PinSet, spec: &str) {
385    for tok in spec.split([',', ';']) {
386        let tok = tok.trim();
387        if tok.is_empty() {
388            continue;
389        }
390        if let Some((kw, val)) = tok.split_once(':') {
391            if let (Some(opt), Some(pin)) = (by_keyword(kw.trim()), pin_from_str(val)) {
392                pins.set(opt, pin);
393            }
394        }
395    }
396}
397
398/// [`admits`] with per-opt pins applied: a pin overrides the cost-derived tier
399/// (HOTSWAP §8). A disabled opt (`!cfg.is_on`) never runs regardless of pin, so
400/// `## No <opt>` always wins over `## Tier <opt> eager`.
401#[inline]
402pub fn admits_pinned(cfg: &OptimizationConfig, hs: &HotswapConfig, tier: Tier, opt: Opt) -> bool {
403    if !cfg.is_on(opt) {
404        return false;
405    }
406    match hs.pin(opt) {
407        Some(Pin::Never) => false,
408        Some(Pin::Eager) => true,
409        Some(Pin::At(t)) => tier >= t,
410        None => matches!(tier.budget(), Some(b) if opt.cost() <= b),
411    }
412}
413
414/// Where a `## No <X>` decorator for this optimization may legitimately appear.
415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub enum Scope {
417    /// Only meaningful program-wide (whole-program analyses).
418    ProgramOnly,
419    /// Meaningful both program-wide and on an individual function.
420    Both,
421}
422
423/// Every optimization the compiler can toggle. The discriminant is the bit
424/// index used by [`OptimizationConfig`], so the order here is stable and must
425/// not be reordered casually (it also defines conflict-resolution precedence:
426/// an earlier variant wins a conflict against a later one).
427#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
428#[repr(u8)]
429pub enum Opt {
430    /// Automatic memoization of pure functions.
431    Memo = 0,
432    /// Tail-call optimization (self-tail-recursion → loop).
433    Tco = 1,
434    /// Peephole rewrites (swap, vec-fill, for-range, …).
435    Peephole = 2,
436    /// Read-only / mutable borrow inference (`&[T]` / `&mut [T]` params).
437    Borrow = 3,
438    /// Partial evaluation / polyvariant specialization.
439    Specialize = 4,
440    /// Compile-time function evaluation (CTFE).
441    Comptime = 5,
442    /// Inlining of tiny and leaf helper functions.
443    Inline = 6,
444    /// Bounded unrolling of recursive functions.
445    Unfold = 7,
446    /// Defunctionalization (closures → first-order functions).
447    Defunctionalize = 8,
448    /// Loop-invariant code motion (LICM).
449    LoopHoist = 9,
450    /// Loop unrolling of constant-trip loops.
451    Unroll = 10,
452    /// Guard-based loop index-set splitting (for vectorization).
453    LoopSplit = 11,
454    /// Accumulator-loop → closed-form recognition.
455    ClosedForm = 12,
456    /// Deforestation (producer/consumer stream fusion).
457    Fuse = 13,
458    /// Loop-carried common-subexpression hoisting.
459    LoopCse = 14,
460    /// Global value numbering / common-subexpression elimination.
461    Cse = 15,
462    /// Dead-code elimination.
463    DeadCode = 16,
464    /// Fixed-size sequence scalarization (SROA).
465    Scalarize = 17,
466    /// Affine offset-array deletion → closed-form indexing.
467    Affine = 18,
468    /// Array-of-structs interleaving of co-indexed sequences.
469    Interleave = 19,
470    /// De-`Rc`: unique-owned sequences become plain `Vec`.
471    Unbox = 20,
472    /// Hoisting borrow live-ranges out of loop nests.
473    HoistBorrows = 21,
474    /// `i64`→`i32` sequence element narrowing (codegen).
475    Narrow = 22,
476    /// `i64`→`i32` sequence element narrowing (VM).
477    NarrowVm = 23,
478    /// `i64`→`i32` map-key narrowing.
479    NarrowMap = 24,
480    /// Sparse map → dense direct-addressed array.
481    DenseMap = 25,
482    /// Element-type narrowing via abstract interpretation.
483    ElemType = 26,
484    /// Constant-divisor magic-reciprocal division.
485    FastDiv = 27,
486    /// Float induction-variable strength reduction.
487    FloatStrength = 28,
488    /// Abstract interpretation / oracle-fact production (interval analysis).
489    Oracle = 29,
490    /// Oracle-derived precise bounds-check guards.
491    OracleHints = 30,
492    /// Oracle-proven unchecked indexing (`get_unchecked`).
493    Unchecked = 31,
494    /// SIMD-vectorized search kernels.
495    Simd = 32,
496    /// Cascade folding of sequential comparisons.
497    Cascade = 33,
498    /// Indexed string-search codegen.
499    IndexString = 34,
500    /// Capacity-scaling buffer-fill simplification.
501    CapScale = 35,
502    /// Reflection symmetry breaking (bitmask search).
503    Symmetry = 36,
504    /// Popcount base-case collapse for bitmask search.
505    Popcount = 37,
506    /// Equality-saturation rewriting (e-graph).
507    Saturate = 38,
508    /// Supercompilation (heavy residual specialization, AOT only).
509    Supercompile = 39,
510}
511
512impl Opt {
513    /// The bit index this optimization occupies in an [`OptimizationConfig`].
514    #[inline]
515    pub const fn bit(self) -> u64 {
516        1u64 << (self as u8)
517    }
518
519    /// This optimization's registry row.
520    pub fn meta(self) -> &'static OptMeta {
521        // Discriminants are 0..REGISTRY.len() in order, so index directly.
522        &REGISTRY[self as usize]
523    }
524
525    /// How expensive this optimization is to run — the lever the tiered optimizer
526    /// uses to decide which hotness tier pays for it.
527    #[inline]
528    pub fn cost(self) -> OptCost {
529        self.meta().cost
530    }
531}
532
533/// One row of the optimization registry — the complete static description of a
534/// single optimization. Adding an optimization means adding one [`Opt`] variant
535/// and one row here; nothing else hardcodes the list.
536#[derive(Debug, Clone, Copy)]
537pub struct OptMeta {
538    /// The optimization this row describes.
539    pub opt: Opt,
540    /// The `## No <Keyword>` decorator word (lowercase, single token).
541    pub keyword: &'static str,
542    /// Human-readable label for UIs.
543    pub label: &'static str,
544    /// Which group the UI files this under.
545    pub group: &'static str,
546    /// Whether this optimization is on in the default (all-on, speed) config.
547    pub default_on: bool,
548    /// Execution paths it affects (bitmask of [`path`] constants).
549    pub paths: u8,
550    /// Whether enabling it can emit `unsafe` Rust (disabled by the Safety profile).
551    pub emits_unsafe: bool,
552    /// Its memory/speed trade-off classification.
553    pub mem_class: MemClass,
554    /// How expensive the pass is to run — the hotness tier at which the tiered
555    /// optimizer starts paying for it (HOTSWAP §3). For every row,
556    /// `cost(requires) ≤ cost`, so a tier that admits this opt admits its deps.
557    pub cost: OptCost,
558    /// Optimizations that must be on for this one to apply; if any is off,
559    /// `normalize` turns this one off too.
560    pub requires: &'static [Opt],
561    /// Optimizations mutually exclusive with this one — GLOBAL exclusion (both on
562    /// → `normalize` disables the later-declared). Distinct from `preempts`.
563    pub conflicts: &'static [Opt],
564    /// Optimizations this one takes PRECEDENCE over, per instance: when both are
565    /// enabled, this one is tried/applied first for a given function/loop/array/
566    /// map, and the listed ones act as the fallback for the instances it did not
567    /// claim. Both stay enabled (unlike `conflicts`); the listed optimization
568    /// fires only where this one declined. Disabling THIS one is what can let a
569    /// preempted optimization surface — the edge the menu-tree walks.
570    pub preempts: &'static [Opt],
571    /// Where its decorator may appear.
572    pub scope: Scope,
573}
574
575use path::{AOT, CODEGEN, JIT, RUN, VM};
576use MemClass::{Neutral, SavesMem, TradesMemForSpeed};
577use OptCost::{Cheap, Heavy, Medium};
578use Scope::{Both, ProgramOnly};
579
580/// The complete optimization registry: exactly one row per [`Opt`], in
581/// discriminant order. Earlier rows win conflicts against later rows.
582pub static REGISTRY: &[OptMeta] = &[
583    OptMeta { opt: Opt::Memo, keyword: "memo", label: "Memoization", group: "Inlining & calls", default_on: true, paths: AOT | CODEGEN, emits_unsafe: false, mem_class: TradesMemForSpeed, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: Both },
584    OptMeta { opt: Opt::Tco, keyword: "tco", label: "Tail-call optimization", group: "Inlining & calls", default_on: true, paths: AOT | RUN | CODEGEN, emits_unsafe: false, mem_class: Neutral, cost: Cheap, requires: &[], conflicts: &[], preempts: &[Opt::Memo], scope: Both },
585    OptMeta { opt: Opt::Peephole, keyword: "peephole", label: "Peephole rewrites", group: "Peephole", default_on: true, paths: AOT | CODEGEN, emits_unsafe: true, mem_class: Neutral, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: Both },
586    // Borrow preempts Tco: a `&mut [T]` in-place recursion (quicksort's
587    // consume-alias shape) keeps its plain recursive calls — the pair-TCE
588    // rewrite would reassign the borrowed param.
589    OptMeta { opt: Opt::Borrow, keyword: "borrow", label: "Borrow inference", group: "Arrays & memory", default_on: true, paths: AOT | CODEGEN, emits_unsafe: false, mem_class: SavesMem, cost: Cheap, requires: &[], conflicts: &[], preempts: &[Opt::Tco], scope: Both },
590    OptMeta { opt: Opt::Specialize, keyword: "specialize", label: "Partial evaluation", group: "Inlining & calls", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: TradesMemForSpeed, cost: Medium, requires: &[], conflicts: &[], preempts: &[], scope: Both },
591    OptMeta { opt: Opt::Comptime, keyword: "comptime", label: "Compile-time evaluation (CTFE)", group: "Inlining & calls", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: Neutral, cost: Medium, requires: &[], conflicts: &[], preempts: &[Opt::Supercompile], scope: Both },
592    OptMeta { opt: Opt::Inline, keyword: "inline", label: "Function inlining", group: "Inlining & calls", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: TradesMemForSpeed, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: Both },
593    OptMeta { opt: Opt::Unfold, keyword: "unfold", label: "Recursion unrolling", group: "Inlining & calls", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: TradesMemForSpeed, cost: Heavy, requires: &[], conflicts: &[], preempts: &[], scope: Both },
594    OptMeta { opt: Opt::Defunctionalize, keyword: "defunctionalize", label: "Defunctionalization", group: "Inlining & calls", default_on: true, paths: AOT, emits_unsafe: false, mem_class: Neutral, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: Both },
595    OptMeta { opt: Opt::LoopHoist, keyword: "loophoist", label: "Loop-invariant code motion", group: "Loops", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: Neutral, cost: Medium, requires: &[], conflicts: &[], preempts: &[], scope: Both },
596    OptMeta { opt: Opt::Unroll, keyword: "unroll", label: "Loop unrolling", group: "Loops", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: TradesMemForSpeed, cost: Heavy, requires: &[], conflicts: &[], preempts: &[Opt::Interleave], scope: Both },
597    OptMeta { opt: Opt::LoopSplit, keyword: "loopsplit", label: "Loop index-set splitting", group: "Loops", default_on: true, paths: AOT, emits_unsafe: false, mem_class: Neutral, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: Both },
598    OptMeta { opt: Opt::ClosedForm, keyword: "closedform", label: "Closed-form loop recognition", group: "Loops", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: Neutral, cost: Medium, requires: &[], conflicts: &[], preempts: &[Opt::Peephole], scope: Both },
599    OptMeta { opt: Opt::Fuse, keyword: "fuse", label: "Deforestation (stream fusion)", group: "Loops", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: SavesMem, cost: Medium, requires: &[], conflicts: &[], preempts: &[], scope: Both },
600    OptMeta { opt: Opt::LoopCse, keyword: "loopcse", label: "Loop-carried CSE", group: "Loops", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: Neutral, cost: Medium, requires: &[], conflicts: &[], preempts: &[], scope: Both },
601    OptMeta { opt: Opt::Cse, keyword: "cse", label: "Common-subexpression elimination (GVN)", group: "Redundancy", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: Neutral, cost: Medium, requires: &[], conflicts: &[], preempts: &[], scope: Both },
602    OptMeta { opt: Opt::DeadCode, keyword: "deadcode", label: "Dead-code elimination", group: "Redundancy", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: SavesMem, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: Both },
603    OptMeta { opt: Opt::Scalarize, keyword: "scalarize", label: "Array scalarization (SROA)", group: "Arrays & memory", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: TradesMemForSpeed, cost: Heavy, requires: &[Opt::Cse], conflicts: &[], preempts: &[Opt::HoistBorrows, Opt::Interleave, Opt::Unbox], scope: Both },
604    OptMeta { opt: Opt::Affine, keyword: "affine", label: "Affine array → closed form", group: "Arrays & memory", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: SavesMem, cost: Medium, requires: &[Opt::Unbox], conflicts: &[], preempts: &[], scope: Both },
605    // AoS interleaving is mutually exclusive with Unroll/Scalarize only at RUNTIME
606    // (the codegen regime gate skips it when a co-indexed array has any constant
607    // index access — i.e. once it has been unrolled/scalarized), NOT at config
608    // level. All three stay enabled by default; the gate arbitrates per program.
609    OptMeta { opt: Opt::Interleave, keyword: "interleave", label: "Array-of-structs interleaving", group: "Arrays & memory", default_on: true, paths: AOT | CODEGEN, emits_unsafe: false, mem_class: Neutral, cost: Heavy, requires: &[Opt::Scalarize], conflicts: &[], preempts: &[], scope: ProgramOnly },
610    OptMeta { opt: Opt::Unbox, keyword: "unbox", label: "De-Rc (Vec instead of Rc<RefCell>)", group: "Arrays & memory", default_on: true, paths: AOT | CODEGEN, emits_unsafe: false, mem_class: SavesMem, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: ProgramOnly },
611    OptMeta { opt: Opt::HoistBorrows, keyword: "hoistborrows", label: "Borrow hoisting", group: "Arrays & memory", default_on: true, paths: AOT | CODEGEN, emits_unsafe: false, mem_class: Neutral, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: ProgramOnly },
612    OptMeta { opt: Opt::Narrow, keyword: "narrow", label: "i32 sequence narrowing (codegen)", group: "Number representation", default_on: true, paths: AOT | CODEGEN, emits_unsafe: false, mem_class: SavesMem, cost: Cheap, requires: &[Opt::Unbox], conflicts: &[], preempts: &[], scope: ProgramOnly },
613    OptMeta { opt: Opt::NarrowVm, keyword: "narrowvm", label: "i32 sequence narrowing (VM)", group: "Number representation", default_on: true, paths: VM | RUN, emits_unsafe: false, mem_class: SavesMem, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: ProgramOnly },
614    OptMeta { opt: Opt::NarrowMap, keyword: "narrowmap", label: "i32 map-key narrowing", group: "Number representation", default_on: true, paths: AOT | CODEGEN, emits_unsafe: false, mem_class: SavesMem, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: ProgramOnly },
615    OptMeta { opt: Opt::DenseMap, keyword: "densemap", label: "Dense direct-addressed map", group: "Number representation", default_on: true, paths: AOT | CODEGEN, emits_unsafe: false, mem_class: TradesMemForSpeed, cost: Cheap, requires: &[], conflicts: &[], preempts: &[Opt::NarrowMap], scope: ProgramOnly },
616    OptMeta { opt: Opt::ElemType, keyword: "elemtype", label: "Element-type narrowing", group: "Number representation", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: SavesMem, cost: Medium, requires: &[Opt::Oracle], conflicts: &[], preempts: &[], scope: Both },
617    OptMeta { opt: Opt::FastDiv, keyword: "fastdiv", label: "Magic-reciprocal division", group: "Number representation", default_on: true, paths: AOT | VM | CODEGEN, emits_unsafe: false, mem_class: Neutral, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: Both },
618    OptMeta { opt: Opt::FloatStrength, keyword: "floatstrength", label: "Float induction strength reduction", group: "Number representation", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: Neutral, cost: Medium, requires: &[], conflicts: &[], preempts: &[], scope: Both },
619    OptMeta { opt: Opt::Oracle, keyword: "oracle", label: "Abstract interpretation (oracle facts)", group: "Bounds & checks", default_on: true, paths: AOT | RUN, emits_unsafe: false, mem_class: Neutral, cost: Medium, requires: &[], conflicts: &[], preempts: &[], scope: Both },
620    OptMeta { opt: Opt::OracleHints, keyword: "oraclehints", label: "Oracle bounds-check guards", group: "Bounds & checks", default_on: true, paths: AOT | CODEGEN, emits_unsafe: false, mem_class: Neutral, cost: Medium, requires: &[Opt::Oracle], conflicts: &[], preempts: &[], scope: Both },
621    OptMeta { opt: Opt::Unchecked, keyword: "unchecked", label: "Oracle-proven unchecked indexing", group: "Bounds & checks", default_on: true, paths: AOT | JIT | CODEGEN, emits_unsafe: true, mem_class: Neutral, cost: Medium, requires: &[Opt::Oracle], conflicts: &[], preempts: &[], scope: Both },
622    OptMeta { opt: Opt::Simd, keyword: "simd", label: "SIMD search kernels", group: "Strings & SIMD", default_on: true, paths: AOT | CODEGEN, emits_unsafe: true, mem_class: Neutral, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: ProgramOnly },
623    OptMeta { opt: Opt::Cascade, keyword: "cascade", label: "Cascade folding", group: "Strings & SIMD", default_on: true, paths: AOT | CODEGEN, emits_unsafe: false, mem_class: Neutral, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: ProgramOnly },
624    OptMeta { opt: Opt::IndexString, keyword: "indexstring", label: "Indexed string search", group: "Strings & SIMD", default_on: true, paths: AOT | CODEGEN, emits_unsafe: false, mem_class: Neutral, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: ProgramOnly },
625    OptMeta { opt: Opt::CapScale, keyword: "capscale", label: "Capacity-scaling buffer fill", group: "Strings & SIMD", default_on: true, paths: AOT | CODEGEN, emits_unsafe: false, mem_class: Neutral, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: ProgramOnly },
626    OptMeta { opt: Opt::Symmetry, keyword: "symmetry", label: "Symmetry breaking", group: "Search-space", default_on: true, paths: AOT, emits_unsafe: false, mem_class: Neutral, cost: Heavy, requires: &[Opt::Specialize], conflicts: &[], preempts: &[], scope: Both },
627    OptMeta { opt: Opt::Popcount, keyword: "popcount", label: "Popcount leaf collapse", group: "Search-space", default_on: true, paths: AOT, emits_unsafe: false, mem_class: Neutral, cost: Cheap, requires: &[], conflicts: &[], preempts: &[], scope: Both },
628    OptMeta { opt: Opt::Saturate, keyword: "saturate", label: "Equality saturation (e-graph)", group: "Search-space", default_on: true, paths: AOT, emits_unsafe: false, mem_class: Neutral, cost: Heavy, requires: &[], conflicts: &[], preempts: &[], scope: Both },
629    OptMeta { opt: Opt::Supercompile, keyword: "supercompile", label: "Supercompilation", group: "Search-space", default_on: true, paths: AOT, emits_unsafe: false, mem_class: TradesMemForSpeed, cost: Heavy, requires: &[], conflicts: &[], preempts: &[], scope: Both },
630];
631
632/// Look up an optimization by its decorator keyword (case-insensitive).
633pub fn by_keyword(word: &str) -> Option<Opt> {
634    let w = word.to_ascii_lowercase();
635    REGISTRY.iter().find(|m| m.keyword == w).map(|m| m.opt)
636}
637
638/// Produce a copy of `src` with a file-level `## No <keyword>` decorator inserted
639/// (just before `## Main`, where the parser folds it into the program-wide config)
640/// for each disabled-optimization keyword. This is what the benchmarks UI shows on
641/// the Logos source when a toggle is flipped off; compiling the result applies the
642/// same config the toggle represents.
643pub fn decorate_source(src: &str, disabled_keywords: &[&str]) -> String {
644    if disabled_keywords.is_empty() {
645        return src.to_string();
646    }
647    let decorators: String = disabled_keywords
648        .iter()
649        .map(|kw| format!("## No {kw}\n"))
650        .collect();
651    match src.find("## Main") {
652        Some(idx) => format!("{}{}{}", &src[..idx], decorators, &src[idx..]),
653        None => format!("{src}{decorators}"),
654    }
655}
656
657/// Produce a copy of `src` with a file-level `## Tier <keyword> <value>` decorator
658/// inserted (just before `## Main`, where the parser collects program-level tier
659/// pins) for each `(keyword, value)` pair. The tiering analog of [`decorate_source`]:
660/// the benchmarks UI injects these to pin an optimization to a tier exactly as it
661/// injects `## No <X>` to disable one. `value` is one of `eager|t1|t2|t3|never`.
662pub fn decorate_tiers(src: &str, pins: &[(&str, &str)]) -> String {
663    if pins.is_empty() {
664        return src.to_string();
665    }
666    let decorators: String = pins
667        .iter()
668        .map(|(kw, val)| format!("## Tier {kw} {val}\n"))
669        .collect();
670    match src.find("## Main") {
671        Some(idx) => format!("{}{}{}", &src[..idx], decorators, &src[idx..]),
672        None => format!("{src}{decorators}"),
673    }
674}
675
676/// Why `normalize` flipped an optimization off.
677#[derive(Debug, Clone, Copy, PartialEq, Eq)]
678pub enum Reason {
679    /// A required optimization was off.
680    DependencyOff(Opt),
681    /// A conflicting optimization (declared earlier) was on.
682    ConflictWith(Opt),
683}
684
685/// What `normalize` changed, so a UI can explain auto-disabled toggles.
686#[derive(Debug, Clone, Default, PartialEq, Eq)]
687pub struct NormalizeReport {
688    /// Optimizations turned off by normalization, with the reason.
689    pub auto_disabled: Vec<(Opt, Reason)>,
690}
691
692impl NormalizeReport {
693    /// Whether normalization changed anything.
694    pub fn is_empty(&self) -> bool {
695        self.auto_disabled.is_empty()
696    }
697}
698
699/// A live choice of which optimizations are enabled — one bit per [`Opt`].
700///
701/// The default is every optimization on (the speed-tuned baseline). Clearing
702/// all bits yields plain, boring Rust.
703#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
704pub struct OptimizationConfig {
705    enabled: u64,
706}
707
708impl Default for OptimizationConfig {
709    fn default() -> Self {
710        Self::all_on()
711    }
712}
713
714impl OptimizationConfig {
715    /// Every default-on optimization enabled (the speed baseline).
716    pub fn all_on() -> Self {
717        let mut enabled = 0u64;
718        for m in REGISTRY {
719            if m.default_on {
720                enabled |= m.opt.bit();
721            }
722        }
723        Self { enabled }
724    }
725
726    /// No optimizations enabled — the "boring Rust" config.
727    pub const fn all_off() -> Self {
728        Self { enabled: 0 }
729    }
730
731    /// Whether a given optimization is on.
732    #[inline]
733    pub const fn is_on(&self, opt: Opt) -> bool {
734        self.enabled & opt.bit() != 0
735    }
736
737    /// Turn an optimization on or off.
738    #[inline]
739    pub fn set(&mut self, opt: Opt, on: bool) {
740        if on {
741            self.enabled |= opt.bit();
742        } else {
743            self.enabled &= !opt.bit();
744        }
745    }
746
747    /// Turn an optimization off (builder style).
748    pub fn disable(mut self, opt: Opt) -> Self {
749        self.set(opt, false);
750        self
751    }
752
753    /// Turn an optimization on (builder style).
754    pub fn enable(mut self, opt: Opt) -> Self {
755        self.set(opt, true);
756        self
757    }
758
759    /// Turn `opt` on together with the transitive closure of what it `requires`.
760    ///
761    /// The complement of [`normalize`](Self::normalize)'s dependency-closure
762    /// (which turns *dependents* off when a requirement goes off): enabling a
763    /// leaf must pull its whole requires-chain on, or `normalize` would just turn
764    /// the leaf back off. This is the on-direction of the UI's toggle-linking.
765    pub fn enable_with_requires(&mut self, opt: Opt) {
766        let mut visited = 0u64;
767        let mut stack = vec![opt];
768        while let Some(o) = stack.pop() {
769            if visited & o.bit() != 0 {
770                continue;
771            }
772            visited |= o.bit();
773            self.set(o, true);
774            for &req in o.meta().requires {
775                stack.push(req);
776            }
777        }
778    }
779
780    /// Whether every optimization is off.
781    pub const fn is_all_off(&self) -> bool {
782        self.enabled == 0
783    }
784
785    /// The raw enabled bitmask (bit layout = [`Opt::bit`]). Lets callers relate
786    /// an [`OptimizationConfig`] to a [`FiredOptimizations`] set bit-for-bit.
787    pub const fn bits(&self) -> u64 {
788        self.enabled
789    }
790
791    /// Combine a program-level config with a function's per-function overrides:
792    /// an optimization is on for the function only if it is on in both.
793    pub const fn merged(&self, func: &OptimizationConfig) -> OptimizationConfig {
794        OptimizationConfig { enabled: self.enabled & func.enabled }
795    }
796
797    /// The keywords of every optimization currently off (registry order).
798    pub fn disabled_keywords(&self) -> impl Iterator<Item = &'static str> + '_ {
799        REGISTRY
800            .iter()
801            .filter(move |m| !self.is_on(m.opt))
802            .map(|m| m.keyword)
803    }
804
805    /// Resolve conflicts and dependencies, returning what was auto-disabled.
806    ///
807    /// Conflicts: when two mutually exclusive optimizations are both on, the
808    /// one declared earlier in [`REGISTRY`] wins and the later is disabled.
809    /// Dependencies: any optimization whose `requires` set is not fully on is
810    /// disabled (transitively, to a fixed point).
811    pub fn normalize(&mut self) -> NormalizeReport {
812        let mut report = NormalizeReport::default();
813        if self.is_all_off() {
814            return report;
815        }
816
817        // Conflict resolution: earlier-declared optimization wins.
818        for m in REGISTRY {
819            if !self.is_on(m.opt) {
820                continue;
821            }
822            for &other in m.conflicts {
823                if (other as u8) < (m.opt as u8) && self.is_on(other) {
824                    self.set(m.opt, false);
825                    report.auto_disabled.push((m.opt, Reason::ConflictWith(other)));
826                    break;
827                }
828            }
829        }
830
831        // Dependency closure: disable anything whose requirement is off.
832        loop {
833            let mut changed = false;
834            for m in REGISTRY {
835                if !self.is_on(m.opt) {
836                    continue;
837                }
838                for &req in m.requires {
839                    if !self.is_on(req) {
840                        self.set(m.opt, false);
841                        report.auto_disabled.push((m.opt, Reason::DependencyOff(req)));
842                        changed = true;
843                        break;
844                    }
845                }
846            }
847            if !changed {
848                break;
849            }
850        }
851
852        report
853    }
854
855    /// Build a config from the environment, the single external entry point.
856    ///
857    /// - `LOGOS_OPT=off` (or `LOGOS_NO_OPTIMIZE=1`) disables everything.
858    /// - `LOGOS_OPT_PROFILE=speed|memory|safety` selects a base profile.
859    /// - `LOGOS_OPT_OFF="scalarize,unroll,…"` disables the listed keywords.
860    ///
861    /// The result is normalized.
862    pub fn from_env() -> Self {
863        let opt = std::env::var("LOGOS_OPT").ok();
864        let no_opt = std::env::var("LOGOS_NO_OPTIMIZE").ok();
865        let profile = std::env::var("LOGOS_OPT_PROFILE").ok();
866        let off = std::env::var("LOGOS_OPT_OFF").ok();
867        let master_off = opt.as_deref() == Some("off") || no_opt.as_deref() == Some("1");
868        Self::from_spec(master_off, profile.as_deref(), off.as_deref())
869    }
870
871    /// The pure, testable core of `from_env`. `master_off` disables everything;
872    /// `profile` picks the base preset (speed by default); `off_list` is a
873    /// comma/space/`;`-separated list of keywords to disable. The result is
874    /// normalized.
875    pub fn from_spec(master_off: bool, profile: Option<&str>, off_list: Option<&str>) -> Self {
876        if master_off {
877            return Self::all_off();
878        }
879
880        let mut cfg = match profile {
881            Some("memory") => Profile::Memory.config(),
882            Some("safety") => Profile::Safety.config(),
883            _ => Profile::Speed.config(),
884        };
885
886        if let Some(list) = off_list {
887            for tok in list.split([',', ' ', ';']) {
888                let tok = tok.trim();
889                if tok.is_empty() {
890                    continue;
891                }
892                if let Some(opt) = by_keyword(tok) {
893                    cfg.set(opt, false);
894                }
895            }
896        }
897
898        cfg.normalize();
899        cfg
900    }
901
902    /// Build a config from a keyword→enabled map (e.g. UI toggles), returning
903    /// the normalized config and the optimizations normalization forced off.
904    pub fn from_toggles(toggles: &BTreeMap<String, bool>) -> (Self, Vec<Opt>) {
905        let mut cfg = Self::all_on();
906        for m in REGISTRY {
907            if let Some(&on) = toggles.get(m.keyword) {
908                cfg.set(m.opt, on);
909            }
910        }
911        let before = cfg.enabled;
912        let report = cfg.normalize();
913        let _ = before;
914        let forced = report.auto_disabled.iter().map(|(opt, _)| *opt).collect();
915        (cfg, forced)
916    }
917}
918
919/// Which optimizations actually FIRED during a single compile — one bit per
920/// [`Opt`], the same bit layout as [`OptimizationConfig`]. An optimization is
921/// "fired" when it actually changed the program or emitted its optimized form
922/// for this compile, as distinct from merely being *enabled*. Populated only
923/// while a firing trace is active (see `logicaffeine_compile`'s traced compile
924/// entry points); the normal compile path never records into it.
925#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
926pub struct FiredOptimizations {
927    fired: u64,
928}
929
930impl FiredOptimizations {
931    /// An empty set — nothing fired.
932    pub const fn new() -> Self {
933        Self { fired: 0 }
934    }
935
936    /// Build from a raw bitmask (bit layout = [`Opt::bit`]).
937    pub const fn from_bits(fired: u64) -> Self {
938        Self { fired }
939    }
940
941    /// Record that `opt` fired.
942    #[inline]
943    pub fn mark(&mut self, opt: Opt) {
944        self.fired |= opt.bit();
945    }
946
947    /// Whether `opt` fired.
948    #[inline]
949    pub const fn fired(&self, opt: Opt) -> bool {
950        self.fired & opt.bit() != 0
951    }
952
953    /// Whether nothing fired.
954    pub const fn is_empty(&self) -> bool {
955        self.fired == 0
956    }
957
958    /// The raw fired bitmask.
959    pub const fn bits(&self) -> u64 {
960        self.fired
961    }
962
963    /// Fold another set's fired bits into this one.
964    pub fn merge(&mut self, other: &FiredOptimizations) {
965        self.fired |= other.fired;
966    }
967
968    /// The optimizations that fired, in registry order.
969    pub fn opts(&self) -> Vec<Opt> {
970        REGISTRY
971            .iter()
972            .filter(|m| self.fired(m.opt))
973            .map(|m| m.opt)
974            .collect()
975    }
976
977    /// The decorator keywords of the optimizations that fired, in registry order.
978    pub fn keywords(&self) -> Vec<&'static str> {
979        REGISTRY
980            .iter()
981            .filter(|m| self.fired(m.opt))
982            .map(|m| m.keyword)
983            .collect()
984    }
985}
986
987/// Why an optimization appears in a program's [`relationship_tree`].
988#[derive(Debug, Clone, Copy, PartialEq, Eq)]
989pub enum OptRole {
990    /// It fired for this program.
991    Fired,
992    /// A `requires`-parent pulled in so a fired/preempted descendant has its
993    /// dependency shown — it did not itself fire (e.g. `oracle` for a program
994    /// that fires `unchecked` but produces no oracle-folded constant).
995    Enabler,
996    /// It had a candidate site but a higher-precedence optimization claimed it
997    /// (a `preempts` loser) — it is enabled, and would fire if its winner were
998    /// turned off. The "skipped because they don't play nice together" node.
999    Preempted,
1000}
1001
1002/// One node of a program's optimization relationship tree: an optimization that
1003/// is *in play* for that program, with its nesting depth and its edges.
1004#[derive(Debug, Clone)]
1005pub struct OptNode {
1006    /// The optimization.
1007    pub opt: Opt,
1008    /// Nesting depth (0 = root) for tree indentation — under its `requires`-parent
1009    /// or its per-program dependency parent.
1010    pub depth: usize,
1011    /// Why it is in play.
1012    pub role: OptRole,
1013    /// Whether any in-play optimization depends on this one (drives the chevron).
1014    pub has_children: bool,
1015    /// What it requires (static, from the registry).
1016    pub requires: Vec<Opt>,
1017    /// What it takes precedence over (static, from the registry).
1018    pub preempts: Vec<Opt>,
1019    /// The winners that beat this optimization on THIS program (from the trace).
1020    pub preempted_by: Vec<Opt>,
1021    /// The optimizations this one only fired because of, on THIS program (the
1022    /// emergent dependencies the all-on evaluation revealed — distinct from the
1023    /// universal `requires`).
1024    pub depends_on: Vec<Opt>,
1025}
1026
1027/// The complete optimization chain for one program, derived deterministically
1028/// from a single all-optimizations-on evaluation plus the static registry graph —
1029/// no differential toggling in the hot path. `fired` is the set that fired;
1030/// `preempted` is the `(winner, loser)` BLOCKER pairs that occurred; `dependencies`
1031/// is the `(dependent, dep)` per-program DEPENDENCY pairs (one optimization only
1032/// fired because another was on) — all three come from the baked per-program graph
1033/// (`compile::optimization_graph`).
1034///
1035/// The in-play set is `fired ∪ {losers} ∪ {dependency endpoints} ∪ closure` over
1036/// the parent relation, where a node's parents are its static `requires` PLUS its
1037/// per-program dependencies: every opt that fired, every opt skipped because a
1038/// higher-precedence one claimed it, and the parents they hang under (so the tree
1039/// never orphans a child). Each node is placed by a depth-first walk along that
1040/// combined parent relation, parents before children. Closures over the fixed
1041/// 40-row registry — O(n²), and the same for identical input.
1042pub fn relationship_tree(
1043    fired: &[Opt],
1044    preempted: &[(Opt, Opt)],
1045    dependencies: &[(Opt, Opt)],
1046) -> Vec<OptNode> {
1047    let n = REGISTRY.len();
1048    let idx = |o: Opt| o as usize;
1049
1050    // A node's parents for nesting: its static `requires` PLUS the per-program
1051    // dependencies it only fired because of.
1052    let parents_of = |y: usize| -> Vec<usize> {
1053        let yopt = REGISTRY[y].opt;
1054        let mut ps: Vec<usize> = REGISTRY[y].requires.iter().map(|&r| idx(r)).collect();
1055        for &(dep, on) in dependencies {
1056            if dep == yopt {
1057                ps.push(idx(on));
1058            }
1059        }
1060        ps.sort_unstable();
1061        ps.dedup();
1062        ps
1063    };
1064
1065    // 1. The in-play set: seed with fired + blockers' losers + dependency
1066    //    endpoints, then pull in the transitive parent-closure.
1067    let mut want = vec![false; n];
1068    for &o in fired {
1069        want[idx(o)] = true;
1070    }
1071    for &(_, loser) in preempted {
1072        want[idx(loser)] = true;
1073    }
1074    for &(dep, on) in dependencies {
1075        want[idx(dep)] = true;
1076        want[idx(on)] = true;
1077    }
1078    loop {
1079        let mut changed = false;
1080        for i in 0..n {
1081            if want[i] {
1082                for p in parents_of(i) {
1083                    if !want[p] {
1084                        want[p] = true;
1085                        changed = true;
1086                    }
1087                }
1088            }
1089        }
1090        if !changed {
1091            break;
1092        }
1093    }
1094
1095    // 2. Role for each in-play opt: Fired wins over Preempted wins over Enabler
1096    //    (an opt that fired somewhere but was preempted on one instance is still
1097    //    Fired — the per-instance loss is shown as a live annotation, not a role).
1098    let fired_set: Vec<bool> = {
1099        let mut v = vec![false; n];
1100        for &o in fired {
1101            v[idx(o)] = true;
1102        }
1103        v
1104    };
1105    let loser_set: Vec<bool> = {
1106        let mut v = vec![false; n];
1107        for &(_, l) in preempted {
1108            v[idx(l)] = true;
1109        }
1110        v
1111    };
1112    let role_of = |i: usize| {
1113        if fired_set[i] {
1114            OptRole::Fired
1115        } else if loser_set[i] {
1116            OptRole::Preempted
1117        } else {
1118            OptRole::Enabler
1119        }
1120    };
1121
1122    // 3. Depth-first order along the combined parent relation: roots are in-play
1123    //    opts with no in-play parent; children are in-play opts that hang under the
1124    //    current one (require it, or depend on it for this program).
1125    let parent_in_want = |i: usize| parents_of(i).iter().any(|&p| want[p]);
1126    let mut order: Vec<(usize, usize)> = Vec::new();
1127    let mut visited = vec![false; n];
1128    let roots: Vec<usize> = (0..n).filter(|&i| want[i] && !parent_in_want(i)).collect();
1129    let mut stack: Vec<(usize, usize)> = roots.iter().rev().map(|&i| (i, 0usize)).collect();
1130    while let Some((i, depth)) = stack.pop() {
1131        if visited[i] {
1132            continue;
1133        }
1134        visited[i] = true;
1135        order.push((i, depth));
1136        let kids: Vec<usize> = (0..n)
1137            .filter(|&j| want[j] && !visited[j] && parents_of(j).contains(&i))
1138            .collect();
1139        for &k in kids.iter().rev() {
1140            stack.push((k, depth + 1));
1141        }
1142    }
1143    // Any in-play opt not reached by the walk (defensive; multi-parent diamonds
1144    // are already handled by `visited`) is appended at depth 0.
1145    for i in 0..n {
1146        if want[i] && !visited[i] {
1147            order.push((i, 0));
1148        }
1149    }
1150
1151    // 4. Materialize the nodes.
1152    order
1153        .into_iter()
1154        .map(|(i, depth)| {
1155            let opt = REGISTRY[i].opt;
1156            let has_children = (0..n).any(|j| want[j] && parents_of(j).contains(&i));
1157            let preempted_by: Vec<Opt> = preempted
1158                .iter()
1159                .filter(|&&(_, l)| l == opt)
1160                .map(|&(w, _)| w)
1161                .collect();
1162            let depends_on: Vec<Opt> = dependencies
1163                .iter()
1164                .filter(|&&(d, _)| d == opt)
1165                .map(|&(_, x)| x)
1166                .collect();
1167            OptNode {
1168                opt,
1169                depth,
1170                role: role_of(i),
1171                has_children,
1172                requires: REGISTRY[i].requires.to_vec(),
1173                preempts: REGISTRY[i].preempts.to_vec(),
1174                preempted_by,
1175                depends_on,
1176            }
1177        })
1178        .collect()
1179}
1180
1181/// A named optimization preset.
1182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1183pub enum Profile {
1184    /// Everything on (the default).
1185    Speed,
1186    /// Drop optimizations that spend memory for speed.
1187    Memory,
1188    /// Drop optimizations that can emit `unsafe` Rust.
1189    Safety,
1190}
1191
1192impl Profile {
1193    /// The (normalized) config this profile produces.
1194    pub fn config(self) -> OptimizationConfig {
1195        let mut cfg = OptimizationConfig::all_on();
1196        match self {
1197            Profile::Speed => {}
1198            Profile::Memory => {
1199                for m in REGISTRY {
1200                    if m.mem_class == MemClass::TradesMemForSpeed {
1201                        cfg.set(m.opt, false);
1202                    }
1203                }
1204            }
1205            Profile::Safety => {
1206                for m in REGISTRY {
1207                    if m.emits_unsafe {
1208                        cfg.set(m.opt, false);
1209                    }
1210                }
1211            }
1212        }
1213        cfg.normalize();
1214        cfg
1215    }
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220    use super::*;
1221    use std::collections::HashSet;
1222
1223    #[test]
1224    fn registry_has_one_row_per_opt_in_order() {
1225        // Every row's opt must equal its index (discriminant == bit == index).
1226        for (i, m) in REGISTRY.iter().enumerate() {
1227            assert_eq!(m.opt as usize, i, "registry row {i} out of order: {:?}", m.opt);
1228        }
1229    }
1230
1231    #[test]
1232    fn keywords_are_unique_lowercase_single_tokens() {
1233        let mut seen = HashSet::new();
1234        for m in REGISTRY {
1235            assert!(seen.insert(m.keyword), "duplicate keyword {}", m.keyword);
1236            assert_eq!(m.keyword, m.keyword.to_ascii_lowercase(), "keyword not lowercase: {}", m.keyword);
1237            assert!(!m.keyword.is_empty(), "empty keyword");
1238            assert!(!m.keyword.contains(char::is_whitespace), "keyword has whitespace: {}", m.keyword);
1239        }
1240    }
1241
1242    #[test]
1243    fn requires_and_conflicts_reference_real_variants_no_cycle() {
1244        // All references resolve (they are typed `Opt`, so this is structural):
1245        // assert no opt requires itself and there is no trivial requires-cycle.
1246        for m in REGISTRY {
1247            assert!(!m.requires.contains(&m.opt), "{:?} requires itself", m.opt);
1248            assert!(!m.conflicts.contains(&m.opt), "{:?} conflicts with itself", m.opt);
1249            for &r in m.requires {
1250                // No 2-cycle: the required opt must not require this one back.
1251                assert!(!r.meta().requires.contains(&m.opt), "requires cycle: {:?} <-> {:?}", m.opt, r);
1252            }
1253        }
1254    }
1255
1256    #[test]
1257    fn run_path_opt_costs_match_fixture() {
1258        // The RUN-path opts `optimize_for_run` actually gates, with the cost that
1259        // decides which hotness tier pays for them (HOTSWAP §3). Pinned as a fixture
1260        // so a misclassification is a hard failure, never silently absorbed.
1261        use OptCost::{Cheap, Heavy, Medium};
1262        let expect = |opt: Opt, c: OptCost| assert_eq!(opt.cost(), c, "{opt:?} cost");
1263        for (opt, c) in [
1264            (Opt::Inline, Cheap),
1265            (Opt::Tco, Cheap),
1266            (Opt::DeadCode, Cheap),
1267            (Opt::Specialize, Medium), // PE-light at T2 / PE-full at T3 via the iteration cap
1268            (Opt::Comptime, Medium),
1269            (Opt::LoopCse, Medium),
1270            (Opt::FloatStrength, Medium),
1271            (Opt::Cse, Medium),
1272            (Opt::Affine, Medium),
1273            (Opt::LoopHoist, Medium),
1274            (Opt::ClosedForm, Medium),
1275            (Opt::Fuse, Medium),
1276            (Opt::Oracle, Medium),
1277            (Opt::ElemType, Medium),
1278            (Opt::Unroll, Heavy),
1279            (Opt::Scalarize, Heavy),
1280            (Opt::Unfold, Heavy),
1281        ] {
1282            expect(opt, c);
1283        }
1284    }
1285
1286    #[test]
1287    fn requires_cost_monotone() {
1288        // Well-formedness for the tiered optimizer: every opt's `requires` must cost
1289        // no more than the opt itself, so a tier that admits the dependent always
1290        // admits its dependencies (HOTSWAP §4.2). A failure is a HARD STOP — never
1291        // resolve it by lowering a dependency's cost.
1292        for m in REGISTRY {
1293            for &r in m.requires {
1294                assert!(
1295                    r.cost() <= m.opt.cost(),
1296                    "{:?} (cost {:?}) requires {:?} (cost {:?}) — violates cost monotonicity",
1297                    m.opt,
1298                    m.opt.cost(),
1299                    r,
1300                    r.cost()
1301                );
1302            }
1303        }
1304    }
1305
1306    #[test]
1307    fn tier_budget_and_admits_truth_table() {
1308        use OptCost::{Cheap, Heavy, Medium};
1309        assert_eq!(Tier::T0.budget(), None);
1310        assert_eq!(Tier::T1.budget(), Some(Cheap));
1311        assert_eq!(Tier::T2.budget(), Some(Medium));
1312        assert_eq!(Tier::T3.budget(), Some(Heavy));
1313
1314        let on = OptimizationConfig::all_on();
1315        // T0 optimizes nothing.
1316        assert!(!admits(&on, Tier::T0, Opt::Inline));
1317        // T1: Cheap only.
1318        assert!(admits(&on, Tier::T1, Opt::Inline)); // Cheap
1319        assert!(!admits(&on, Tier::T1, Opt::Cse)); // Medium
1320        assert!(!admits(&on, Tier::T1, Opt::Unroll)); // Heavy
1321        // T2: Cheap + Medium.
1322        assert!(admits(&on, Tier::T2, Opt::Inline));
1323        assert!(admits(&on, Tier::T2, Opt::Cse));
1324        assert!(!admits(&on, Tier::T2, Opt::Unroll));
1325        // T3: everything.
1326        assert!(admits(&on, Tier::T3, Opt::Inline));
1327        assert!(admits(&on, Tier::T3, Opt::Cse));
1328        assert!(admits(&on, Tier::T3, Opt::Unroll));
1329        // admits still respects cfg.is_on — a disabled opt never runs at any tier.
1330        let off = OptimizationConfig::all_on().disable(Opt::Inline);
1331        assert!(!admits(&off, Tier::T3, Opt::Inline));
1332        // Tiers order like their budgets.
1333        assert!(Tier::T0 < Tier::T1 && Tier::T1 < Tier::T2 && Tier::T2 < Tier::T3);
1334    }
1335
1336    #[test]
1337    fn opt_count_matches_registry() {
1338        assert_eq!(OPT_COUNT, REGISTRY.len());
1339    }
1340
1341    #[test]
1342    fn tier_thresholds_default_and_tier_for() {
1343        let t = TierThresholds::default();
1344        assert_eq!((t.t1, t.t2, t.t3), (8, 32, 100));
1345        assert_eq!(t.tier_for(0), Tier::T0);
1346        assert_eq!(t.tier_for(7), Tier::T0);
1347        assert_eq!(t.tier_for(8), Tier::T1);
1348        assert_eq!(t.tier_for(31), Tier::T1);
1349        assert_eq!(t.tier_for(32), Tier::T2);
1350        assert_eq!(t.tier_for(99), Tier::T2);
1351        assert_eq!(t.tier_for(100), Tier::T3);
1352        assert_eq!(t.tier_for(10_000), Tier::T3);
1353    }
1354
1355    #[test]
1356    fn hotswap_mode_effective_tier() {
1357        let th = TierThresholds::default();
1358        let mk = |mode, force_tier| HotswapConfig {
1359            mode,
1360            thresholds: th,
1361            force_tier,
1362            pins: PinSet::none(),
1363        };
1364        // Eager: always T3 regardless of count.
1365        assert_eq!(mk(TierMode::Eager, None).effective_tier(0), Tier::T3);
1366        // Baseline: always T0.
1367        assert_eq!(mk(TierMode::Baseline, None).effective_tier(10_000), Tier::T0);
1368        // Tiered: climbs by hotness.
1369        let tiered = mk(TierMode::Tiered, None);
1370        assert_eq!(tiered.effective_tier(0), Tier::T0);
1371        assert_eq!(tiered.effective_tier(8), Tier::T1);
1372        assert_eq!(tiered.effective_tier(32), Tier::T2);
1373        assert_eq!(tiered.effective_tier(100), Tier::T3);
1374        // force_tier overrides everything (the determinism lever for tests).
1375        let forced = mk(TierMode::Tiered, Some(Tier::T2));
1376        assert_eq!(forced.effective_tier(0), Tier::T2);
1377        assert_eq!(forced.effective_tier(10_000), Tier::T2);
1378    }
1379
1380    #[test]
1381    fn hotswap_from_spec_modes_and_force() {
1382        let th = TierThresholds::default();
1383        // LOGOS_HOTSWAP=off forces Eager.
1384        assert_eq!(
1385            HotswapConfig::from_spec(Some("off"), Some("baseline"), None, th, None).mode,
1386            TierMode::Eager
1387        );
1388        // Profiles.
1389        assert_eq!(HotswapConfig::from_spec(None, Some("eager"), None, th, None).mode, TierMode::Eager);
1390        assert_eq!(HotswapConfig::from_spec(None, Some("baseline"), None, th, None).mode, TierMode::Baseline);
1391        assert_eq!(HotswapConfig::from_spec(None, Some("tiered"), None, th, None).mode, TierMode::Tiered);
1392        // Default mode is Eager (the §12.2 ratchet — Tiered is opt-in until proven).
1393        assert_eq!(HotswapConfig::from_spec(None, None, None, th, None).mode, TierMode::Eager);
1394        assert_eq!(HotswapConfig::default().mode, TierMode::Eager);
1395        // force_tier parses number and word forms.
1396        assert_eq!(HotswapConfig::from_spec(None, None, Some("2"), th, None).force_tier, Some(Tier::T2));
1397        assert_eq!(HotswapConfig::from_spec(None, None, Some("veryhot"), th, None).force_tier, Some(Tier::T3));
1398        assert_eq!(HotswapConfig::from_spec(None, None, Some("nonsense"), th, None).force_tier, None);
1399    }
1400
1401    #[test]
1402    fn run_tier_maps_mode_to_upfront_tier() {
1403        let th = TierThresholds::default();
1404        let mk = |mode, force_tier| HotswapConfig { mode, thresholds: th, force_tier, pins: PinSet::none() };
1405        // Eager pays the full optimizer upfront; Tiered/Baseline start at the baseline.
1406        assert_eq!(mk(TierMode::Eager, None).run_tier(), Tier::T3);
1407        assert_eq!(mk(TierMode::Tiered, None).run_tier(), Tier::T0);
1408        assert_eq!(mk(TierMode::Baseline, None).run_tier(), Tier::T0);
1409        // force_tier overrides the mode (test determinism).
1410        assert_eq!(mk(TierMode::Tiered, Some(Tier::T2)).run_tier(), Tier::T2);
1411        assert_eq!(mk(TierMode::Eager, Some(Tier::T1)).run_tier(), Tier::T1);
1412    }
1413
1414    #[test]
1415    fn admits_pinned_overrides_cost_tier() {
1416        let on = OptimizationConfig::all_on();
1417        let mut pins = PinSet::none();
1418        pins.set(Opt::Unfold, Pin::Never);
1419        pins.set(Opt::Fuse, Pin::Eager);
1420        pins.set(Opt::Specialize, Pin::At(Tier::T2));
1421        let hs = HotswapConfig {
1422            mode: TierMode::Tiered,
1423            thresholds: TierThresholds::default(),
1424            force_tier: None,
1425            pins,
1426        };
1427        // Never: off at every tier.
1428        assert!(!admits_pinned(&on, &hs, Tier::T3, Opt::Unfold));
1429        // Eager: on as soon as warm (T1), despite Medium cost.
1430        assert!(admits_pinned(&on, &hs, Tier::T1, Opt::Fuse));
1431        // At(T2): off at T1, on at T2.
1432        assert!(!admits_pinned(&on, &hs, Tier::T1, Opt::Specialize));
1433        assert!(admits_pinned(&on, &hs, Tier::T2, Opt::Specialize));
1434        // Unpinned: cost-derived (matches `admits`).
1435        assert!(admits_pinned(&on, &hs, Tier::T1, Opt::Inline));
1436        assert!(!admits_pinned(&on, &hs, Tier::T1, Opt::Cse));
1437        // A disabled opt never runs regardless of pin.
1438        let off = OptimizationConfig::all_on().disable(Opt::Fuse);
1439        assert!(!admits_pinned(&off, &hs, Tier::T3, Opt::Fuse));
1440    }
1441
1442    #[test]
1443    fn pin_spec_parses_keyword_value_pairs() {
1444        let mut pins = PinSet::none();
1445        apply_pin_spec(&mut pins, "specialize:eager, fuse:t1 ; unfold:never, bogus:t2, cse:nonsense");
1446        assert_eq!(pins.get(Opt::Specialize), Some(Pin::Eager));
1447        assert_eq!(pins.get(Opt::Fuse), Some(Pin::At(Tier::T1)));
1448        assert_eq!(pins.get(Opt::Unfold), Some(Pin::Never));
1449        assert_eq!(pins.get(Opt::Cse), None, "unparseable value is ignored");
1450    }
1451
1452    #[test]
1453    fn decorate_source_inserts_file_level_decorators() {
1454        let src = "## To f (x: Int) -> Int:\n    Return x.\n\n## Main\nShow 1.\n";
1455        let d = decorate_source(src, &["scalarize", "unroll"]);
1456        assert!(d.contains("## No scalarize\n## No unroll\n## Main"), "got:\n{d}");
1457        assert!(d.starts_with("## To f"), "functions stay before the decorators");
1458        // Empty list is a no-op.
1459        assert_eq!(decorate_source(src, &[]), src);
1460        // A library with no `## Main` appends the decorators (EOF = file-level).
1461        let lib = "## To f (x: Int) -> Int:\n    Return x.\n";
1462        assert!(decorate_source(lib, &["cse"]).ends_with("## No cse\n"));
1463    }
1464
1465    #[test]
1466    fn decorate_tiers_inserts_file_level_pins() {
1467        let src = "## To f (x: Int) -> Int:\n    Return x.\n\n## Main\nShow 1.\n";
1468        let d = decorate_tiers(src, &[("specialize", "eager"), ("unfold", "never")]);
1469        assert!(
1470            d.contains("## Tier specialize eager\n## Tier unfold never\n## Main"),
1471            "got:\n{d}"
1472        );
1473        assert!(d.starts_with("## To f"), "functions stay before the decorators");
1474        assert_eq!(decorate_tiers(src, &[]), src, "empty list is a no-op");
1475        let lib = "## To f (x: Int) -> Int:\n    Return x.\n";
1476        assert!(decorate_tiers(lib, &[("fuse", "t1")]).ends_with("## Tier fuse t1\n"));
1477    }
1478
1479    #[test]
1480    fn by_keyword_round_trips() {
1481        for m in REGISTRY {
1482            assert_eq!(by_keyword(m.keyword), Some(m.opt));
1483            assert_eq!(by_keyword(&m.keyword.to_uppercase()), Some(m.opt));
1484        }
1485        assert_eq!(by_keyword("nonsense"), None);
1486    }
1487
1488    #[test]
1489    fn all_on_enables_everything_all_off_nothing() {
1490        let on = OptimizationConfig::all_on();
1491        let off = OptimizationConfig::all_off();
1492        for m in REGISTRY {
1493            assert!(on.is_on(m.opt), "{:?} should be on", m.opt);
1494            assert!(!off.is_on(m.opt), "{:?} should be off", m.opt);
1495        }
1496        assert!(off.is_all_off());
1497        assert_eq!(OptimizationConfig::default(), on);
1498    }
1499
1500    #[test]
1501    fn serde_round_trips() {
1502        let cfg = OptimizationConfig::all_on().disable(Opt::Scalarize).disable(Opt::Unroll);
1503        let json = serde_json::to_string(&cfg).unwrap();
1504        let back: OptimizationConfig = serde_json::from_str(&json).unwrap();
1505        assert_eq!(cfg, back);
1506    }
1507
1508    #[test]
1509    fn normalize_disables_dependents_and_reports() {
1510        // Scalarize requires Cse; turning Cse off must disable Scalarize.
1511        let mut cfg = OptimizationConfig::all_on().disable(Opt::Cse);
1512        let report = cfg.normalize();
1513        assert!(!cfg.is_on(Opt::Scalarize), "Scalarize must follow Cse off");
1514        assert!(report
1515            .auto_disabled
1516            .contains(&(Opt::Scalarize, Reason::DependencyOff(Opt::Cse))));
1517    }
1518
1519    #[test]
1520    fn normalize_oracle_closure() {
1521        // Oracle off must disable ElemType, OracleHints, Unchecked.
1522        let mut cfg = OptimizationConfig::all_on().disable(Opt::Oracle);
1523        cfg.normalize();
1524        assert!(!cfg.is_on(Opt::ElemType));
1525        assert!(!cfg.is_on(Opt::OracleHints));
1526        assert!(!cfg.is_on(Opt::Unchecked));
1527    }
1528
1529    #[test]
1530    fn default_config_is_stable_under_normalize() {
1531        // The default (all-on) config has NO config-level conflicts: AoS/Interleave
1532        // is mutually exclusive with Unroll/Scalarize only at RUNTIME (the codegen
1533        // regime gate), never in the config, so all three stay enabled by default
1534        // and normalization is a no-op. Guards the regression where a config-level
1535        // Interleave⇄Unroll/Scalarize conflict silently disabled AoS by default.
1536        let mut cfg = OptimizationConfig::all_on();
1537        let report = cfg.normalize();
1538        assert!(report.is_empty(), "default config must normalize to a no-op, got {:?}", report);
1539        assert!(cfg.is_on(Opt::Interleave));
1540        assert!(cfg.is_on(Opt::Unroll));
1541        assert!(cfg.is_on(Opt::Scalarize));
1542        assert_eq!(cfg, OptimizationConfig::all_on());
1543    }
1544
1545    #[test]
1546    fn safety_profile_drops_all_unsafe() {
1547        let cfg = Profile::Safety.config();
1548        for m in REGISTRY {
1549            if m.emits_unsafe {
1550                assert!(!cfg.is_on(m.opt), "Safety must disable unsafe-emitting {:?}", m.opt);
1551            }
1552        }
1553    }
1554
1555    #[test]
1556    fn memory_profile_drops_mem_hogs_keeps_savers() {
1557        let cfg = Profile::Memory.config();
1558        assert!(!cfg.is_on(Opt::Unroll));
1559        assert!(!cfg.is_on(Opt::Scalarize));
1560        assert!(cfg.is_on(Opt::Narrow) || !OptimizationConfig::all_on().is_on(Opt::Unbox));
1561        assert!(cfg.is_on(Opt::Unbox), "Memory keeps memory-saving Unbox");
1562    }
1563
1564    #[test]
1565    fn from_toggles_reports_forced_off() {
1566        let mut t = BTreeMap::new();
1567        t.insert("cse".to_string(), false);
1568        let (cfg, forced) = OptimizationConfig::from_toggles(&t);
1569        assert!(!cfg.is_on(Opt::Cse));
1570        assert!(!cfg.is_on(Opt::Scalarize));
1571        assert!(forced.contains(&Opt::Scalarize));
1572    }
1573
1574    #[test]
1575    fn from_spec_master_off_is_all_off() {
1576        assert!(OptimizationConfig::from_spec(true, None, None).is_all_off());
1577        assert!(OptimizationConfig::from_spec(true, Some("safety"), Some("cse")).is_all_off());
1578    }
1579
1580    #[test]
1581    fn from_spec_default_is_all_on() {
1582        assert_eq!(OptimizationConfig::from_spec(false, None, None), OptimizationConfig::all_on());
1583    }
1584
1585    #[test]
1586    fn from_spec_off_list_disables_listed_keywords() {
1587        let cfg = OptimizationConfig::from_spec(false, None, Some("scalarize, unroll;nonsense"));
1588        assert!(!cfg.is_on(Opt::Scalarize));
1589        assert!(!cfg.is_on(Opt::Unroll));
1590        assert!(cfg.is_on(Opt::Memo), "unlisted opts stay on; unknown tokens ignored");
1591    }
1592
1593    #[test]
1594    fn from_spec_off_list_normalizes_dependents() {
1595        // Disabling Oracle cascades to its dependents.
1596        let cfg = OptimizationConfig::from_spec(false, None, Some("oracle"));
1597        assert!(!cfg.is_on(Opt::Oracle));
1598        assert!(!cfg.is_on(Opt::Unchecked));
1599        assert!(!cfg.is_on(Opt::OracleHints));
1600        assert!(!cfg.is_on(Opt::ElemType));
1601    }
1602
1603    #[test]
1604    fn from_spec_profiles_drop_the_right_opts() {
1605        let safety = OptimizationConfig::from_spec(false, Some("safety"), None);
1606        for m in REGISTRY {
1607            if m.emits_unsafe {
1608                assert!(!safety.is_on(m.opt), "Safety must drop unsafe-emitting {:?}", m.opt);
1609            }
1610        }
1611        let memory = OptimizationConfig::from_spec(false, Some("memory"), None);
1612        assert!(!memory.is_on(Opt::Unroll));
1613        assert!(memory.is_on(Opt::Unbox), "Memory keeps memory-saving opts");
1614    }
1615
1616    // --- Toggle-linking: the requires graph drives both cascade directions ---
1617
1618    #[test]
1619    fn disabling_any_requirement_cascades_to_dependents() {
1620        // The toggle-link contract, generic over the registry: for EVERY `requires`
1621        // edge, turning the requirement off (from all-on) must, after normalize,
1622        // also turn the dependent off. This is what makes "turn a parent off and
1623        // its children turn off" hold for every edge, not just the hand-picked ones.
1624        for m in REGISTRY {
1625            for &req in m.requires {
1626                let mut cfg = OptimizationConfig::all_on();
1627                cfg.set(req, false);
1628                cfg.normalize();
1629                assert!(
1630                    !cfg.is_on(m.opt),
1631                    "{:?} requires {:?}; disabling {:?} must cascade {:?} off",
1632                    m.opt, req, req, m.opt
1633                );
1634            }
1635        }
1636    }
1637
1638    #[test]
1639    fn enable_with_requires_pulls_ancestors() {
1640        // Enabling a leaf from all-off must pull its whole requires-chain on, so the
1641        // act of turning a child on is not instantly undone by normalize.
1642        let mut cfg = OptimizationConfig::all_off();
1643        cfg.enable_with_requires(Opt::Interleave);
1644        assert!(cfg.is_on(Opt::Interleave));
1645        assert!(cfg.is_on(Opt::Scalarize), "Interleave needs Scalarize");
1646        assert!(cfg.is_on(Opt::Cse), "…which needs Cse");
1647        assert!(!cfg.is_on(Opt::Unroll), "unrelated opts stay off");
1648        assert!(!cfg.is_on(Opt::Oracle), "unrelated opts stay off");
1649        let report = cfg.normalize();
1650        assert!(report.is_empty(), "an enabled chain must be normalize-stable: {report:?}");
1651    }
1652
1653    // --- relationship_tree: the deterministic per-program chain from one trace ---
1654
1655    fn role_of(tree: &[OptNode], opt: Opt) -> Option<OptRole> {
1656        tree.iter().find(|n| n.opt == opt).map(|n| n.role)
1657    }
1658    fn depth_of(tree: &[OptNode], opt: Opt) -> usize {
1659        tree.iter().find(|n| n.opt == opt).unwrap().depth
1660    }
1661    fn pos_of(tree: &[OptNode], opt: Opt) -> usize {
1662        tree.iter().position(|n| n.opt == opt).unwrap()
1663    }
1664
1665    #[test]
1666    fn relationship_tree_pulls_enabler_parents_for_orphan_children() {
1667        // coins-like: unchecked/oraclehints/elemtype fire, but their `requires`
1668        // parent oracle does not — it must still appear, as an Enabler, parent of
1669        // the children, so the tree is never orphaned.
1670        let fired = [Opt::Unchecked, Opt::OracleHints, Opt::ElemType];
1671        let tree = relationship_tree(&fired, &[], &[]);
1672        assert_eq!(role_of(&tree, Opt::Unchecked), Some(OptRole::Fired));
1673        assert_eq!(role_of(&tree, Opt::Oracle), Some(OptRole::Enabler));
1674        assert!(depth_of(&tree, Opt::Oracle) < depth_of(&tree, Opt::Unchecked));
1675        assert!(tree.iter().find(|n| n.opt == Opt::Oracle).unwrap().has_children);
1676    }
1677
1678    #[test]
1679    fn relationship_tree_surfaces_preempted_losers() {
1680        // densemap fired and beat narrowmap: narrowmap must appear as a Preempted
1681        // node (the "skipped because they don't play nice" opt), annotated with the
1682        // winner that beat it, even though it never fired.
1683        let fired = [Opt::DenseMap];
1684        let preempted = [(Opt::DenseMap, Opt::NarrowMap)];
1685        let tree = relationship_tree(&fired, &preempted, &[]);
1686        let node = tree.iter().find(|n| n.opt == Opt::NarrowMap).expect("narrowmap present");
1687        assert_eq!(node.role, OptRole::Preempted);
1688        assert!(node.preempted_by.contains(&Opt::DenseMap));
1689        assert_eq!(role_of(&tree, Opt::DenseMap), Some(OptRole::Fired));
1690    }
1691
1692    #[test]
1693    fn relationship_tree_nests_requires_chain_by_depth() {
1694        // cse → scalarize → interleave all fired: depths 0,1,2, parents first.
1695        let fired = [Opt::Cse, Opt::Scalarize, Opt::Interleave];
1696        let tree = relationship_tree(&fired, &[], &[]);
1697        assert_eq!(depth_of(&tree, Opt::Cse), 0);
1698        assert_eq!(depth_of(&tree, Opt::Scalarize), 1);
1699        assert_eq!(depth_of(&tree, Opt::Interleave), 2);
1700        assert!(pos_of(&tree, Opt::Cse) < pos_of(&tree, Opt::Scalarize));
1701        assert!(pos_of(&tree, Opt::Scalarize) < pos_of(&tree, Opt::Interleave));
1702    }
1703
1704    #[test]
1705    fn relationship_tree_is_deterministic_and_only_in_play() {
1706        // The in-play set is exactly fired ∪ losers ∪ requires-closure — nothing
1707        // else — and the derivation is deterministic (same input → same output).
1708        let fired = [Opt::DenseMap, Opt::Unchecked];
1709        let preempted = [(Opt::DenseMap, Opt::NarrowMap)];
1710        let a = relationship_tree(&fired, &preempted, &[]);
1711        let b = relationship_tree(&fired, &preempted, &[]);
1712        let keys = |t: &[OptNode]| {
1713            t.iter().map(|n| (n.opt, n.depth, n.role)).collect::<Vec<_>>()
1714        };
1715        assert_eq!(keys(&a), keys(&b), "derivation must be deterministic");
1716        let in_play: std::collections::BTreeSet<Opt> = a.iter().map(|n| n.opt).collect();
1717        let expected: std::collections::BTreeSet<Opt> =
1718            [Opt::DenseMap, Opt::NarrowMap, Opt::Unchecked, Opt::Oracle].into_iter().collect();
1719        assert_eq!(in_play, expected, "only in-play opts appear");
1720    }
1721
1722    #[test]
1723    fn relationship_tree_nests_by_per_program_dependencies() {
1724        // A per-program dependency (dependent, dep) nests the dependent under the
1725        // dep even with no static `requires` edge — e.g. dead-code elimination only
1726        // fired because scalarization produced the dead code. Both fired.
1727        let fired = [Opt::Scalarize, Opt::DeadCode, Opt::Cse];
1728        let deps = [(Opt::DeadCode, Opt::Scalarize)];
1729        let tree = relationship_tree(&fired, &[], &deps);
1730        // deadcode hangs under scalarize (deeper), and records the dependency.
1731        assert!(depth_of(&tree, Opt::DeadCode) > depth_of(&tree, Opt::Scalarize));
1732        assert!(pos_of(&tree, Opt::Scalarize) < pos_of(&tree, Opt::DeadCode));
1733        let dc = tree.iter().find(|n| n.opt == Opt::DeadCode).unwrap();
1734        assert!(dc.depends_on.contains(&Opt::Scalarize));
1735        assert_eq!(dc.role, OptRole::Fired);
1736        // scalarize has children now (deadcode depends on it).
1737        assert!(tree.iter().find(|n| n.opt == Opt::Scalarize).unwrap().has_children);
1738        // static requires still compose: scalarize under cse.
1739        assert!(depth_of(&tree, Opt::Scalarize) > depth_of(&tree, Opt::Cse));
1740    }
1741}