Skip to main content

oxilean_codegen/opt_licm/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use crate::lcnf::*;
6use std::collections::HashSet;
7
8use super::types::{
9    HoistCandidate, InvariantPattern, LICMAnalysisCache, LICMConfig, LICMConstantFoldingHelper,
10    LICMDepGraph, LICMDominatorTree, LICMLivenessInfo, LICMPass, LICMPassConfig, LICMPassPhase,
11    LICMPassRegistry, LICMPassStats, LICMPhase, LICMProfileData, LICMReport, LICMWorklist,
12    LicmConfigExt, LicmHoistCandidate, LoopBodyComplexity, LoopInvariantExpr, LoopNode,
13    LoopStructure, LoopVersion, LoopVersioningConfig, PreheaderBlock, RedundantLoadInfo,
14};
15
16/// Collect the set of variables *used* (read) inside `expr`.
17pub(super) fn collect_used_vars(expr: &LcnfExpr, out: &mut HashSet<LcnfVarId>) {
18    match expr {
19        LcnfExpr::Let { value, body, .. } => {
20            collect_used_in_let_value(value, out);
21            collect_used_vars(body, out);
22        }
23        LcnfExpr::Case {
24            scrutinee,
25            alts,
26            default,
27            ..
28        } => {
29            out.insert(*scrutinee);
30            for alt in alts {
31                collect_used_vars(&alt.body, out);
32            }
33            if let Some(d) = default {
34                collect_used_vars(d, out);
35            }
36        }
37        LcnfExpr::Return(arg) => collect_used_in_arg(arg, out),
38        LcnfExpr::TailCall(f, args) => {
39            collect_used_in_arg(f, out);
40            for a in args {
41                collect_used_in_arg(a, out);
42            }
43        }
44        LcnfExpr::Unreachable => {}
45    }
46}
47pub(super) fn collect_used_in_arg(arg: &LcnfArg, out: &mut HashSet<LcnfVarId>) {
48    if let LcnfArg::Var(v) = arg {
49        out.insert(*v);
50    }
51}
52pub(super) fn collect_used_in_let_value(val: &LcnfLetValue, out: &mut HashSet<LcnfVarId>) {
53    match val {
54        LcnfLetValue::App(f, args) => {
55            collect_used_in_arg(f, out);
56            for a in args {
57                collect_used_in_arg(a, out);
58            }
59        }
60        LcnfLetValue::Proj(_, _, v) => {
61            out.insert(*v);
62        }
63        LcnfLetValue::Ctor(_, _, args) => {
64            for a in args {
65                collect_used_in_arg(a, out);
66            }
67        }
68        LcnfLetValue::FVar(v) => {
69            out.insert(*v);
70        }
71        LcnfLetValue::Reset(v) => {
72            out.insert(*v);
73        }
74        LcnfLetValue::Reuse(slot, _, _, args) => {
75            out.insert(*slot);
76            for a in args {
77                collect_used_in_arg(a, out);
78            }
79        }
80        LcnfLetValue::Lit(_) | LcnfLetValue::Erased => {}
81    }
82}
83/// Collect the set of variables used as **call targets** (function position in
84/// `App` / `TailCall`) inside `expr`.  This is a stricter variant of
85/// `collect_used_vars` that only picks up variables that are *called*, not
86/// those merely passed as arguments or returned.
87pub(super) fn collect_call_targets(expr: &LcnfExpr, out: &mut HashSet<LcnfVarId>) {
88    match expr {
89        LcnfExpr::Let { value, body, .. } => {
90            if let LcnfLetValue::App(LcnfArg::Var(v), _) = value {
91                out.insert(*v);
92            }
93            collect_call_targets(body, out);
94        }
95        LcnfExpr::Case { alts, default, .. } => {
96            for alt in alts {
97                collect_call_targets(&alt.body, out);
98            }
99            if let Some(d) = default {
100                collect_call_targets(d, out);
101            }
102        }
103        LcnfExpr::TailCall(LcnfArg::Var(v), _) => {
104            out.insert(*v);
105        }
106        _ => {}
107    }
108}
109/// Collect the set of variables *defined* (written) inside `expr`.
110pub(super) fn collect_defined_vars(expr: &LcnfExpr, out: &mut HashSet<LcnfVarId>) {
111    match expr {
112        LcnfExpr::Let { id, body, .. } => {
113            out.insert(*id);
114            collect_defined_vars(body, out);
115        }
116        LcnfExpr::Case { alts, default, .. } => {
117            for alt in alts {
118                collect_defined_vars(&alt.body, out);
119            }
120            if let Some(d) = default {
121                collect_defined_vars(d, out);
122            }
123        }
124        LcnfExpr::Return(_) | LcnfExpr::Unreachable | LcnfExpr::TailCall(..) => {}
125    }
126}
127/// Collect the free variables of a `LcnfLetValue`.
128pub(super) fn free_vars_of_let_value(val: &LcnfLetValue) -> HashSet<LcnfVarId> {
129    let mut out = HashSet::new();
130    collect_used_in_let_value(val, &mut out);
131    out
132}
133/// Remove let-bindings whose `id` is in `hoisted` from `expr` in-place.
134/// The binding itself is deleted; the body continues unchanged.
135pub(super) fn remove_hoisted_bindings(expr: &mut LcnfExpr, hoisted: &HashSet<LcnfVarId>) {
136    loop {
137        match expr {
138            LcnfExpr::Let { id, body, .. } if hoisted.contains(id) => {
139                let new_expr = std::mem::replace(body.as_mut(), LcnfExpr::Unreachable);
140                *expr = new_expr;
141            }
142            LcnfExpr::Let { body, .. } => {
143                remove_hoisted_bindings(body, hoisted);
144                break;
145            }
146            LcnfExpr::Case { alts, default, .. } => {
147                for alt in alts.iter_mut() {
148                    remove_hoisted_bindings(&mut alt.body, hoisted);
149                }
150                if let Some(d) = default {
151                    remove_hoisted_bindings(d, hoisted);
152                }
153                break;
154            }
155            LcnfExpr::Return(_) | LcnfExpr::Unreachable | LcnfExpr::TailCall(..) => break,
156        }
157    }
158}
159pub(super) fn var(n: u64) -> LcnfVarId {
160    LcnfVarId(n)
161}
162pub(super) fn lit_nat(n: u64) -> LcnfLetValue {
163    LcnfLetValue::Lit(LcnfLit::Nat(n))
164}
165pub(super) fn arg_var(n: u64) -> LcnfArg {
166    LcnfArg::Var(LcnfVarId(n))
167}
168pub(super) fn arg_lit(n: u64) -> LcnfArg {
169    LcnfArg::Lit(LcnfLit::Nat(n))
170}
171pub(super) fn make_decl(name: &str, body: LcnfExpr) -> LcnfFunDecl {
172    LcnfFunDecl {
173        name: name.to_string(),
174        params: vec![],
175        ret_type: LcnfType::Nat,
176        original_name: None,
177        is_recursive: false,
178        is_lifted: false,
179        inline_cost: 0,
180        body,
181    }
182}
183#[cfg(test)]
184mod tests {
185    use super::*;
186    /// Build a simple self-recursive let that models a counted loop.
187    ///
188    /// ```text
189    /// let loop_fn = <invariant: lit 42>
190    ///               let inner = <loop body: lit 1>
191    ///               return inner
192    /// ```
193    /// The variable `loop_fn` is used in the body to simulate recursion.
194    pub(super) fn build_loop_with_invariant() -> LcnfFunDecl {
195        let inner_body = LcnfExpr::Let {
196            id: var(2),
197            name: format!("x{}", var(2).0),
198            ty: LcnfType::Nat,
199            value: LcnfLetValue::Lit(LcnfLit::Nat(1)),
200            body: Box::new(LcnfExpr::Let {
201                id: var(3),
202                name: format!("x{}", var(3).0),
203                ty: LcnfType::Nat,
204                value: LcnfLetValue::App(arg_var(0), vec![arg_var(2)]),
205                body: Box::new(LcnfExpr::Return(arg_var(3))),
206            }),
207        };
208        let body_with_inv = LcnfExpr::Let {
209            id: var(1),
210            name: format!("x{}", var(1).0),
211            ty: LcnfType::Nat,
212            value: LcnfLetValue::Lit(LcnfLit::Nat(42)),
213            body: Box::new(inner_body),
214        };
215        let body = LcnfExpr::Let {
216            id: var(0),
217            name: format!("x{}", var(0).0),
218            ty: LcnfType::Nat,
219            value: LcnfLetValue::Lit(LcnfLit::Nat(0)),
220            body: Box::new(body_with_inv),
221        };
222        make_decl("loop_test", body)
223    }
224    #[test]
225    pub(super) fn test_licm_default_config() {
226        let cfg = LICMConfig::default();
227        assert_eq!(cfg.min_savings_threshold, 0);
228        assert!(!cfg.hoist_function_calls);
229    }
230    #[test]
231    pub(super) fn test_licm_report_default() {
232        let r = LICMReport::default();
233        assert_eq!(r.loops_analyzed, 0);
234        assert_eq!(r.expressions_hoisted, 0);
235        assert_eq!(r.estimated_savings, 0);
236    }
237    #[test]
238    pub(super) fn test_licm_report_merge() {
239        let mut r1 = LICMReport {
240            loops_analyzed: 2,
241            expressions_hoisted: 3,
242            estimated_savings: 30,
243        };
244        let r2 = LICMReport {
245            loops_analyzed: 1,
246            expressions_hoisted: 2,
247            estimated_savings: 20,
248        };
249        r1.merge(&r2);
250        assert_eq!(r1.loops_analyzed, 3);
251        assert_eq!(r1.expressions_hoisted, 5);
252        assert_eq!(r1.estimated_savings, 50);
253    }
254    #[test]
255    pub(super) fn test_find_loops_empty_decl() {
256        let decl = make_decl("empty", LcnfExpr::Return(arg_lit(0)));
257        let pass = LICMPass::default();
258        let loops = pass.find_loops(&decl);
259        assert!(loops.is_empty());
260    }
261    #[test]
262    pub(super) fn test_find_loops_detects_self_recursive() {
263        let decl = build_loop_with_invariant();
264        let pass = LICMPass::default();
265        let loops = pass.find_loops(&decl);
266        assert!(!loops.is_empty());
267        assert_eq!(loops[0].header, var(0));
268    }
269    #[test]
270    pub(super) fn test_is_loop_invariant_literal() {
271        let pass = LICMPass::default();
272        let lp = LoopStructure {
273            header: var(0),
274            body_vars: vec![var(1), var(2)].into_iter().collect(),
275            exit_vars: HashSet::new(),
276            nest_depth: 0,
277        };
278        assert!(pass.is_loop_invariant(&LcnfLetValue::Lit(LcnfLit::Nat(7)), &lp));
279    }
280    #[test]
281    pub(super) fn test_is_loop_invariant_var_inside_loop() {
282        let pass = LICMPass::default();
283        let lp = LoopStructure {
284            header: var(0),
285            body_vars: vec![var(1)].into_iter().collect(),
286            exit_vars: HashSet::new(),
287            nest_depth: 0,
288        };
289        assert!(!pass.is_loop_invariant(&LcnfLetValue::FVar(var(1)), &lp));
290    }
291    #[test]
292    pub(super) fn test_is_loop_invariant_var_outside_loop() {
293        let pass = LICMPass::default();
294        let lp = LoopStructure {
295            header: var(0),
296            body_vars: vec![var(1)].into_iter().collect(),
297            exit_vars: HashSet::new(),
298            nest_depth: 0,
299        };
300        assert!(pass.is_loop_invariant(&LcnfLetValue::FVar(var(99)), &lp));
301    }
302    #[test]
303    pub(super) fn test_is_loop_invariant_call_blocked_by_config() {
304        let pass = LICMPass::default();
305        let lp = LoopStructure {
306            header: var(0),
307            body_vars: HashSet::new(),
308            exit_vars: HashSet::new(),
309            nest_depth: 0,
310        };
311        let val = LcnfLetValue::App(LcnfArg::Lit(LcnfLit::Nat(0)), vec![]);
312        assert!(!pass.is_loop_invariant(&val, &lp));
313    }
314    #[test]
315    pub(super) fn test_is_loop_invariant_call_allowed_by_config() {
316        let mut cfg = LICMConfig::default();
317        cfg.hoist_function_calls = true;
318        let pass = LICMPass::new(cfg);
319        let lp = LoopStructure {
320            header: var(0),
321            body_vars: HashSet::new(),
322            exit_vars: HashSet::new(),
323            nest_depth: 0,
324        };
325        let val = LcnfLetValue::App(LcnfArg::Lit(LcnfLit::Nat(0)), vec![]);
326        assert!(pass.is_loop_invariant(&val, &lp));
327    }
328    #[test]
329    pub(super) fn test_is_loop_invariant_recursive_call_never_hoisted() {
330        let mut cfg = LICMConfig::default();
331        cfg.hoist_function_calls = true;
332        let pass = LICMPass::new(cfg);
333        let lp = LoopStructure {
334            header: var(0),
335            body_vars: HashSet::new(),
336            exit_vars: HashSet::new(),
337            nest_depth: 0,
338        };
339        let val = LcnfLetValue::App(LcnfArg::Var(var(0)), vec![]);
340        assert!(!pass.is_loop_invariant(&val, &lp));
341    }
342    #[test]
343    pub(super) fn test_hoist_invariants_empty_loop() {
344        let mut decl = make_decl("empty", LcnfExpr::Return(arg_lit(0)));
345        let lp = LoopStructure {
346            header: var(0),
347            body_vars: HashSet::new(),
348            exit_vars: HashSet::new(),
349            nest_depth: 0,
350        };
351        let mut pass = LICMPass::default();
352        pass.hoist_invariants(&mut decl, &lp);
353        assert_eq!(pass.report().expressions_hoisted, 0);
354    }
355    #[test]
356    pub(super) fn test_run_no_loops() {
357        let mut decls = vec![make_decl("simple", LcnfExpr::Return(arg_lit(1)))];
358        let mut pass = LICMPass::default();
359        pass.run(&mut decls);
360        let r = pass.report();
361        assert_eq!(r.loops_analyzed, 0);
362        assert_eq!(r.expressions_hoisted, 0);
363    }
364    #[test]
365    pub(super) fn test_run_hoists_literal_from_loop() {
366        let mut decl = build_loop_with_invariant();
367        let pass_detect = LICMPass::default();
368        let loops = pass_detect.find_loops(&decl);
369        let mut pass = LICMPass::default();
370        for lp in &loops {
371            pass.hoist_invariants(&mut decl, lp);
372        }
373        let r = pass.report();
374        assert!(r.expressions_hoisted >= 1);
375    }
376    #[test]
377    pub(super) fn test_loop_structure_nest_depth() {
378        let decl = build_loop_with_invariant();
379        let pass = LICMPass::default();
380        let loops = pass.find_loops(&decl);
381        for lp in &loops {
382            assert_eq!(lp.nest_depth, 0);
383        }
384    }
385    #[test]
386    pub(super) fn test_hoist_candidate_savings_estimate() {
387        let candidate = HoistCandidate {
388            expr: LoopInvariantExpr {
389                var: var(5),
390                value: lit_nat(99),
391                ty: LcnfType::Nat,
392                loop_depth: 0,
393            },
394            target_loop_header: var(0),
395            savings_estimate: 10,
396        };
397        assert_eq!(candidate.savings_estimate, 10);
398    }
399    #[test]
400    pub(super) fn test_preheader_block_construction() {
401        let inv = LoopInvariantExpr {
402            var: var(5),
403            value: lit_nat(99),
404            ty: LcnfType::Nat,
405            loop_depth: 0,
406        };
407        let pb = PreheaderBlock {
408            loop_header: var(0),
409            preheader_lets: vec![inv.clone()],
410        };
411        assert_eq!(pb.preheader_lets.len(), 1);
412        assert_eq!(pb.loop_header, var(0));
413    }
414    #[test]
415    pub(super) fn test_collect_used_vars_return() {
416        let expr = LcnfExpr::Return(arg_var(7));
417        let mut used = HashSet::new();
418        collect_used_vars(&expr, &mut used);
419        assert!(used.contains(&var(7)));
420    }
421    #[test]
422    pub(super) fn test_collect_defined_vars_let() {
423        let expr = LcnfExpr::Let {
424            id: var(3),
425            name: format!("x{}", var(3).0),
426            ty: LcnfType::Nat,
427            value: lit_nat(0),
428            body: Box::new(LcnfExpr::Return(arg_lit(0))),
429        };
430        let mut defined = HashSet::new();
431        collect_defined_vars(&expr, &mut defined);
432        assert!(defined.contains(&var(3)));
433    }
434    #[test]
435    pub(super) fn test_remove_hoisted_bindings() {
436        let mut expr = LcnfExpr::Let {
437            id: var(1),
438            name: format!("x{}", var(1).0),
439            ty: LcnfType::Nat,
440            value: lit_nat(42),
441            body: Box::new(LcnfExpr::Return(arg_var(1))),
442        };
443        let mut hoisted = HashSet::new();
444        hoisted.insert(var(1));
445        remove_hoisted_bindings(&mut expr, &hoisted);
446        assert!(matches!(expr, LcnfExpr::Return(_)));
447    }
448    #[test]
449    pub(super) fn test_run_multiple_decls() {
450        let mut decls = vec![
451            make_decl("a", LcnfExpr::Return(arg_lit(0))),
452            make_decl("b", LcnfExpr::Return(arg_lit(1))),
453        ];
454        let mut pass = LICMPass::default();
455        pass.run(&mut decls);
456        assert_eq!(pass.report().loops_analyzed, 0);
457    }
458}
459/// Try to recognize the invariant pattern of a `LcnfLetValue`, given the
460/// set of variables defined inside the loop.
461#[allow(dead_code)]
462pub fn recognize_invariant_pattern(
463    val: &LcnfLetValue,
464    body_vars: &HashSet<LcnfVarId>,
465) -> Option<InvariantPattern> {
466    match val {
467        LcnfLetValue::Lit(_) => Some(InvariantPattern::Literal),
468        LcnfLetValue::Erased => Some(InvariantPattern::Erased),
469        LcnfLetValue::FVar(v) => {
470            if !body_vars.contains(v) {
471                Some(InvariantPattern::ExternalFVar)
472            } else {
473                None
474            }
475        }
476        LcnfLetValue::Proj(_, _, base) => {
477            if !body_vars.contains(base) {
478                Some(InvariantPattern::ExternalProj)
479            } else {
480                None
481            }
482        }
483        LcnfLetValue::Ctor(_, _, args) => {
484            let all_external = args.iter().all(|a| match a {
485                LcnfArg::Var(v) => !body_vars.contains(v),
486                LcnfArg::Lit(_) | LcnfArg::Erased | LcnfArg::Type(_) => true,
487            });
488            if all_external {
489                Some(InvariantPattern::InvariantCtor)
490            } else {
491                None
492            }
493        }
494        _ => None,
495    }
496}
497/// Summarise a `LICMReport` as a human-readable string.
498#[allow(dead_code)]
499pub fn format_licm_report(report: &LICMReport) -> String {
500    format!(
501        "LICM: {} loops analysed, {} expressions hoisted, {} estimated savings",
502        report.loops_analyzed, report.expressions_hoisted, report.estimated_savings
503    )
504}
505/// Returns `true` if the LICM report shows any improvement was made.
506#[allow(dead_code)]
507pub fn licm_made_changes(report: &LICMReport) -> bool {
508    report.expressions_hoisted > 0
509}
510#[cfg(test)]
511mod licm_utility_tests {
512    use super::*;
513    use crate::opt_licm::*;
514    #[test]
515    pub(super) fn test_redundant_load_info_new() {
516        let info = RedundantLoadInfo::new();
517        assert_eq!(info.redundant_count, 0);
518        assert!(info.available_loads.is_empty());
519    }
520    #[test]
521    pub(super) fn test_redundant_load_register_and_lookup() {
522        let mut info = RedundantLoadInfo::new();
523        info.register_load(LcnfVarId(1), 0, LcnfVarId(2));
524        assert_eq!(info.lookup_load(LcnfVarId(1), 0), Some(LcnfVarId(2)));
525        assert_eq!(info.lookup_load(LcnfVarId(1), 1), None);
526    }
527    #[test]
528    pub(super) fn test_redundant_load_analysis_proj() {
529        let mut info = RedundantLoadInfo::new();
530        let expr = LcnfExpr::Let {
531            id: LcnfVarId(2),
532            name: "p1".to_string(),
533            ty: LcnfType::Nat,
534            value: LcnfLetValue::Proj("0".to_string(), 0, LcnfVarId(1)),
535            body: Box::new(LcnfExpr::Let {
536                id: LcnfVarId(3),
537                name: "p2".to_string(),
538                ty: LcnfType::Nat,
539                value: LcnfLetValue::Proj("0".to_string(), 0, LcnfVarId(1)),
540                body: Box::new(LcnfExpr::Return(LcnfArg::Var(LcnfVarId(3)))),
541            }),
542        };
543        info.analyze(&expr);
544        assert_eq!(info.redundant_count, 1);
545    }
546    #[test]
547    pub(super) fn test_loop_body_complexity_empty_return() {
548        let expr = LcnfExpr::Return(LcnfArg::Lit(LcnfLit::Nat(0)));
549        let c = LoopBodyComplexity::compute(&expr);
550        assert_eq!(c.let_count, 0);
551        assert_eq!(c.case_count, 0);
552        assert_eq!(c.score(), 0);
553    }
554    #[test]
555    pub(super) fn test_loop_body_complexity_let_chain() {
556        let expr = LcnfExpr::Let {
557            id: LcnfVarId(1),
558            name: "a".to_string(),
559            ty: LcnfType::Nat,
560            value: LcnfLetValue::Lit(LcnfLit::Nat(1)),
561            body: Box::new(LcnfExpr::Let {
562                id: LcnfVarId(2),
563                name: "b".to_string(),
564                ty: LcnfType::Nat,
565                value: LcnfLetValue::App(
566                    LcnfArg::Var(LcnfVarId(1)),
567                    vec![LcnfArg::Var(LcnfVarId(1))],
568                ),
569                body: Box::new(LcnfExpr::Return(LcnfArg::Var(LcnfVarId(2)))),
570            }),
571        };
572        let c = LoopBodyComplexity::compute(&expr);
573        assert_eq!(c.let_count, 2);
574        assert_eq!(c.app_count, 1);
575        assert!(c.score() > 0);
576    }
577    #[test]
578    pub(super) fn test_loop_body_complexity_nested_case() {
579        let inner_case = LcnfExpr::Case {
580            scrutinee: LcnfVarId(1),
581            scrutinee_ty: LcnfType::Nat,
582            alts: vec![],
583            default: Some(Box::new(LcnfExpr::Return(LcnfArg::Lit(LcnfLit::Nat(0))))),
584        };
585        let outer = LcnfExpr::Case {
586            scrutinee: LcnfVarId(0),
587            scrutinee_ty: LcnfType::Nat,
588            alts: vec![crate::lcnf::LcnfAlt {
589                ctor_name: "A".to_string(),
590                ctor_tag: 0,
591                params: vec![],
592                body: inner_case,
593            }],
594            default: None,
595        };
596        let c = LoopBodyComplexity::compute(&outer);
597        assert_eq!(c.case_count, 2);
598        assert_eq!(c.max_case_depth, 2);
599    }
600    #[test]
601    pub(super) fn test_recognize_invariant_pattern_literal() {
602        let body_vars: HashSet<LcnfVarId> = HashSet::new();
603        let val = LcnfLetValue::Lit(LcnfLit::Nat(7));
604        assert!(matches!(
605            recognize_invariant_pattern(&val, &body_vars),
606            Some(InvariantPattern::Literal)
607        ));
608    }
609    #[test]
610    pub(super) fn test_recognize_invariant_pattern_erased() {
611        let body_vars: HashSet<LcnfVarId> = HashSet::new();
612        assert!(matches!(
613            recognize_invariant_pattern(&LcnfLetValue::Erased, &body_vars),
614            Some(InvariantPattern::Erased)
615        ));
616    }
617    #[test]
618    pub(super) fn test_recognize_invariant_pattern_external_fvar() {
619        let body_vars: HashSet<LcnfVarId> = vec![LcnfVarId(1)].into_iter().collect();
620        let val = LcnfLetValue::FVar(LcnfVarId(99));
621        assert!(matches!(
622            recognize_invariant_pattern(&val, &body_vars),
623            Some(InvariantPattern::ExternalFVar)
624        ));
625        let val2 = LcnfLetValue::FVar(LcnfVarId(1));
626        assert!(recognize_invariant_pattern(&val2, &body_vars).is_none());
627    }
628    #[test]
629    pub(super) fn test_recognize_invariant_pattern_external_proj() {
630        let body_vars: HashSet<LcnfVarId> = vec![LcnfVarId(1)].into_iter().collect();
631        let val = LcnfLetValue::Proj("0".to_string(), 0, LcnfVarId(50));
632        assert!(matches!(
633            recognize_invariant_pattern(&val, &body_vars),
634            Some(InvariantPattern::ExternalProj)
635        ));
636    }
637    #[test]
638    pub(super) fn test_recognize_invariant_pattern_ctor_all_external() {
639        let body_vars: HashSet<LcnfVarId> = HashSet::new();
640        let val = LcnfLetValue::Ctor(
641            "T".to_string(),
642            0,
643            vec![LcnfArg::Lit(LcnfLit::Nat(1)), LcnfArg::Lit(LcnfLit::Nat(2))],
644        );
645        assert!(matches!(
646            recognize_invariant_pattern(&val, &body_vars),
647            Some(InvariantPattern::InvariantCtor)
648        ));
649    }
650    #[test]
651    pub(super) fn test_recognize_invariant_pattern_ctor_internal_arg() {
652        let body_vars: HashSet<LcnfVarId> = vec![LcnfVarId(5)].into_iter().collect();
653        let val = LcnfLetValue::Ctor("T".to_string(), 0, vec![LcnfArg::Var(LcnfVarId(5))]);
654        assert!(recognize_invariant_pattern(&val, &body_vars).is_none());
655    }
656    #[test]
657    pub(super) fn test_format_licm_report() {
658        let r = LICMReport {
659            loops_analyzed: 3,
660            expressions_hoisted: 5,
661            estimated_savings: 50,
662        };
663        let s = format_licm_report(&r);
664        assert!(s.contains("3 loops"));
665        assert!(s.contains("5 expressions"));
666        assert!(s.contains("50 estimated"));
667    }
668    #[test]
669    pub(super) fn test_licm_made_changes_true() {
670        let r = LICMReport {
671            loops_analyzed: 1,
672            expressions_hoisted: 2,
673            estimated_savings: 20,
674        };
675        assert!(licm_made_changes(&r));
676    }
677    #[test]
678    pub(super) fn test_licm_made_changes_false() {
679        let r = LICMReport::default();
680        assert!(!licm_made_changes(&r));
681    }
682    #[test]
683    pub(super) fn test_preheader_block_empty() {
684        let pb = PreheaderBlock {
685            loop_header: LcnfVarId(0),
686            preheader_lets: vec![],
687        };
688        let inner = LcnfExpr::Return(LcnfArg::Lit(LcnfLit::Nat(42)));
689        let result = materialize_preheader(&pb, inner.clone());
690        assert_eq!(result, inner);
691    }
692    #[test]
693    pub(super) fn test_loop_nest_info_multiple_depths() {
694        let loops = vec![
695            LoopStructure {
696                header: LcnfVarId(0),
697                body_vars: HashSet::new(),
698                exit_vars: HashSet::new(),
699                nest_depth: 0,
700            },
701            LoopStructure {
702                header: LcnfVarId(1),
703                body_vars: vec![LcnfVarId(10)].into_iter().collect(),
704                exit_vars: HashSet::new(),
705                nest_depth: 1,
706            },
707            LoopStructure {
708                header: LcnfVarId(2),
709                body_vars: vec![LcnfVarId(20), LcnfVarId(21)].into_iter().collect(),
710                exit_vars: HashSet::new(),
711                nest_depth: 2,
712            },
713        ];
714        let info = LoopNestInfo::from_loops(loops);
715        assert_eq!(info.max_depth, 2);
716        assert_eq!(info.total_body_vars, 3);
717    }
718}
719#[cfg(test)]
720mod licm_phase_tests {
721    use super::*;
722    use crate::opt_licm::*;
723    #[test]
724    pub(super) fn test_loop_version_structure() {
725        let v = LoopVersion {
726            cond_var: LcnfVarId(10),
727            fast_path_header: LcnfVarId(20),
728            slow_path_header: LcnfVarId(30),
729        };
730        assert_eq!(v.cond_var, LcnfVarId(10));
731        assert_eq!(v.fast_path_header, LcnfVarId(20));
732        assert_eq!(v.slow_path_header, LcnfVarId(30));
733    }
734    #[test]
735    pub(super) fn test_loop_versioning_config_conservative() {
736        let cfg = LoopVersioningConfig::conservative();
737        assert_eq!(cfg.max_versions, 2);
738        assert!((cfg.min_speedup_ratio - 1.5).abs() < 0.001);
739    }
740    #[test]
741    pub(super) fn test_licm_profile_data_new() {
742        let pd = LICMProfileData::new();
743        assert!(pd.loop_counts.is_empty());
744        assert!(pd.binding_counts.is_empty());
745    }
746    #[test]
747    pub(super) fn test_licm_profile_data_record_and_query() {
748        let mut pd = LICMProfileData::new();
749        pd.record_loop(LcnfVarId(0), 1000);
750        assert_eq!(pd.loop_count(LcnfVarId(0)), 1000);
751        assert_eq!(pd.loop_count(LcnfVarId(99)), 1);
752    }
753    #[test]
754    pub(super) fn test_licm_profile_data_dynamic_savings() {
755        let mut pd = LICMProfileData::new();
756        pd.record_loop(LcnfVarId(0), 50);
757        let candidate = HoistCandidate {
758            expr: LoopInvariantExpr {
759                var: LcnfVarId(1),
760                value: LcnfLetValue::Erased,
761                ty: LcnfType::Nat,
762                loop_depth: 0,
763            },
764            target_loop_header: LcnfVarId(0),
765            savings_estimate: 10,
766        };
767        assert_eq!(pd.dynamic_savings(&candidate), 500);
768    }
769    #[test]
770    pub(super) fn test_licm_phase_display() {
771        assert_eq!(LICMPhase::LICMBeforeCSE.to_string(), "LICM-before-CSE");
772        assert_eq!(LICMPhase::LICMAfterCSE.to_string(), "LICM-after-CSE");
773        assert_eq!(LICMPhase::LICMIterative.to_string(), "LICM-iterative");
774        assert_eq!(LICMPhase::LICMOnce.to_string(), "LICM-once");
775    }
776    #[test]
777    pub(super) fn test_licm_phase_equality() {
778        assert_eq!(LICMPhase::LICMOnce, LICMPhase::LICMOnce);
779        assert_ne!(LICMPhase::LICMOnce, LICMPhase::LICMIterative);
780    }
781    #[test]
782    pub(super) fn test_loop_versioning_config_default() {
783        let cfg = LoopVersioningConfig::default();
784        assert_eq!(cfg.max_versions, 0);
785    }
786    #[test]
787    pub(super) fn test_licm_profile_data_record_binding() {
788        let mut pd = LICMProfileData::new();
789        pd.record_binding(LcnfVarId(5), 200);
790        assert_eq!(
791            *pd.binding_counts
792                .get(&LcnfVarId(5))
793                .expect("value should be present in map"),
794            200
795        );
796    }
797    #[test]
798    pub(super) fn test_redundant_load_multiple_fields() {
799        let mut info = RedundantLoadInfo::new();
800        info.register_load(LcnfVarId(1), 0, LcnfVarId(10));
801        info.register_load(LcnfVarId(1), 1, LcnfVarId(11));
802        info.register_load(LcnfVarId(2), 0, LcnfVarId(12));
803        assert_eq!(info.lookup_load(LcnfVarId(1), 0), Some(LcnfVarId(10)));
804        assert_eq!(info.lookup_load(LcnfVarId(1), 1), Some(LcnfVarId(11)));
805        assert_eq!(info.lookup_load(LcnfVarId(2), 0), Some(LcnfVarId(12)));
806        assert_eq!(info.lookup_load(LcnfVarId(2), 1), None);
807    }
808    #[test]
809    pub(super) fn test_loop_body_complexity_case_with_apps() {
810        let expr = LcnfExpr::Case {
811            scrutinee: LcnfVarId(0),
812            scrutinee_ty: LcnfType::Nat,
813            alts: vec![crate::lcnf::LcnfAlt {
814                ctor_name: "A".to_string(),
815                ctor_tag: 0,
816                params: vec![],
817                body: LcnfExpr::Let {
818                    id: LcnfVarId(1),
819                    name: "r".to_string(),
820                    ty: LcnfType::Nat,
821                    value: LcnfLetValue::App(LcnfArg::Lit(LcnfLit::Nat(0)), vec![]),
822                    body: Box::new(LcnfExpr::Return(LcnfArg::Var(LcnfVarId(1)))),
823                },
824            }],
825            default: None,
826        };
827        let c = LoopBodyComplexity::compute(&expr);
828        assert_eq!(c.case_count, 1);
829        assert_eq!(c.let_count, 1);
830        assert_eq!(c.app_count, 1);
831    }
832    #[test]
833    pub(super) fn test_topo_sort_two_independent() {
834        let c1 = HoistCandidate {
835            expr: LoopInvariantExpr {
836                var: LcnfVarId(1),
837                value: LcnfLetValue::Lit(LcnfLit::Nat(1)),
838                ty: LcnfType::Nat,
839                loop_depth: 0,
840            },
841            target_loop_header: LcnfVarId(0),
842            savings_estimate: 5,
843        };
844        let c2 = HoistCandidate {
845            expr: LoopInvariantExpr {
846                var: LcnfVarId(2),
847                value: LcnfLetValue::Lit(LcnfLit::Nat(2)),
848                ty: LcnfType::Nat,
849                loop_depth: 0,
850            },
851            target_loop_header: LcnfVarId(0),
852            savings_estimate: 5,
853        };
854        let sorted = topo_sort_candidates(&[c1, c2]);
855        assert_eq!(sorted.len(), 2);
856        let vars: Vec<LcnfVarId> = sorted.iter().map(|c| c.expr.var).collect();
857        assert!(vars.contains(&LcnfVarId(1)));
858        assert!(vars.contains(&LcnfVarId(2)));
859    }
860    #[test]
861    pub(super) fn test_topo_sort_dependent_pair() {
862        let c1 = HoistCandidate {
863            expr: LoopInvariantExpr {
864                var: LcnfVarId(1),
865                value: LcnfLetValue::Lit(LcnfLit::Nat(42)),
866                ty: LcnfType::Nat,
867                loop_depth: 0,
868            },
869            target_loop_header: LcnfVarId(0),
870            savings_estimate: 5,
871        };
872        let c2 = HoistCandidate {
873            expr: LoopInvariantExpr {
874                var: LcnfVarId(2),
875                value: LcnfLetValue::FVar(LcnfVarId(1)),
876                ty: LcnfType::Nat,
877                loop_depth: 0,
878            },
879            target_loop_header: LcnfVarId(0),
880            savings_estimate: 5,
881        };
882        let sorted = topo_sort_candidates(&[c1, c2]);
883        assert_eq!(sorted.len(), 2);
884        let pos1 = sorted
885            .iter()
886            .position(|c| c.expr.var == LcnfVarId(1))
887            .expect("operation should succeed");
888        let pos2 = sorted
889            .iter()
890            .position(|c| c.expr.var == LcnfVarId(2))
891            .expect("operation should succeed");
892        assert!(pos1 < pos2, "producer must precede consumer");
893    }
894    #[test]
895    pub(super) fn test_licm_pass_v2_with_heuristics() {
896        let mut pass = LICMPassV2::new();
897        pass.heuristics.max_hoist_cost = 0;
898        let mut decl = make_decl(
899            "heuristic_test",
900            LcnfExpr::Return(LcnfArg::Lit(LcnfLit::Nat(1))),
901        );
902        pass.run(std::slice::from_mut(&mut decl));
903        assert_eq!(pass.report().loops_analyzed, 0);
904    }
905    #[test]
906    pub(super) fn test_collect_used_vars_tailcall() {
907        let expr = LcnfExpr::TailCall(
908            LcnfArg::Var(LcnfVarId(1)),
909            vec![LcnfArg::Var(LcnfVarId(2)), LcnfArg::Lit(LcnfLit::Nat(0))],
910        );
911        let mut used = HashSet::new();
912        collect_used_vars(&expr, &mut used);
913        assert!(used.contains(&LcnfVarId(1)));
914        assert!(used.contains(&LcnfVarId(2)));
915    }
916    #[test]
917    pub(super) fn test_collect_defined_vars_case() {
918        let expr = LcnfExpr::Case {
919            scrutinee: LcnfVarId(0),
920            scrutinee_ty: LcnfType::Nat,
921            alts: vec![crate::lcnf::LcnfAlt {
922                ctor_name: "A".to_string(),
923                ctor_tag: 0,
924                params: vec![],
925                body: LcnfExpr::Let {
926                    id: LcnfVarId(10),
927                    name: "x".to_string(),
928                    ty: LcnfType::Nat,
929                    value: LcnfLetValue::Lit(LcnfLit::Nat(0)),
930                    body: Box::new(LcnfExpr::Return(LcnfArg::Var(LcnfVarId(10)))),
931                },
932            }],
933            default: None,
934        };
935        let mut defined = HashSet::new();
936        collect_defined_vars(&expr, &mut defined);
937        assert!(defined.contains(&LcnfVarId(10)));
938    }
939}
940/// LICM pass version
941#[allow(dead_code)]
942pub const LICM_PASS_VERSION: &str = "1.0.0";
943/// LICM loop invariant check helper
944#[allow(dead_code)]
945pub fn licm_is_loop_invariant_const(value: i64) -> bool {
946    let _ = value;
947    true
948}
949/// LICM hoist decision helper
950#[allow(dead_code)]
951pub fn licm_should_hoist(candidate: &LicmHoistCandidate, config: &LicmConfigExt) -> bool {
952    if !config.enable_hoist {
953        return false;
954    }
955    if !candidate.is_side_effect_free && !config.hoist_stores {
956        return false;
957    }
958    if candidate.cost > config.max_hoist_cost {
959        return false;
960    }
961    true
962}
963/// LICM trip count estimation
964#[allow(dead_code)]
965pub fn licm_estimate_trip_count(loop_node: &LoopNode) -> Option<u64> {
966    loop_node.trip_count
967}
968/// LICM benefit estimator
969#[allow(dead_code)]
970pub fn licm_hoist_benefit(candidate: &LicmHoistCandidate, trip_count: u64) -> i64 {
971    let savings_per_iter = candidate.cost as i64;
972    let total_savings = savings_per_iter * trip_count as i64;
973    total_savings - savings_per_iter
974}
975/// LICM version info
976#[allow(dead_code)]
977pub const LICM_VERSION_INFO: &str = "licm-pass-1.0.0";
978/// LICM default max hoist cost
979#[allow(dead_code)]
980pub const LICM_DEFAULT_MAX_HOIST_COST: i32 = 100;
981/// LICM min profitable trip count
982#[allow(dead_code)]
983pub const LICM_MIN_TRIP_COUNT: u64 = 2;
984/// LICM loop depth-1 invariant check
985#[allow(dead_code)]
986pub fn licm_is_trivially_invariant(var: u32, loop_defs: &std::collections::HashSet<u32>) -> bool {
987    !loop_defs.contains(&var)
988}
989/// LICM safe to hoist check
990#[allow(dead_code)]
991pub fn licm_is_safe_to_hoist(inst_id: u32, has_side_effects: bool, aliases_loop_mem: bool) -> bool {
992    let _ = inst_id;
993    !has_side_effects && !aliases_loop_mem
994}
995/// LICM loop-independent variable detection
996#[allow(dead_code)]
997pub fn licm_all_operands_invariant(
998    operands: &[u32],
999    invariants: &std::collections::HashSet<u32>,
1000) -> bool {
1001    operands.iter().all(|op| invariants.contains(op))
1002}
1003/// LICM loop exit block collector
1004#[allow(dead_code)]
1005pub fn licm_collect_loop_exits(
1006    loop_node: &LoopNode,
1007    cfg_succs: &std::collections::HashMap<u32, Vec<u32>>,
1008) -> Vec<u32> {
1009    let body: std::collections::HashSet<u32> = loop_node.body_blocks.iter().cloned().collect();
1010    let mut exits = Vec::new();
1011    for &blk in &loop_node.body_blocks {
1012        if let Some(succs) = cfg_succs.get(&blk) {
1013            for &s in succs {
1014                if !body.contains(&s) {
1015                    exits.push(s);
1016                }
1017            }
1018        }
1019    }
1020    exits
1021}
1022/// LICM dominated-by check (simplified)
1023#[allow(dead_code)]
1024pub fn licm_dominates(
1025    _dom: u32,
1026    _target: u32,
1027    _dom_tree: &std::collections::HashMap<u32, u32>,
1028) -> bool {
1029    true
1030}
1031/// LICM version
1032#[allow(dead_code)]
1033pub const LICM_BACKEND_VERSION: &str = "1.0.0";
1034#[cfg(test)]
1035mod LICM_infra_tests {
1036    use super::*;
1037    #[test]
1038    pub(super) fn test_pass_config() {
1039        let config = LICMPassConfig::new("test_pass", LICMPassPhase::Transformation);
1040        assert!(config.enabled);
1041        assert!(config.phase.is_modifying());
1042        assert_eq!(config.phase.name(), "transformation");
1043    }
1044    #[test]
1045    pub(super) fn test_pass_stats() {
1046        let mut stats = LICMPassStats::new();
1047        stats.record_run(10, 100, 3);
1048        stats.record_run(20, 200, 5);
1049        assert_eq!(stats.total_runs, 2);
1050        assert!((stats.average_changes_per_run() - 15.0).abs() < 0.01);
1051        assert!((stats.success_rate() - 1.0).abs() < 0.01);
1052        let s = stats.format_summary();
1053        assert!(s.contains("Runs: 2/2"));
1054    }
1055    #[test]
1056    pub(super) fn test_pass_registry() {
1057        let mut reg = LICMPassRegistry::new();
1058        reg.register(LICMPassConfig::new("pass_a", LICMPassPhase::Analysis));
1059        reg.register(LICMPassConfig::new("pass_b", LICMPassPhase::Transformation).disabled());
1060        assert_eq!(reg.total_passes(), 2);
1061        assert_eq!(reg.enabled_count(), 1);
1062        reg.update_stats("pass_a", 5, 50, 2);
1063        let stats = reg.get_stats("pass_a").expect("stats should exist");
1064        assert_eq!(stats.total_changes, 5);
1065    }
1066    #[test]
1067    pub(super) fn test_analysis_cache() {
1068        let mut cache = LICMAnalysisCache::new(10);
1069        cache.insert("key1".to_string(), vec![1, 2, 3]);
1070        assert!(cache.get("key1").is_some());
1071        assert!(cache.get("key2").is_none());
1072        assert!((cache.hit_rate() - 0.5).abs() < 0.01);
1073        cache.invalidate("key1");
1074        assert!(!cache.entries["key1"].valid);
1075        assert_eq!(cache.size(), 1);
1076    }
1077    #[test]
1078    pub(super) fn test_worklist() {
1079        let mut wl = LICMWorklist::new();
1080        assert!(wl.push(1));
1081        assert!(wl.push(2));
1082        assert!(!wl.push(1));
1083        assert_eq!(wl.len(), 2);
1084        assert_eq!(wl.pop(), Some(1));
1085        assert!(!wl.contains(1));
1086        assert!(wl.contains(2));
1087    }
1088    #[test]
1089    pub(super) fn test_dominator_tree() {
1090        let mut dt = LICMDominatorTree::new(5);
1091        dt.set_idom(1, 0);
1092        dt.set_idom(2, 0);
1093        dt.set_idom(3, 1);
1094        assert!(dt.dominates(0, 3));
1095        assert!(dt.dominates(1, 3));
1096        assert!(!dt.dominates(2, 3));
1097        assert!(dt.dominates(3, 3));
1098    }
1099    #[test]
1100    pub(super) fn test_liveness() {
1101        let mut liveness = LICMLivenessInfo::new(3);
1102        liveness.add_def(0, 1);
1103        liveness.add_use(1, 1);
1104        assert!(liveness.defs[0].contains(&1));
1105        assert!(liveness.uses[1].contains(&1));
1106    }
1107    #[test]
1108    pub(super) fn test_constant_folding() {
1109        assert_eq!(LICMConstantFoldingHelper::fold_add_i64(3, 4), Some(7));
1110        assert_eq!(LICMConstantFoldingHelper::fold_div_i64(10, 0), None);
1111        assert_eq!(LICMConstantFoldingHelper::fold_div_i64(10, 2), Some(5));
1112        assert_eq!(
1113            LICMConstantFoldingHelper::fold_bitand_i64(0b1100, 0b1010),
1114            0b1000
1115        );
1116        assert_eq!(LICMConstantFoldingHelper::fold_bitnot_i64(0), -1);
1117    }
1118    #[test]
1119    pub(super) fn test_dep_graph() {
1120        let mut g = LICMDepGraph::new();
1121        g.add_dep(1, 2);
1122        g.add_dep(2, 3);
1123        g.add_dep(1, 3);
1124        assert_eq!(g.dependencies_of(2), vec![1]);
1125        let topo = g.topological_sort();
1126        assert_eq!(topo.len(), 3);
1127        assert!(!g.has_cycle());
1128        let pos: std::collections::HashMap<u32, usize> =
1129            topo.iter().enumerate().map(|(i, &n)| (n, i)).collect();
1130        assert!(pos[&1] < pos[&2]);
1131        assert!(pos[&1] < pos[&3]);
1132        assert!(pos[&2] < pos[&3]);
1133    }
1134}
1135/// LICM code version
1136#[allow(dead_code)]
1137pub const LICM_CODE_VERSION: &str = "1.0.0";
1138
1139// ── CFG-based LICM functions ──────────────────────────────────────────────────
1140
1141use super::types::{
1142    CfgHoistCandidate, LicmBlock, LicmCfg, LicmInstruction, LicmResult, LicmStats, LoopInfo,
1143};
1144
1145/// Compute dominator sets for every block in `cfg`.
1146///
1147/// Returns a vector where `dominators[i]` is the set of block ids that
1148/// dominate block `i` (a block always dominates itself).  Uses the standard
1149/// iterative data-flow algorithm.
1150pub fn compute_dominators(cfg: &LicmCfg) -> Vec<Vec<usize>> {
1151    let n = cfg.blocks.len();
1152    if n == 0 {
1153        return Vec::new();
1154    }
1155
1156    // Initialise: entry is dominated only by itself; all others by the full set.
1157    let all: Vec<usize> = (0..n).collect();
1158    let mut dom: Vec<Vec<usize>> = vec![all.clone(); n];
1159    dom[cfg.entry] = vec![cfg.entry];
1160
1161    let mut changed = true;
1162    while changed {
1163        changed = false;
1164        for block in &cfg.blocks {
1165            if block.id == cfg.entry {
1166                continue;
1167            }
1168            // new_dom = {block.id} ∪ (∩ dom[pred] for each pred)
1169            let mut new_dom: Option<Vec<usize>> = None;
1170            for &pred in &block.predecessors {
1171                let pred_dom = &dom[pred];
1172                new_dom = Some(match new_dom {
1173                    None => pred_dom.clone(),
1174                    Some(existing) => existing
1175                        .into_iter()
1176                        .filter(|x| pred_dom.contains(x))
1177                        .collect(),
1178                });
1179            }
1180            let mut nd = new_dom.unwrap_or_default();
1181            if !nd.contains(&block.id) {
1182                nd.push(block.id);
1183                nd.sort_unstable();
1184            }
1185            if nd != dom[block.id] {
1186                dom[block.id] = nd;
1187                changed = true;
1188            }
1189        }
1190    }
1191    dom
1192}
1193
1194/// Identify natural loops in `cfg` via dominator analysis.
1195///
1196/// A natural loop is defined by a back-edge `(n, h)` where `h` dominates `n`.
1197/// The loop body consists of all blocks from which `n` is reachable without
1198/// passing through `h`.
1199pub fn identify_loops(cfg: &LicmCfg) -> Vec<LoopInfo> {
1200    let dom = compute_dominators(cfg);
1201    let mut loops: Vec<LoopInfo> = Vec::new();
1202
1203    // Find back-edges.
1204    for block in &cfg.blocks {
1205        for &succ in &block.successors {
1206            // back-edge if succ dominates block
1207            if dom[block.id].contains(&succ) {
1208                let header = succ;
1209                let from = block.id;
1210                // Collect loop body via reverse-reachability from `from` up to `header`.
1211                let body = collect_loop_body(cfg, header, from);
1212                // Merge into existing loop with same header or create new.
1213                if let Some(existing) = loops.iter_mut().find(|l| l.header == header) {
1214                    existing.back_edges.push((from, header));
1215                    for b in &body {
1216                        if !existing.body.contains(b) {
1217                            existing.body.push(*b);
1218                        }
1219                    }
1220                } else {
1221                    loops.push(LoopInfo {
1222                        header,
1223                        body,
1224                        preheader: None,
1225                        back_edges: vec![(from, header)],
1226                    });
1227                }
1228            }
1229        }
1230    }
1231    loops
1232}
1233
1234/// Collect all block ids reachable from `from` going *backwards* through
1235/// predecessors, stopping at (and including) `header`.
1236fn collect_loop_body(cfg: &LicmCfg, header: usize, from: usize) -> Vec<usize> {
1237    let mut body = vec![header];
1238    let mut worklist = vec![from];
1239    while let Some(current) = worklist.pop() {
1240        if body.contains(&current) {
1241            continue;
1242        }
1243        body.push(current);
1244        if let Some(blk) = cfg.blocks.iter().find(|b| b.id == current) {
1245            for &pred in &blk.predecessors {
1246                if !body.contains(&pred) {
1247                    worklist.push(pred);
1248                }
1249            }
1250        }
1251    }
1252    body.sort_unstable();
1253    body
1254}
1255
1256/// Return `true` if `instr` is loop-invariant with respect to `loop_body`.
1257///
1258/// An instruction is loop-invariant when none of its operand ids are
1259/// *defined* inside the loop body.  Constant instructions (no uses) are
1260/// always invariant.
1261pub fn is_loop_invariant(instr: &LicmInstruction, loop_body: &[usize], cfg: &LicmCfg) -> bool {
1262    // Collect all instruction ids defined inside the loop.
1263    let loop_defs: Vec<usize> = cfg
1264        .blocks
1265        .iter()
1266        .filter(|b| loop_body.contains(&b.id))
1267        .flat_map(|b| b.instructions.iter().flat_map(|i| i.defs.iter().copied()))
1268        .collect();
1269
1270    // The instruction is invariant if none of its uses are defined in the loop.
1271    instr.uses.iter().all(|u| !loop_defs.contains(u))
1272}
1273
1274/// Find all instructions inside `loop_` that can be hoisted to a preheader.
1275///
1276/// An instruction is eligible when:
1277/// 1. It is loop-invariant.
1278/// 2. It has no side-effects (approximated as having at least one def).
1279pub fn find_hoist_candidates(
1280    cfg: &LicmCfg,
1281    loop_: &LoopInfo,
1282    loop_id: usize,
1283) -> Vec<CfgHoistCandidate> {
1284    let mut candidates = Vec::new();
1285    for &block_id in &loop_.body {
1286        if let Some(blk) = cfg.blocks.iter().find(|b| b.id == block_id) {
1287            for instr in &blk.instructions {
1288                if !instr.defs.is_empty() && is_loop_invariant(instr, &loop_.body, cfg) {
1289                    candidates.push(CfgHoistCandidate {
1290                        instr_id: instr.id,
1291                        loop_id,
1292                        reason: format!(
1293                            "instruction {} is loop-invariant (no operands defined in loop body)",
1294                            instr.id
1295                        ),
1296                    });
1297                }
1298            }
1299        }
1300    }
1301    candidates
1302}
1303
1304/// Create a preheader block for `loop_` and insert it into `cfg`.
1305///
1306/// The preheader is a new block that becomes the sole predecessor of the
1307/// loop header from outside the loop.  All non-back-edge predecessors of
1308/// the header are redirected to the preheader.
1309pub fn create_preheader(cfg: &mut LicmCfg, loop_: &mut LoopInfo) {
1310    if loop_.preheader.is_some() {
1311        return; // already has a preheader
1312    }
1313
1314    let new_id = cfg.blocks.iter().map(|b| b.id).max().unwrap_or(0) + 1;
1315    let header = loop_.header;
1316
1317    // Determine which predecessors of the header come from *outside* the loop.
1318    let outside_preds: Vec<usize> =
1319        if let Some(hdr_block) = cfg.blocks.iter().find(|b| b.id == header) {
1320            hdr_block
1321                .predecessors
1322                .iter()
1323                .copied()
1324                .filter(|p| !loop_.body.contains(p))
1325                .collect()
1326        } else {
1327            Vec::new()
1328        };
1329
1330    // Build the preheader block.
1331    let preheader = LicmBlock {
1332        id: new_id,
1333        instructions: Vec::new(),
1334        successors: vec![header],
1335        predecessors: outside_preds.clone(),
1336    };
1337    cfg.blocks.push(preheader);
1338
1339    // Redirect outside predecessors to go to the preheader instead of header.
1340    for blk in cfg.blocks.iter_mut() {
1341        if outside_preds.contains(&blk.id) {
1342            for succ in blk.successors.iter_mut() {
1343                if *succ == header {
1344                    *succ = new_id;
1345                }
1346            }
1347        }
1348    }
1349
1350    // Update the header's predecessor list.
1351    if let Some(hdr_block) = cfg.blocks.iter_mut().find(|b| b.id == header) {
1352        hdr_block
1353            .predecessors
1354            .retain(|p| !outside_preds.contains(p));
1355        if !hdr_block.predecessors.contains(&new_id) {
1356            hdr_block.predecessors.push(new_id);
1357        }
1358    }
1359
1360    loop_.preheader = Some(new_id);
1361}
1362
1363/// Physically move the instruction identified by `candidate.instr_id` from
1364/// its current block in `loop_` to the loop's preheader block.
1365///
1366/// If no preheader exists, one is created first.
1367pub fn hoist_instruction(cfg: &mut LicmCfg, candidate: &CfgHoistCandidate, loop_: &mut LoopInfo) {
1368    create_preheader(cfg, loop_);
1369    let preheader_id = match loop_.preheader {
1370        Some(id) => id,
1371        None => return,
1372    };
1373
1374    // Extract the instruction from its source block.
1375    let mut hoisted_instr: Option<LicmInstruction> = None;
1376    'outer: for blk in cfg.blocks.iter_mut() {
1377        if loop_.body.contains(&blk.id) {
1378            let pos = blk
1379                .instructions
1380                .iter()
1381                .position(|i| i.id == candidate.instr_id);
1382            if let Some(idx) = pos {
1383                hoisted_instr = Some(blk.instructions.remove(idx));
1384                break 'outer;
1385            }
1386        }
1387    }
1388
1389    // Insert it at the end of the preheader.
1390    if let Some(instr) = hoisted_instr {
1391        if let Some(ph) = cfg.blocks.iter_mut().find(|b| b.id == preheader_id) {
1392            ph.instructions.push(instr);
1393        }
1394    }
1395}
1396
1397/// Run the full LICM pass over `cfg`.
1398///
1399/// Identifies all natural loops, finds hoist candidates in each, creates
1400/// preheaders as needed, and moves invariant instructions out of loops.
1401pub fn run_licm(mut cfg: LicmCfg) -> LicmResult {
1402    let mut loops = identify_loops(&cfg);
1403    let mut all_candidates: Vec<CfgHoistCandidate> = Vec::new();
1404    let mut stats = LicmStats::default();
1405
1406    // Collect candidates before mutating cfg.
1407    for (loop_id, loop_) in loops.iter().enumerate() {
1408        stats.loops_analyzed += 1;
1409        let candidates = find_hoist_candidates(&cfg, loop_, loop_id);
1410        all_candidates.extend(candidates);
1411    }
1412
1413    // Hoist each candidate.
1414    let mut blocks_modified: std::collections::HashSet<usize> = std::collections::HashSet::new();
1415    for candidate in &all_candidates {
1416        let loop_id = candidate.loop_id;
1417        if loop_id < loops.len() {
1418            let loop_ = &mut loops[loop_id];
1419            // Track which blocks will be modified.
1420            for &block_id in &loop_.body {
1421                if let Some(blk) = cfg.blocks.iter().find(|b| b.id == block_id) {
1422                    if blk.instructions.iter().any(|i| i.id == candidate.instr_id) {
1423                        blocks_modified.insert(block_id);
1424                    }
1425                }
1426            }
1427            hoist_instruction(&mut cfg, candidate, loop_);
1428            stats.instructions_hoisted += 1;
1429        }
1430    }
1431    stats.blocks_modified = blocks_modified.len();
1432
1433    LicmResult {
1434        hoisted: all_candidates,
1435        cfg,
1436        stats,
1437    }
1438}
1439
1440#[cfg(test)]
1441mod cfg_licm_tests {
1442    use super::super::types::{
1443        CfgHoistCandidate, LicmBlock, LicmCfg, LicmInstruction, LicmStats, LoopInfo,
1444    };
1445    use super::{
1446        compute_dominators, create_preheader, find_hoist_candidates, hoist_instruction,
1447        identify_loops, is_loop_invariant, run_licm,
1448    };
1449
1450    fn make_instr(id: usize, expr: &str, uses: Vec<usize>, defs: Vec<usize>) -> LicmInstruction {
1451        LicmInstruction {
1452            id,
1453            expr: expr.to_string(),
1454            uses,
1455            defs,
1456            is_invariant: false,
1457        }
1458    }
1459
1460    fn make_block(
1461        id: usize,
1462        instructions: Vec<LicmInstruction>,
1463        successors: Vec<usize>,
1464        predecessors: Vec<usize>,
1465    ) -> LicmBlock {
1466        LicmBlock {
1467            id,
1468            instructions,
1469            successors,
1470            predecessors,
1471        }
1472    }
1473
1474    /// Build a minimal CFG with one loop:
1475    ///   0 -> 1 -> 2 -> 1 (back-edge), 2 -> 3
1476    fn simple_loop_cfg() -> LicmCfg {
1477        let b0 = make_block(
1478            0,
1479            vec![make_instr(10, "x = 5", vec![], vec![10])],
1480            vec![1],
1481            vec![],
1482        );
1483        let b1 = make_block(
1484            1,
1485            vec![
1486                make_instr(20, "y = x + 1", vec![10], vec![20]),
1487                make_instr(21, "z = 42", vec![], vec![21]), // loop-invariant
1488            ],
1489            vec![2],
1490            vec![0, 2],
1491        );
1492        let b2 = make_block(
1493            2,
1494            vec![make_instr(30, "w = y * 2", vec![20], vec![30])],
1495            vec![1, 3],
1496            vec![1],
1497        );
1498        let b3 = make_block(3, vec![], vec![], vec![2]);
1499        LicmCfg {
1500            blocks: vec![b0, b1, b2, b3],
1501            entry: 0,
1502        }
1503    }
1504
1505    #[test]
1506    fn test_compute_dominators_entry() {
1507        let cfg = simple_loop_cfg();
1508        let dom = compute_dominators(&cfg);
1509        // Entry dominates itself only initially
1510        assert!(dom[0].contains(&0));
1511        assert!(!dom[0].contains(&1));
1512    }
1513
1514    #[test]
1515    fn test_compute_dominators_all_blocks_dominated_by_entry() {
1516        let cfg = simple_loop_cfg();
1517        let dom = compute_dominators(&cfg);
1518        for i in 0..cfg.blocks.len() {
1519            assert!(
1520                dom[i].contains(&0),
1521                "block {} should be dominated by entry",
1522                i
1523            );
1524        }
1525    }
1526
1527    #[test]
1528    fn test_compute_dominators_self_dominance() {
1529        let cfg = simple_loop_cfg();
1530        let dom = compute_dominators(&cfg);
1531        for i in 0..cfg.blocks.len() {
1532            assert!(dom[i].contains(&i), "block {} must dominate itself", i);
1533        }
1534    }
1535
1536    #[test]
1537    fn test_identify_loops_finds_one_loop() {
1538        let cfg = simple_loop_cfg();
1539        let loops = identify_loops(&cfg);
1540        assert_eq!(loops.len(), 1, "should find exactly one loop");
1541    }
1542
1543    #[test]
1544    fn test_identify_loops_correct_header() {
1545        let cfg = simple_loop_cfg();
1546        let loops = identify_loops(&cfg);
1547        assert_eq!(loops[0].header, 1);
1548    }
1549
1550    #[test]
1551    fn test_identify_loops_body_contains_header() {
1552        let cfg = simple_loop_cfg();
1553        let loops = identify_loops(&cfg);
1554        assert!(loops[0].body.contains(&1));
1555    }
1556
1557    #[test]
1558    fn test_identify_loops_back_edge() {
1559        let cfg = simple_loop_cfg();
1560        let loops = identify_loops(&cfg);
1561        assert!(
1562            loops[0].back_edges.contains(&(2, 1)),
1563            "back-edge (2->1) expected"
1564        );
1565    }
1566
1567    #[test]
1568    fn test_is_loop_invariant_constant() {
1569        let cfg = simple_loop_cfg();
1570        let instr = make_instr(21, "z = 42", vec![], vec![21]);
1571        let loop_body = vec![1, 2];
1572        assert!(is_loop_invariant(&instr, &loop_body, &cfg));
1573    }
1574
1575    #[test]
1576    fn test_is_loop_invariant_uses_loop_def() {
1577        let cfg = simple_loop_cfg();
1578        // instr 20 (y = x+1) uses 10 which is defined in block 0 (outside loop)
1579        let instr = make_instr(20, "y = x+1", vec![10], vec![20]);
1580        let loop_body = vec![1, 2];
1581        // x (10) is defined in block 0, which is outside the loop body
1582        assert!(is_loop_invariant(&instr, &loop_body, &cfg));
1583    }
1584
1585    #[test]
1586    fn test_is_loop_invariant_uses_inloop_def() {
1587        let cfg = simple_loop_cfg();
1588        // instr 30 uses 20, which is defined by instr 20 in block 1 (inside loop)
1589        let instr = make_instr(30, "w = y*2", vec![20], vec![30]);
1590        let loop_body = vec![1, 2];
1591        assert!(!is_loop_invariant(&instr, &loop_body, &cfg));
1592    }
1593
1594    #[test]
1595    fn test_find_hoist_candidates_finds_constant() {
1596        let cfg = simple_loop_cfg();
1597        let loops = identify_loops(&cfg);
1598        let candidates = find_hoist_candidates(&cfg, &loops[0], 0);
1599        // instr 21 (z=42) should be a hoist candidate
1600        assert!(
1601            candidates.iter().any(|c| c.instr_id == 21),
1602            "instruction 21 should be a hoist candidate"
1603        );
1604    }
1605
1606    #[test]
1607    fn test_find_hoist_candidates_excludes_loop_dependent() {
1608        let cfg = simple_loop_cfg();
1609        let loops = identify_loops(&cfg);
1610        let candidates = find_hoist_candidates(&cfg, &loops[0], 0);
1611        // instr 30 (w=y*2) uses 20 which is defined in the loop
1612        assert!(
1613            !candidates.iter().any(|c| c.instr_id == 30),
1614            "instruction 30 is not invariant"
1615        );
1616    }
1617
1618    #[test]
1619    fn test_create_preheader_adds_block() {
1620        let mut cfg = simple_loop_cfg();
1621        let mut loop_ = identify_loops(&cfg).remove(0);
1622        let original_count = cfg.blocks.len();
1623        create_preheader(&mut cfg, &mut loop_);
1624        assert_eq!(cfg.blocks.len(), original_count + 1);
1625        assert!(loop_.preheader.is_some());
1626    }
1627
1628    #[test]
1629    fn test_create_preheader_idempotent() {
1630        let mut cfg = simple_loop_cfg();
1631        let mut loop_ = identify_loops(&cfg).remove(0);
1632        create_preheader(&mut cfg, &mut loop_);
1633        let count_after_first = cfg.blocks.len();
1634        create_preheader(&mut cfg, &mut loop_);
1635        assert_eq!(cfg.blocks.len(), count_after_first, "should be idempotent");
1636    }
1637
1638    #[test]
1639    fn test_create_preheader_links_to_header() {
1640        let mut cfg = simple_loop_cfg();
1641        let mut loop_ = identify_loops(&cfg).remove(0);
1642        create_preheader(&mut cfg, &mut loop_);
1643        let ph_id = loop_.preheader.unwrap();
1644        let ph = cfg.blocks.iter().find(|b| b.id == ph_id).unwrap();
1645        assert!(ph.successors.contains(&loop_.header));
1646    }
1647
1648    #[test]
1649    fn test_hoist_instruction_moves_instr() {
1650        let mut cfg = simple_loop_cfg();
1651        let mut loops = identify_loops(&cfg);
1652        let candidates = find_hoist_candidates(&cfg, &loops[0], 0);
1653        let candidate = candidates
1654            .iter()
1655            .find(|c| c.instr_id == 21)
1656            .unwrap()
1657            .clone();
1658        hoist_instruction(&mut cfg, &candidate, &mut loops[0]);
1659        // instr 21 should no longer be in block 1
1660        let b1 = cfg.blocks.iter().find(|b| b.id == 1).unwrap();
1661        assert!(
1662            !b1.instructions.iter().any(|i| i.id == 21),
1663            "instruction should have been moved out of block 1"
1664        );
1665    }
1666
1667    #[test]
1668    fn test_hoist_instruction_instr_in_preheader() {
1669        let mut cfg = simple_loop_cfg();
1670        let mut loops = identify_loops(&cfg);
1671        let candidates = find_hoist_candidates(&cfg, &loops[0], 0);
1672        let candidate = candidates
1673            .iter()
1674            .find(|c| c.instr_id == 21)
1675            .unwrap()
1676            .clone();
1677        hoist_instruction(&mut cfg, &candidate, &mut loops[0]);
1678        let ph_id = loops[0].preheader.unwrap();
1679        let ph = cfg.blocks.iter().find(|b| b.id == ph_id).unwrap();
1680        assert!(
1681            ph.instructions.iter().any(|i| i.id == 21),
1682            "instruction should be in preheader"
1683        );
1684    }
1685
1686    #[test]
1687    fn test_run_licm_result_stats() {
1688        let cfg = simple_loop_cfg();
1689        let result = run_licm(cfg);
1690        assert_eq!(result.stats.loops_analyzed, 1);
1691        assert!(result.stats.instructions_hoisted > 0);
1692    }
1693
1694    #[test]
1695    fn test_run_licm_hoisted_candidates_nonempty() {
1696        let cfg = simple_loop_cfg();
1697        let result = run_licm(cfg);
1698        assert!(!result.hoisted.is_empty());
1699    }
1700
1701    #[test]
1702    fn test_run_licm_empty_cfg() {
1703        let cfg = LicmCfg {
1704            blocks: vec![],
1705            entry: 0,
1706        };
1707        let result = run_licm(cfg);
1708        assert_eq!(result.stats.loops_analyzed, 0);
1709        assert_eq!(result.stats.instructions_hoisted, 0);
1710    }
1711
1712    #[test]
1713    fn test_run_licm_no_loops() {
1714        // Linear chain: 0 -> 1 -> 2
1715        let b0 = make_block(
1716            0,
1717            vec![make_instr(1, "a=1", vec![], vec![1])],
1718            vec![1],
1719            vec![],
1720        );
1721        let b1 = make_block(
1722            1,
1723            vec![make_instr(2, "b=2", vec![], vec![2])],
1724            vec![2],
1725            vec![0],
1726        );
1727        let b2 = make_block(2, vec![], vec![], vec![1]);
1728        let cfg = LicmCfg {
1729            blocks: vec![b0, b1, b2],
1730            entry: 0,
1731        };
1732        let result = run_licm(cfg);
1733        assert_eq!(result.stats.loops_analyzed, 0);
1734        assert_eq!(result.stats.instructions_hoisted, 0);
1735    }
1736
1737    #[test]
1738    fn test_loop_info_fields() {
1739        let cfg = simple_loop_cfg();
1740        let loops = identify_loops(&cfg);
1741        let l = &loops[0];
1742        assert!(!l.body.is_empty());
1743        assert!(!l.back_edges.is_empty());
1744        assert!(l.preheader.is_none());
1745    }
1746
1747    #[test]
1748    fn test_licm_stats_default() {
1749        let s = LicmStats::default();
1750        assert_eq!(s.loops_analyzed, 0);
1751        assert_eq!(s.instructions_hoisted, 0);
1752        assert_eq!(s.blocks_modified, 0);
1753    }
1754
1755    #[test]
1756    fn test_cfg_hoist_candidate_fields() {
1757        let c = CfgHoistCandidate {
1758            instr_id: 5,
1759            loop_id: 0,
1760            reason: "test".to_string(),
1761        };
1762        assert_eq!(c.instr_id, 5);
1763        assert_eq!(c.loop_id, 0);
1764        assert_eq!(c.reason, "test");
1765    }
1766}