logicaffeine_compile/optimize/mod.rs
1mod defunctionalize;
2mod fold;
3mod splice_fuse;
4mod inline_tiny;
5mod inline_leaf;
6mod inline_recursive;
7mod popcount_leaf;
8mod symmetry;
9mod dce;
10mod propagate;
11mod gvn;
12mod loop_carried_cse;
13mod float_induction_sr;
14mod licm;
15mod bound_version;
16mod closed_form;
17mod ctfe;
18mod deforest;
19// Loop unrolling reuses the codegen peephole's counted-loop recognizer, so it
20// rides the same feature gate; the AOT (Rust-emitting) pipeline is its consumer.
21#[cfg(feature = "codegen")]
22mod unroll;
23// Fixed-size float/int Seq scalarization (the interpreter's SROA). Reuses the
24// codegen scalarization detection, so it rides the same feature gate; paired
25// AFTER the run-path unroller, which makes every surviving index constant.
26#[cfg(feature = "codegen")]
27mod scalarize;
28// Affine read-only `Seq` scalarization (the interpreter's analog of the AOT's
29// `codegen::affine_array`): delete a CSR-style offset array built by one affine
30// `Push f(i) to arr` loop and substitute every `item k of arr` read with the
31// closed form `f(k-1)`. Default OFF behind `LOGOS_RUN_AFFINE`; AOT-only feature
32// gate (it rewrites the run-path AST). Runs before `scalarize`, on the array
33// form GVN already proved reload-correct.
34#[cfg(feature = "codegen")]
35mod affine_scalarize;
36// Guard-based loop index-set splitting reuses the same recognizer (its emitted
37// sub-loops must satisfy it to vectorize), so it rides the same gate; AOT-only.
38#[cfg(feature = "codegen")]
39mod loop_split;
40mod affine;
41mod abstract_interp;
42pub use abstract_interp::{
43 lin_to_rust, oracle_analyze, oracle_analyze_with, oracle_analyze_with_entry_guards, OracleFacts,
44 ScalarKind, VarProvenFacts,
45};
46pub mod egraph;
47pub mod supercompile;
48pub mod effects;
49pub mod bta;
50pub mod partial_eval;
51
52use std::collections::HashMap;
53
54use crate::arena::Arena;
55use crate::ast::stmt::{Expr, Stmt};
56use crate::intern::{Interner, Symbol};
57use crate::optimization::{
58 admits_pinned, FiredOptimizations, HotswapConfig, Opt, OptimizationConfig, Pin, Tier,
59};
60
61thread_local! {
62 /// The optimization config for the CURRENT compile, consulted by codegen and
63 /// the VM compiler for their per-emit toggle decisions (e.g. emit
64 /// `get_unchecked` vs a checked index). It is set ONCE at each compile entry
65 /// (`codegen_program`, `optimize_for_run`) from the explicitly-threaded
66 /// `OptimizationConfig`; any path that does not set it sees the all-on
67 /// default (the prior, fully-optimized behaviour). This is the single
68 /// replacement for the ~16 scattered `std::env::var("LOGOS_*")` reads codegen
69 /// and the VM used to perform — one controlled per-compile source, not
70 /// ambient process state.
71 static ACTIVE_CONFIG: std::cell::Cell<OptimizationConfig> =
72 std::cell::Cell::new(OptimizationConfig::all_on());
73}
74
75/// Record the optimization config for the current compile. Call at every compile
76/// entry (codegen / VM) so the deep codegen + VM toggle reads see it.
77pub fn set_active_config(cfg: OptimizationConfig) {
78 ACTIVE_CONFIG.with(|c| c.set(cfg));
79}
80
81thread_local! {
82 /// Expr node addresses whose Int op is PROVEN in-range by the pass that
83 /// CONSTRUCTED them — the proof lives where the knowledge lives (e.g.
84 /// `try_defer_modulus` sizes its version guard so no op in the guarded
85 /// chunk can overflow, a relation the interval oracle's widening cannot
86 /// re-derive). Codegen consults this BEFORE the interval oracle and
87 /// lowers a registered op as raw i64. Cleared at every optimizer entry
88 /// so a freed arena's reused addresses can never leak a stale proof
89 /// into a later program on the same thread.
90 static PROVEN_RAW_INT_OPS: std::cell::RefCell<std::collections::HashSet<usize>> =
91 std::cell::RefCell::new(std::collections::HashSet::new());
92}
93
94/// Forget all constructed-proof registrations (call at optimizer entry).
95pub(crate) fn clear_proven_raw_int_ops() {
96 PROVEN_RAW_INT_OPS.with(|s| s.borrow_mut().clear());
97}
98
99/// Register an Int op the CONSTRUCTING pass proved can never leave i64.
100pub(crate) fn mark_proven_raw_int_op(e: &crate::ast::stmt::Expr) {
101 PROVEN_RAW_INT_OPS.with(|s| {
102 s.borrow_mut().insert(e as *const crate::ast::stmt::Expr as usize);
103 });
104}
105
106/// Was this exact op node proven in-range by its constructing pass?
107pub fn expr_proven_raw_int_op(e: &crate::ast::stmt::Expr) -> bool {
108 PROVEN_RAW_INT_OPS.with(|s| s.borrow().contains(&(e as *const crate::ast::stmt::Expr as usize)))
109}
110
111/// The current compile's optimization config (all-on if unset).
112pub fn active_config() -> OptimizationConfig {
113 ACTIVE_CONFIG.with(|c| c.get())
114}
115
116thread_local! {
117 /// When `Some`, an optimization-firing trace is active for the current
118 /// compile: each optimization records (via [`mark_fired`]) the moment it
119 /// actually fires. `None` (the default) means no trace — [`mark_fired`] is
120 /// a no-op and the change-detection in [`run_traced`] is skipped, so the
121 /// normal compile path pays nothing. Opt-in, set once per traced compile by
122 /// [`begin_fired_trace`]; lives on the same thread that runs both the
123 /// optimizer and codegen, so codegen-time marks are recorded too.
124 static FIRED: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
125
126 /// The PRECEDENCE decisions recorded during the current trace: each
127 /// `(winner, loser)` pair is a spot where the code chose `winner` and skipped
128 /// `loser` for an instance (the conflict the optimization SYSTEM resolves).
129 /// `None` when no trace is active, so [`mark_preempted`] is then a no-op and
130 /// the normal compile path pays nothing. Shares the trace lifetime with
131 /// [`FIRED`] — both reset by [`begin_fired_trace`].
132 static PREEMPTED: std::cell::RefCell<Option<Vec<(Opt, Opt)>>> =
133 const { std::cell::RefCell::new(None) };
134}
135
136/// Begin recording which optimizations fire AND which they preempt for the
137/// current compile. Resets both accumulators; pair with [`end_fired_trace`] /
138/// [`end_preempted_trace`].
139pub fn begin_fired_trace() {
140 FIRED.with(|f| f.set(Some(0)));
141 PREEMPTED.with(|p| *p.borrow_mut() = Some(Vec::new()));
142}
143
144/// Stop recording and take the fired set (`None` if no trace was active).
145pub fn end_fired_trace() -> Option<FiredOptimizations> {
146 FIRED.with(|f| f.take().map(FiredOptimizations::from_bits))
147}
148
149/// Whether a firing trace is currently active. Gates the cost of the
150/// before/after fingerprint in [`run_traced`].
151#[inline]
152pub fn fired_trace_active() -> bool {
153 FIRED.with(|f| f.get().is_some())
154}
155
156/// Record that `opt` fired in the current compile. No-op when no trace is active.
157#[inline]
158pub fn mark_fired(opt: Opt) {
159 FIRED.with(|f| {
160 if let Some(bits) = f.get() {
161 f.set(Some(bits | opt.bit()));
162 }
163 });
164}
165
166/// Record that `winner` took precedence over `loser` here — the exact spot the
167/// code skips one optimization for another (the conflict, traced at its source,
168/// the same way [`mark_fired`] traces firing). No-op when no trace is active, so
169/// the normal compile path and the benchmark scripts pay nothing. Self-gated on
170/// BOTH being enabled in the current config: a preemption is only meaningful when
171/// the two genuinely contest an instance — so toggling either off cleanly stops
172/// the edge from being reported.
173#[inline]
174pub fn mark_preempted(winner: Opt, loser: Opt) {
175 if !fired_trace_active() {
176 return;
177 }
178 let cfg = active_config();
179 if !cfg.is_on(winner) || !cfg.is_on(loser) {
180 return;
181 }
182 PREEMPTED.with(|p| {
183 if let Some(v) = p.borrow_mut().as_mut() {
184 v.push((winner, loser));
185 }
186 });
187}
188
189/// Stop recording and take the deduplicated set of `(winner, loser)` precedence
190/// decisions made during the trace (empty if no trace was active).
191pub fn end_preempted_trace() -> Vec<(Opt, Opt)> {
192 let mut edges = PREEMPTED.with(|p| p.borrow_mut().take().unwrap_or_default());
193 edges.sort_by_key(|&(w, l)| (w as u8, l as u8));
194 edges.dedup();
195 edges
196}
197
198/// Run an AST pass, recording that `opt` fired iff the pass changed the program
199/// — but only while a trace is active. The AST derives `Debug` (not `PartialEq`),
200/// so a `{:?}` fingerprint is the change signal; it is taken only under a live
201/// trace, so the normal compile path runs the pass exactly as before with no
202/// extra cost. The pass runs exactly once either way (no behavior change).
203#[inline]
204pub(crate) fn run_traced<'a>(
205 opt: Opt,
206 input: Vec<Stmt<'a>>,
207 pass: impl FnOnce(Vec<Stmt<'a>>) -> Vec<Stmt<'a>>,
208) -> Vec<Stmt<'a>> {
209 if !fired_trace_active() {
210 return pass(input);
211 }
212 let before = format!("{input:?}");
213 let out = pass(input);
214 if format!("{out:?}") != before {
215 mark_fired(opt);
216 }
217 out
218}
219
220/// Lighter optimization for Futamura P1: fold + propagate + PE + CTFE.
221/// Skips DCE, abstract interpretation, supercompilation, and structural
222/// transforms (LICM, closed-form, deforestation) that eliminate branches
223/// or restructure control flow. The residual preserves the program's
224/// original control-flow structure while still folding constants,
225/// propagating values, specializing functions, and evaluating CTFE.
226pub fn optimize_for_projection<'a>(
227 stmts: Vec<Stmt<'a>>,
228 expr_arena: &'a Arena<Expr<'a>>,
229 stmt_arena: &'a Arena<Stmt<'a>>,
230 interner: &mut Interner,
231 cfg: &OptimizationConfig,
232) -> Vec<Stmt<'a>> {
233 let bta_cache = bta::analyze_with_sccs(&stmts, interner);
234 let mut current = stmts;
235 if cfg.is_on(Opt::Specialize) {
236 let mut variant_count: HashMap<Symbol, usize> = HashMap::new();
237 for _ in 0..16 {
238 let folded = fold::fold_stmts(current, expr_arena, stmt_arena, interner);
239 let propagated = propagate::propagate_stmts(folded, expr_arena, stmt_arena, interner);
240 let (specialized, changes) = partial_eval::specialize_stmts_with_state(
241 propagated, expr_arena, stmt_arena, interner, &mut variant_count,
242 Some(&bta_cache),
243 );
244 current = specialized;
245 if changes == 0 {
246 break;
247 }
248 }
249 }
250 if cfg.is_on(Opt::Comptime) {
251 current = ctfe::ctfe_stmts(current, expr_arena, stmt_arena, interner);
252 }
253 fold::fold_stmts(current, expr_arena, stmt_arena, interner)
254}
255
256/// The RUN-PATH pipeline (EXODIA D1): the Futamura residual — fold,
257/// propagate, polyvariant PE, CTFE, GVN, LICM, closed-form, deforestation,
258/// interval analysis, DCE — everything except supercompilation (whose
259/// driving cost is unbounded; it stays AOT-only). Budgeted: programs beyond
260/// the statement gate run raw, and `LOGOS_RUN_OPT=0` kills the pass
261/// entirely. Optimizer time lands inside the measured run, so the budget is
262/// part of the contract.
263pub fn optimize_for_run<'a>(
264 stmts: Vec<Stmt<'a>>,
265 expr_arena: &'a Arena<Expr<'a>>,
266 stmt_arena: &'a Arena<Stmt<'a>>,
267 interner: &mut Interner,
268 cfg: &OptimizationConfig,
269) -> Vec<Stmt<'a>> {
270 // The un-tiered entry point is `Tier::T3`: every admitted opt runs, so with an
271 // all-on config this is today's whole-program pipeline, bit-for-bit (HOTSWAP §2).
272 optimize_for_run_tiered(
273 stmts,
274 expr_arena,
275 stmt_arena,
276 interner,
277 cfg,
278 &HotswapConfig::default(),
279 Tier::T3,
280 )
281}
282
283/// How many partial-evaluation fixpoint iterations to pay for. `Tier::T3` runs the
284/// full 16-iteration fixpoint (PE-full); `Tier::T2` runs a capped 4 (PE-light) to feed
285/// the Medium passes folded/specialized code without the full tax. Below T2 the
286/// Specialize pass is not admitted at all (it is `Medium`), so the cap is unused. A
287/// `## Tier specialize <eager|t..>` pin brings PE forward AND gives it the full
288/// fixpoint ("ignore its cost", HOTSWAP §8).
289fn pe_iters(hs: &HotswapConfig, tier: Tier) -> usize {
290 if matches!(hs.pin(Opt::Specialize), Some(Pin::Eager) | Some(Pin::At(_))) {
291 return 16;
292 }
293 match tier {
294 Tier::T0 | Tier::T1 => 0,
295 Tier::T2 => 4,
296 Tier::T3 => 16,
297 }
298}
299
300/// The run-path optimizer, gated by the unit's hotness `tier` (HOTSWAP §4). Every
301/// pass runs in the SAME order as the un-tiered pipeline; `tier` decides WHICH passes
302/// run (via [`admits_pinned`], a single cost comparison, with `hs`'s per-opt pins
303/// applied) and how many PE-fixpoint iterations to pay for. `Tier::T3` with an all-on
304/// config and no pins reproduces [`optimize_for_run`] exactly — the compatibility +
305/// soundness anchor.
306pub fn optimize_for_run_tiered<'a>(
307 stmts: Vec<Stmt<'a>>,
308 expr_arena: &'a Arena<Expr<'a>>,
309 stmt_arena: &'a Arena<Stmt<'a>>,
310 interner: &mut Interner,
311 cfg: &OptimizationConfig,
312 hs: &HotswapConfig,
313 tier: Tier,
314) -> Vec<Stmt<'a>> {
315 set_active_config(*cfg);
316 clear_proven_raw_int_ops();
317 if cfg.is_all_off() || tier == Tier::T0 {
318 return stmts;
319 }
320 const MAX_RUN_OPT_STMTS: usize = 5_000;
321 if stmts.len() > MAX_RUN_OPT_STMTS {
322 return stmts;
323 }
324 let bta_cache = bta::analyze_with_sccs(&stmts, interner);
325 let mut current = stmts;
326 if admits_pinned(cfg, hs, tier, Opt::Inline) {
327 // Tiny pure helpers inline first, so PE/fold/GVN/LICM see straight
328 // arithmetic and the loop regions compile call-free.
329 current = run_traced(Opt::Inline, current, |c| {
330 inline_tiny::inline_tiny_fns(c, expr_arena, stmt_arena, interner)
331 });
332 // Statement-body leaf helpers (iterative gcd shape) fold in next, so
333 // their calling loops compile as one call-free region.
334 current = run_traced(Opt::Inline, current, |c| {
335 inline_leaf::inline_leaf_fns(c, expr_arena, stmt_arena, interner)
336 });
337 }
338 if admits_pinned(cfg, hs, tier, Opt::Specialize) {
339 let mut variant_count: HashMap<Symbol, usize> = HashMap::new();
340 let mut specialized_any = false;
341 let cap = pe_iters(hs, tier);
342 let mut capped_early = false;
343 for i in 0..cap {
344 let folded = fold::fold_stmts(current, expr_arena, stmt_arena, interner);
345 let propagated = propagate::propagate_stmts(folded, expr_arena, stmt_arena, interner);
346 let (specialized, changes) = partial_eval::specialize_stmts_with_state(
347 propagated, expr_arena, stmt_arena, interner, &mut variant_count,
348 Some(&bta_cache),
349 );
350 current = specialized;
351 if changes == 0 {
352 break;
353 }
354 specialized_any = true;
355 // The fixpoint had not converged by the tier's iteration budget.
356 capped_early = i + 1 == cap;
357 }
358 if specialized_any {
359 mark_fired(Opt::Specialize);
360 }
361 // PE-light (T2) stops the fixpoint early, which can leave the residual not
362 // fold-stable; the Medium passes below (loop-carried CSE, GVN, closed-form)
363 // assume canonical sub-expressions. Re-normalize after a capped-early exit so
364 // they fire on the same shape they would at T3. T3 runs to convergence (its
365 // last iteration specialized 0 changes on folded+propagated input), so it is
366 // already canonical and stays byte-for-byte today's pipeline.
367 if capped_early && tier < Tier::T3 {
368 current = fold::fold_stmts(current, expr_arena, stmt_arena, interner);
369 current = propagate::propagate_stmts(current, expr_arena, stmt_arena, interner);
370 }
371 }
372 if admits_pinned(cfg, hs, tier, Opt::Comptime) {
373 current = run_traced(Opt::Comptime, current, |c| {
374 ctfe::ctfe_stmts(c, expr_arena, stmt_arena, interner)
375 });
376 current = fold::fold_stmts(current, expr_arena, stmt_arena, interner);
377 }
378 // Loop-carried CSE: hoist a squared term that a `While`'s escape guard
379 // computes over the freshly assigned loop iterate and the next iteration's
380 // body recomputes over the same value (mandelbrot's `zr*zr` / `zi*zi`) into a
381 // fresh per-iteration temp — computed ONCE just after the operand's `Set`,
382 // substituted into both the guard and the body recomputation. Runs after the
383 // mask&1 PE fixpoint (so identical sub-expressions are already canonical) and
384 // before GVN; fold+propagate refold the substituted forms. PROMOTED 2026-06-21:
385 // default ON (kill-switch LOGOS_RUN_LCSE=0) — mandelbrot -14% on the faithful
386 // interleaved A/B (2.40x->2.07x Node), 30/30 benchmarks bit-identical, no regress.
387 if admits_pinned(cfg, hs, tier, Opt::LoopCse) {
388 let (c, fired) =
389 loop_carried_cse::loop_carried_cse_stmts(current, expr_arena, stmt_arena, interner);
390 current = c;
391 if fired {
392 mark_fired(Opt::LoopCse);
393 current = fold::fold_stmts(current, expr_arena, stmt_arena, interner);
394 current = propagate::propagate_stmts(current, expr_arena, stmt_arena, interner);
395 }
396 }
397 // U6: float induction-variable strength reduction — replace a per-iteration
398 // `c1*k + c2` (a multiply + int->float cvtsi2sd) with a guarded incremental
399 // float accumulator. PROMOTED 2026-06-21: default ON (kill-switch
400 // LOGOS_RUN_FLOATSR=0) — pi_leibniz -31.7% on the faithful A/B (geomean
401 // 0.891->0.879), bit-identical, no regression (the runtime trip-count guard
402 // keeps it bit-identical for ALL inputs). Found via the stencil investigation.
403 if admits_pinned(cfg, hs, tier, Opt::FloatStrength) {
404 let (c, fired) = float_induction_sr::float_induction_sr_stmts(
405 current, expr_arena, stmt_arena, interner,
406 );
407 current = c;
408 if fired {
409 mark_fired(Opt::FloatStrength);
410 current = fold::fold_stmts(current, expr_arena, stmt_arena, interner);
411 current = propagate::propagate_stmts(current, expr_arena, stmt_arena, interner);
412 }
413 }
414 if admits_pinned(cfg, hs, tier, Opt::Cse) {
415 current = run_traced(Opt::Cse, current, |c| {
416 gvn::cse_stmts(c, expr_arena, stmt_arena, interner)
417 });
418 }
419 // Run-path array scalarization (the interpreter's SROA): fully unroll the
420 // constant-trip loops over a fixed-size float/int Seq, then replace the Seq
421 // with N scalar locals so every `item k of arr` is a register-resident
422 // variable read instead of a bounds-checked heap load. Unroll alone was a
423 // measured loss (a bigger region over the same loads); the scalarize half is
424 // what removes them. PROMOTED 2026-06-21: default ON (kill-switch
425 // LOGOS_RUN_SCALARIZE=0) — nbody -17% on the faithful interleaved A/B,
426 // 30/30 benchmarks bit-identical, no benchmark regressed.
427 //
428 // It runs AFTER GVN deliberately: GVN's value numbering is reload-correct on
429 // the array form (it never CSEs an `item k of arr` read across a write), but
430 // it would unsoundly CSE the loop-carried SCALAR reads this pass introduces
431 // across the step loop's mutations. Sequencing scalarization after GVN keeps
432 // GVN on the sound array form; the structural passes below (LICM, closed-form,
433 // deforestation, interval analysis, DCE) are reload-correct on the scalar form.
434 // Affine read-only array scalarization: delete a CSR-style offset array
435 // (graph_bfs's `adjStarts`, built by one affine `Push i*5 to adjStarts`
436 // loop) and substitute every `item k of arr` random heap load with the
437 // closed form `5*(k-1)` — C's `v*5` shift. Runs on the ARRAY form, BEFORE
438 // unroll/scalarize, so GVN (which already ran) stayed reload-correct over
439 // the original reads. fold+propagate after it collapse the substituted
440 // closed forms. PROMOTED 2026-06-21: default ON (kill-switch
441 // LOGOS_RUN_AFFINE=0) — graph_bfs -20% on the faithful interleaved A/B
442 // (2.86x->2.29x Node), 30/30 benchmarks bit-identical, no benchmark regressed.
443 #[cfg(feature = "codegen")]
444 if admits_pinned(cfg, hs, tier, Opt::Affine) {
445 let (a, ca) =
446 affine_scalarize::affine_scalarize_seqs(current, expr_arena, stmt_arena, interner);
447 current = a;
448 if ca {
449 mark_fired(Opt::Affine);
450 current = fold::fold_stmts(current, expr_arena, stmt_arena, interner);
451 current = propagate::propagate_stmts(current, expr_arena, stmt_arena, interner);
452 }
453 }
454 #[cfg(feature = "codegen")]
455 {
456 let mut changed = false;
457 if admits_pinned(cfg, hs, tier, Opt::Unroll) {
458 let (u, c1) = unroll::unroll_stmts_run(current, expr_arena, stmt_arena, interner);
459 current = u;
460 if c1 {
461 mark_fired(Opt::Unroll);
462 }
463 changed |= c1;
464 }
465 if admits_pinned(cfg, hs, tier, Opt::Scalarize) {
466 let (s, c2) = scalarize::scalarize_seqs(current, expr_arena, stmt_arena, interner);
467 current = s;
468 if c2 {
469 mark_fired(Opt::Scalarize);
470 }
471 changed |= c2;
472 }
473 if changed {
474 current = fold::fold_stmts(current, expr_arena, stmt_arena, interner);
475 current = propagate::propagate_stmts(current, expr_arena, stmt_arena, interner);
476 }
477 }
478 if admits_pinned(cfg, hs, tier, Opt::LoopHoist) {
479 current = run_traced(Opt::LoopHoist, current, |c| {
480 licm::licm_stmts_run(c, expr_arena, stmt_arena, interner)
481 });
482 }
483 if admits_pinned(cfg, hs, tier, Opt::ClosedForm) {
484 current = run_traced(Opt::ClosedForm, current, |c| {
485 closed_form::closed_form_stmts(c, expr_arena, stmt_arena, interner)
486 });
487 }
488 if admits_pinned(cfg, hs, tier, Opt::Fuse) {
489 current = run_traced(Opt::Fuse, current, |c| {
490 deforest::deforest_stmts(c, expr_arena, stmt_arena, interner)
491 });
492 }
493 if admits_pinned(cfg, hs, tier, Opt::Oracle) {
494 current = run_traced(Opt::Oracle, current, |c| {
495 abstract_interp::abstract_interp_stmts(c, expr_arena, stmt_arena)
496 });
497 }
498 if admits_pinned(cfg, hs, tier, Opt::DeadCode) {
499 current = run_traced(Opt::DeadCode, current, |c| {
500 dce::eliminate_dead_code(c, stmt_arena, expr_arena)
501 });
502 }
503 // Bounded recursion inlining (recursion unrolling) — the lone AOT pass we
504 // also run on the live run path. It flattens a self-recursive function's
505 // own loop-interleaved body k levels deep (the inline LLVM refuses, gcc
506 // -O3 performs), cutting the per-call VM overhead that dominates the
507 // recursion benchmarks. It MUST run LAST: placed before the fixpoint
508 // passes it re-analyses k enlarged clones and the optimizer cost balloons
509 // ~30×, so it runs on the already-optimized residual exactly like the AOT
510 // pipeline (each clone is pre-optimized). It defers to TCE/accumulator by
511 // construction — it fires only on loop-interleaved recursion, never on the
512 // return-position shape `tail_call` later rewrites to a constant-stack
513 // loop. Optimizer time lands inside the measured run, so it is gated twice:
514 // the mask bit (256), and a tight statement budget (`MAX_RUN_INLINE_STMTS`)
515 // below the broad run-opt gate — the recursive kernels are tiny source
516 // programs, while a large body's inlining cost would outweigh the savings.
517 const MAX_RUN_INLINE_STMTS: usize = 1_500;
518 if admits_pinned(cfg, hs, tier, Opt::Unfold) && current.len() <= MAX_RUN_INLINE_STMTS {
519 let unrolled = run_traced(Opt::Unfold, current, |c| {
520 inline_recursive::inline_recursive_fns_run(c, expr_arena, stmt_arena, interner)
521 });
522 // Collapse the argument-binding temps the splice introduces
523 // (`__rN_n = n`), exactly as the AOT pipeline does after this pass.
524 let folded = fold::fold_stmts(unrolled, expr_arena, stmt_arena, interner);
525 let propagated = propagate::propagate_stmts(folded, expr_arena, stmt_arena, interner);
526 current = dce::eliminate_dead_code(propagated, stmt_arena, expr_arena);
527 }
528 current
529}
530
531/// The AOT optimizer — ONE pipeline. The Futamura residual (fold, propagate,
532/// polyvariant PE, CTFE) then the ARCHITECT: equality saturation over a
533/// kernel-certified e-graph, Oracle facts gating the conditional rewrites
534/// (value facts suppressed for loop-mutated variables — see `egraph::convert`);
535/// then defunctionalization, deforestation, interval analysis, DCE, and
536/// supercompilation. GVN/LICM/closed-form STILL run after the e-graph until it
537/// subsumes their cross-statement and loop-recurrence reach (the EG waves).
538pub fn optimize_program<'a>(
539 stmts: Vec<Stmt<'a>>,
540 expr_arena: &'a Arena<Expr<'a>>,
541 stmt_arena: &'a Arena<Stmt<'a>>,
542 interner: &mut Interner,
543 cfg: &OptimizationConfig,
544) -> Vec<Stmt<'a>> {
545 set_active_config(*cfg);
546 clear_proven_raw_int_ops();
547 // De-desugar FIRST: under reference semantics the parser's place-write
548 // Splices fuse back to the direct nested writes every downstream pass
549 // (BTA, borrow-hoist, bounds elision) pattern-matches.
550 let stmts = splice_fuse::fuse_place_splices(stmts, expr_arena, stmt_arena, interner);
551 // NOTE: no early tiny-fn inliner here — that is a RUN-PATH device
552 // (optimize_for_run skips supercompile for budget). This AOT pipeline
553 // inlines through supercompile at the end exactly like v1, so the PE
554 // machinery keeps its subjects.
555 let bta_cache = bta::analyze_with_sccs(&stmts, interner);
556 let mut current = stmts;
557 if cfg.is_on(Opt::Specialize) {
558 let mut variant_count: HashMap<Symbol, usize> = HashMap::new();
559 let mut specialized_any = false;
560 for _ in 0..16 {
561 let folded = fold::fold_stmts(current, expr_arena, stmt_arena, interner);
562 let propagated = propagate::propagate_stmts(folded, expr_arena, stmt_arena, interner);
563 let (specialized, changes) = partial_eval::specialize_stmts_with_state(
564 propagated, expr_arena, stmt_arena, interner, &mut variant_count,
565 Some(&bta_cache),
566 );
567 current = specialized;
568 if changes == 0 {
569 break;
570 }
571 specialized_any = true;
572 }
573 if specialized_any {
574 mark_fired(Opt::Specialize);
575 }
576 }
577 if cfg.is_on(Opt::Comptime) {
578 current = run_traced(Opt::Comptime, current, |c| {
579 ctfe::ctfe_stmts(c, expr_arena, stmt_arena, interner)
580 });
581 }
582 current = fold::fold_stmts(current, expr_arena, stmt_arena, interner);
583 // Reflection symmetry-breaking: halve a bitmask counting search's first-row
584 // enumeration (×2 + odd-n middle) when the kernel proves the reflection
585 // invariance for all n. Runs on the clean specialized entry, before the
586 // structural passes; the sub-counts it calls still get popcount-leaf +
587 // recursion unrolling later. Fail-closed (kernel certificate gates it).
588 if cfg.is_on(Opt::Symmetry) {
589 current = run_traced(Opt::Symmetry, current, |c| {
590 symmetry::break_symmetry_stmts(c, expr_arena, stmt_arena, interner)
591 });
592 }
593 current = fold::fold_stmts(current, expr_arena, stmt_arena, interner);
594 // Fully unroll small constant-trip loops nested inside hot loops (nbody's
595 // per-step force loops), then refold/repropagate so the substituted indices
596 // collapse to constants (`item (3-1) of bx` → `bx[2]`) and the dead counter
597 // scaffolding is cleaned up. LLVM then SROAs the `[f64; N]` arrays into
598 // registers and vectorizes the straight-line body — the codegen it already
599 // produces for top-level constant loops but refuses inside a runtime loop.
600 #[cfg(feature = "codegen")]
601 if cfg.is_on(Opt::Unroll) {
602 let (unrolled, changed) = unroll::unroll_stmts(current, expr_arena, stmt_arena, interner);
603 current = unrolled;
604 if changed {
605 mark_fired(Opt::Unroll);
606 current = fold::fold_stmts(current, expr_arena, stmt_arena, interner);
607 current = propagate::propagate_stmts(current, expr_arena, stmt_arena, interner);
608 }
609 }
610 // Group 4: direct closures lift to first-order functions BEFORE the
611 // e-graph runs, so the Architect (and every later pass) sees plain
612 // calls instead of heap closures.
613 if cfg.is_on(Opt::Defunctionalize) {
614 current = run_traced(Opt::Defunctionalize, current, |c| {
615 defunctionalize::defunctionalize_stmts(c, expr_arena, stmt_arena, interner)
616 });
617 }
618 // The Architect saturates every expression under kernel-certified
619 // rewrites with Oracle-gated conditionals. GVN/LICM/closed-form STAY
620 // until the e-graph's cross-statement runs (M13's SSA versioning)
621 // subsume their capabilities — the structural suite pins them, and a
622 // capability superset is the honest sprint-22 wiring.
623 if cfg.is_on(Opt::Saturate) {
624 current = run_traced(Opt::Saturate, current, |c| {
625 egraph::convert::egraph_stmts(c, expr_arena, stmt_arena, interner)
626 });
627 }
628 if cfg.is_on(Opt::Cse) {
629 current = run_traced(Opt::Cse, current, |c| {
630 gvn::cse_stmts(c, expr_arena, stmt_arena, interner)
631 });
632 }
633 if cfg.is_on(Opt::LoopHoist) {
634 current = run_traced(Opt::LoopHoist, current, |c| {
635 licm::licm_stmts(c, expr_arena, stmt_arena, interner)
636 });
637 }
638 if cfg.is_on(Opt::ClosedForm) {
639 current = run_traced(Opt::ClosedForm, current, |c| {
640 closed_form::closed_form_stmts(c, expr_arena, stmt_arena, interner)
641 });
642 }
643 if cfg.is_on(Opt::Fuse) {
644 current = run_traced(Opt::Fuse, current, |c| {
645 deforest::deforest_stmts(c, expr_arena, stmt_arena, interner)
646 });
647 }
648 // Guard-based loop index-set splitting: split a counted loop carrying a
649 // top-level affine-monotone IV guard into a guard-false prefix (then-block
650 // dropped → memcpy) and a guard-true suffix (then-block inlined, branch-free
651 // → vectorizable unconditional load), behind a version guard that keeps the
652 // original loop for an out-of-range threshold. fold+propagate immediately
653 // after collapse the version guard / branches where statically resolvable,
654 // and the suffix's literal-threshold IV init lets abstract_interp re-prove
655 // the now-unconditional access in range. AOT-only (no SIMD payoff for the
656 // interpreter), so it is not in optimize_for_run / optimize_for_projection.
657 #[cfg(feature = "codegen")]
658 {
659 current = run_traced(Opt::LoopSplit, current, |c| {
660 loop_split::loop_split_stmts(c, expr_arena, stmt_arena, interner, cfg)
661 });
662 current = fold::fold_stmts(current, expr_arena, stmt_arena, interner);
663 current = propagate::propagate_stmts(current, expr_arena, stmt_arena, interner);
664 }
665 if cfg.is_on(Opt::Oracle) {
666 current = run_traced(Opt::Oracle, current, |c| {
667 abstract_interp::abstract_interp_stmts(c, expr_arena, stmt_arena)
668 });
669 }
670 let dced = if cfg.is_on(Opt::DeadCode) {
671 run_traced(Opt::DeadCode, current, |c| {
672 dce::eliminate_dead_code(c, stmt_arena, expr_arena)
673 })
674 } else {
675 current
676 };
677 let current = if cfg.is_on(Opt::Supercompile) {
678 run_traced(Opt::Supercompile, dced, |c| {
679 supercompile::supercompile_stmts(c, expr_arena, stmt_arena, interner)
680 })
681 } else {
682 dced
683 };
684 // Popcount-leaf: collapse the second-to-last level of a bitmask counting
685 // search (`row == n-1`, every remaining bit is one solution) into a single
686 // `count_ones`. Runs BEFORE inline_recursive so the fast path is carried
687 // into every unrolled clone. Pure structural rewrite (no kernel).
688 let current = run_traced(Opt::Popcount, current, |c| {
689 popcount_leaf::popcount_leaf_stmts(c, expr_arena, stmt_arena, interner, cfg)
690 });
691 // Bounded recursive inlining (recursion unrolling) runs LAST: flatten each
692 // self-recursive function's own body k levels deep — the inline LLVM
693 // refuses but gcc -O3 performs (the lever that lets compiled-LOGOS
694 // n-queens match/beat C). Placed after the heavy analyses (e-graph,
695 // supercompile) so they optimize the ORIGINAL body once and never pay to
696 // re-analyze the k enlarged copies — each clone is already optimized.
697 // fold+propagate+DCE then collapse the argument-binding temps the splice
698 // introduces (`__rN_n = n`).
699 let unrolled = if cfg.is_on(Opt::Unfold) {
700 run_traced(Opt::Unfold, current, |c| {
701 inline_recursive::inline_recursive_fns(c, expr_arena, stmt_arena, interner)
702 })
703 } else {
704 current
705 };
706 let folded = fold::fold_stmts(unrolled, expr_arena, stmt_arena, interner);
707 let propagated = propagate::propagate_stmts(folded, expr_arena, stmt_arena, interner);
708 // O9 — bound-versioned loop nests (spectral_norm's vectorization): runs
709 // LAST among the rewriters so nothing rebuilds its proven-raw-marked
710 // clone nodes before codegen. Versioning is guard-based loop cloning, the
711 // LoopSplit family; AOT-only like its sibling (the payoff is SIMD).
712 #[cfg(feature = "codegen")]
713 let propagated = if cfg.is_on(Opt::LoopSplit) {
714 run_traced(Opt::LoopSplit, propagated, |c| {
715 bound_version::bound_version_stmts(c, expr_arena, stmt_arena)
716 })
717 } else {
718 propagated
719 };
720 if cfg.is_on(Opt::DeadCode) {
721 run_traced(Opt::DeadCode, propagated, |c| {
722 dce::eliminate_dead_code(c, stmt_arena, expr_arena)
723 })
724 } else {
725 propagated
726 }
727}