Skip to main content

aver/ir/mir/optimize/
dead_code.rs

1//! Phase 6 wave 6 — dead-code elimination on MIR.
2//!
3//! Drop `Let { binding, value: <pure>, body }` when `binding` is
4//! never read in `body`. Pure = no observable side effect:
5//! `Literal` / `Local` / `BinOp` / `Neg` / `Tuple` / `List` /
6//! `MapLiteral` (with pure entries) / `Project` / `Construct` /
7//! `RecordCreate` / `RecordUpdate` (with pure subtrees). `Call` /
8//! `TailCall` / `Try` / `Return` / `Match` / `InterpolatedStr` /
9//! `IndependentProduct` are conservatively impure.
10//!
11//! Const-fold runs before DCE so any folded sub-arithmetic
12//! collapses to a `Literal` (pure) and unlocks its enclosing
13//! `Let` for elimination.
14
15use crate::ast::{Literal, Spanned};
16
17use super::super::expr::{MirBinOp, MirExpr, MirLet};
18use super::super::program::{LocalId, MirProgram};
19
20pub fn dead_code(mut program: MirProgram) -> MirProgram {
21    for mir_fn in program.fns.values_mut() {
22        dce_in_place(&mut mir_fn.body);
23    }
24    program
25}
26
27/// Post-order DCE: recurse into children first so any inner
28/// `Let` chain collapses bottom-up. The bind-elision shape
29/// then catches `let _unused = pure; body` at every level.
30fn dce_in_place(expr: &mut Spanned<MirExpr>) {
31    dce_walk_children(&mut expr.node);
32
33    let should_elide = if let MirExpr::Let(spanned_let) = &expr.node {
34        let let_node = &spanned_let.node;
35        !local_is_read(let_node.binding, &let_node.body) && is_pure(&let_node.value)
36    } else {
37        false
38    };
39
40    if should_elide {
41        let placeholder = MirExpr::Literal(Spanned {
42            node: Literal::Unit,
43            line: expr.line,
44            ty: std::sync::OnceLock::new(),
45        });
46        let original = std::mem::replace(&mut expr.node, placeholder);
47        if let MirExpr::Let(spanned_let) = original {
48            let body = *spanned_let.node.body;
49            *expr = body;
50        } else {
51            unreachable!("should_elide is only set inside the Let branch")
52        }
53    }
54}
55
56fn dce_walk_children(node: &mut MirExpr) {
57    match node {
58        MirExpr::Literal(_) | MirExpr::Local(_) | MirExpr::FnValue(_) => {}
59        MirExpr::Neg(inner) => dce_in_place(inner),
60        MirExpr::BinOp(spanned_bop) => {
61            let bop: &mut MirBinOp = &mut spanned_bop.node;
62            dce_in_place(&mut bop.lhs);
63            dce_in_place(&mut bop.rhs);
64        }
65        MirExpr::Let(spanned_let) => {
66            let let_node: &mut MirLet = &mut spanned_let.node;
67            dce_in_place(&mut let_node.value);
68            dce_in_place(&mut let_node.body);
69        }
70        MirExpr::Call(spanned_call) => {
71            for arg in &mut spanned_call.node.args {
72                dce_in_place(arg);
73            }
74        }
75        MirExpr::TailCall(spanned_tc) => {
76            for arg in &mut spanned_tc.node.args {
77                dce_in_place(arg);
78            }
79        }
80        MirExpr::Match(spanned_match) => {
81            dce_in_place(&mut spanned_match.node.subject);
82            for arm in &mut spanned_match.node.arms {
83                dce_in_place(&mut arm.body);
84            }
85        }
86        MirExpr::IfThenElse(spanned_ite) => {
87            dce_in_place(&mut spanned_ite.node.cond);
88            dce_in_place(&mut spanned_ite.node.then_branch);
89            dce_in_place(&mut spanned_ite.node.else_branch);
90        }
91        MirExpr::Construct(spanned_ctor) => {
92            for arg in &mut spanned_ctor.node.args {
93                dce_in_place(arg);
94            }
95        }
96        MirExpr::RecordCreate(spanned_rec) => {
97            for f in &mut spanned_rec.node.fields {
98                dce_in_place(&mut f.value);
99            }
100        }
101        MirExpr::RecordUpdate(spanned_upd) => {
102            dce_in_place(&mut spanned_upd.node.base);
103            for f in &mut spanned_upd.node.updates {
104                dce_in_place(&mut f.value);
105            }
106        }
107        MirExpr::Project(spanned_proj) => dce_in_place(&mut spanned_proj.node.base),
108        MirExpr::Try(inner) => dce_in_place(inner),
109        MirExpr::Return(inner) => dce_in_place(inner),
110        MirExpr::Box(inner) | MirExpr::Unbox(inner) => dce_in_place(inner),
111        MirExpr::List(items) | MirExpr::Tuple(items) => {
112            for item in items {
113                dce_in_place(item);
114            }
115        }
116        MirExpr::MapLiteral(entries) => {
117            for (k, v) in entries {
118                dce_in_place(k);
119                dce_in_place(v);
120            }
121        }
122        MirExpr::InterpolatedStr(parts) => {
123            for part in parts {
124                if let super::super::expr::MirStrPart::Expr(e) = part {
125                    dce_in_place(e);
126                }
127            }
128        }
129        MirExpr::IndependentProduct(spanned_ip) => {
130            for item in &mut spanned_ip.node.items {
131                dce_in_place(item);
132            }
133        }
134    }
135}
136
137/// `true` when `body` contains a `MirExpr::Local` whose slot
138/// equals `target`. Lexical — doesn't track scope shadowing
139/// because MIR's slot numbering is already SSA-ish.
140fn local_is_read(target: LocalId, body: &Spanned<MirExpr>) -> bool {
141    let mut found = false;
142    visit_locals(&body.node, &mut |slot| {
143        if slot == target {
144            found = true;
145        }
146    });
147    found
148}
149
150fn visit_locals(node: &MirExpr, visit: &mut impl FnMut(LocalId)) {
151    match node {
152        MirExpr::Literal(_) | MirExpr::FnValue(_) => {}
153        MirExpr::Local(spanned_local) => visit(spanned_local.node.slot),
154        MirExpr::Neg(inner) => visit_locals(&inner.node, visit),
155        MirExpr::BinOp(spanned_bop) => {
156            visit_locals(&spanned_bop.node.lhs.node, visit);
157            visit_locals(&spanned_bop.node.rhs.node, visit);
158        }
159        MirExpr::Let(spanned_let) => {
160            visit_locals(&spanned_let.node.value.node, visit);
161            visit_locals(&spanned_let.node.body.node, visit);
162        }
163        MirExpr::Call(spanned_call) => {
164            for arg in &spanned_call.node.args {
165                visit_locals(&arg.node, visit);
166            }
167        }
168        MirExpr::TailCall(spanned_tc) => {
169            for arg in &spanned_tc.node.args {
170                visit_locals(&arg.node, visit);
171            }
172        }
173        MirExpr::Match(spanned_match) => {
174            visit_locals(&spanned_match.node.subject.node, visit);
175            for arm in &spanned_match.node.arms {
176                visit_locals(&arm.body.node, visit);
177            }
178        }
179        MirExpr::IfThenElse(spanned_ite) => {
180            visit_locals(&spanned_ite.node.cond.node, visit);
181            visit_locals(&spanned_ite.node.then_branch.node, visit);
182            visit_locals(&spanned_ite.node.else_branch.node, visit);
183        }
184        MirExpr::Construct(spanned_ctor) => {
185            for arg in &spanned_ctor.node.args {
186                visit_locals(&arg.node, visit);
187            }
188        }
189        MirExpr::RecordCreate(spanned_rec) => {
190            for f in &spanned_rec.node.fields {
191                visit_locals(&f.value.node, visit);
192            }
193        }
194        MirExpr::RecordUpdate(spanned_upd) => {
195            visit_locals(&spanned_upd.node.base.node, visit);
196            for f in &spanned_upd.node.updates {
197                visit_locals(&f.value.node, visit);
198            }
199        }
200        MirExpr::Project(spanned_proj) => visit_locals(&spanned_proj.node.base.node, visit),
201        MirExpr::Try(inner)
202        | MirExpr::Return(inner)
203        | MirExpr::Box(inner)
204        | MirExpr::Unbox(inner) => visit_locals(&inner.node, visit),
205        MirExpr::List(items) | MirExpr::Tuple(items) => {
206            for item in items {
207                visit_locals(&item.node, visit);
208            }
209        }
210        MirExpr::MapLiteral(entries) => {
211            for (k, v) in entries {
212                visit_locals(&k.node, visit);
213                visit_locals(&v.node, visit);
214            }
215        }
216        MirExpr::InterpolatedStr(parts) => {
217            for part in parts {
218                if let super::super::expr::MirStrPart::Expr(e) = part {
219                    visit_locals(&e.node, visit);
220                }
221            }
222        }
223        MirExpr::IndependentProduct(spanned_ip) => {
224            for item in &spanned_ip.node.items {
225                visit_locals(&item.node, visit);
226            }
227        }
228    }
229}
230
231/// Whether a division divisor is provably non-zero, so the division
232/// cannot trap and the enclosing `BinOp::Div` may count as pure. Only a
233/// literal we can inspect qualifies: a non-zero integer, or any float
234/// (Float `/` is total). A variable or computed divisor could be zero,
235/// so it is conservatively treated as possibly-trapping.
236fn divisor_proven_nonzero(rhs: &Spanned<MirExpr>) -> bool {
237    match &rhs.node {
238        MirExpr::Literal(spanned) => match spanned.node {
239            Literal::Int(n) => n != 0,
240            Literal::Float(_) => true,
241            _ => false,
242        },
243        _ => false,
244    }
245}
246
247/// Conservative purity classification — `true` means the
248/// expression has no observable side effect AND cannot diverge
249/// or raise. Exported `pub(super)` so the algebraic pass can
250/// reuse it for `x * 0` (only collapse when the surviving
251/// operand is pure).
252pub(super) fn is_pure(expr: &Spanned<MirExpr>) -> bool {
253    match &expr.node {
254        MirExpr::Literal(_) | MirExpr::Local(_) | MirExpr::FnValue(_) => true,
255        MirExpr::Neg(inner) => is_pure(inner),
256        MirExpr::BinOp(spanned_bop) => {
257            let bop = &spanned_bop.node;
258            // Integer `/` by a possibly-zero divisor traps at runtime
259            // ("division by zero"). Classifying it pure would let DCE drop
260            // a dead `5 / 0`, or the `x * 0` collapse fold `(5 / 0) * 0`
261            // to `0` — silently turning a trapping program into a
262            // non-trapping one. Only a divisor we can prove non-zero is
263            // safe to elide. (Float `/` is total — `x / 0.0` is Infinity,
264            // never traps — and `%`/modulo is a Result-returning builtin,
265            // not a `BinOp`, so neither applies here.)
266            if matches!(bop.op, crate::ast::BinOp::Div) && !divisor_proven_nonzero(&bop.rhs) {
267                return false;
268            }
269            is_pure(&bop.lhs) && is_pure(&bop.rhs)
270        }
271        MirExpr::Tuple(items) | MirExpr::List(items) => items.iter().all(is_pure),
272        MirExpr::MapLiteral(entries) => entries.iter().all(|(k, v)| is_pure(k) && is_pure(v)),
273        MirExpr::Construct(spanned_ctor) => spanned_ctor.node.args.iter().all(is_pure),
274        MirExpr::RecordCreate(spanned_rec) => {
275            spanned_rec.node.fields.iter().all(|f| is_pure(&f.value))
276        }
277        MirExpr::RecordUpdate(spanned_upd) => {
278            is_pure(&spanned_upd.node.base)
279                && spanned_upd.node.updates.iter().all(|f| is_pure(&f.value))
280        }
281        MirExpr::Project(spanned_proj) => is_pure(&spanned_proj.node.base),
282        MirExpr::Let(spanned_let) => {
283            is_pure(&spanned_let.node.value) && is_pure(&spanned_let.node.body)
284        }
285        MirExpr::IfThenElse(spanned_ite) => {
286            is_pure(&spanned_ite.node.cond)
287                && is_pure(&spanned_ite.node.then_branch)
288                && is_pure(&spanned_ite.node.else_branch)
289        }
290        // A representation boundary is pure iff its inner value is — it is
291        // a pure `from_i64` / `to_i64` conversion over the inner result.
292        MirExpr::Box(inner) | MirExpr::Unbox(inner) => is_pure(inner),
293        MirExpr::Call(_)
294        | MirExpr::TailCall(_)
295        | MirExpr::Try(_)
296        | MirExpr::Return(_)
297        | MirExpr::Match(_)
298        | MirExpr::InterpolatedStr(_)
299        | MirExpr::IndependentProduct(_) => false,
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::super::super::expr::{MirBinOp, MirCall, MirCallee, MirLet};
306    use super::super::super::program::LocalId;
307    use super::super::const_fold::const_fold;
308    use super::super::test_helpers::{body_of, one_fn_program, span};
309    use super::*;
310    use crate::ast::BinOp;
311    use crate::ir::FnId;
312
313    #[test]
314    fn dce_drops_unused_pure_let() {
315        let body = MirExpr::Let(span(MirLet {
316            binding: LocalId(0),
317            binding_name: "x".to_string(),
318            value: Box::new(span(MirExpr::Literal(span(Literal::Int(7))))),
319            body: Box::new(span(MirExpr::Literal(span(Literal::Int(42))))),
320        }));
321        let eliminated = dead_code(one_fn_program(body));
322        assert!(
323            matches!(body_of(&eliminated), MirExpr::Literal(s) if matches!(s.node, Literal::Int(42))),
324            "dead Let with pure value should collapse to body"
325        );
326    }
327
328    #[test]
329    fn dce_keeps_used_let() {
330        use super::super::super::expr::MirLocal;
331        let read = MirExpr::BinOp(span(MirBinOp {
332            op: BinOp::Add,
333            lhs: Box::new(span(MirExpr::Local(span(MirLocal::at(LocalId(0)))))),
334            rhs: Box::new(span(MirExpr::Literal(span(Literal::Int(1))))),
335        }));
336        let body = MirExpr::Let(span(MirLet {
337            binding: LocalId(0),
338            binding_name: "x".to_string(),
339            value: Box::new(span(MirExpr::Literal(span(Literal::Int(7))))),
340            body: Box::new(span(read)),
341        }));
342        let eliminated = dead_code(one_fn_program(body));
343        assert!(
344            matches!(body_of(&eliminated), MirExpr::Let(_)),
345            "Let with read binding must stay"
346        );
347    }
348
349    #[test]
350    fn dce_keeps_unused_impure_let() {
351        let call_value = MirExpr::Call(span(MirCall {
352            callee: MirCallee::Fn(FnId(0)),
353            args: vec![],
354        }));
355        let body = MirExpr::Let(span(MirLet {
356            binding: LocalId(0),
357            binding_name: String::new(),
358            value: Box::new(span(call_value)),
359            body: Box::new(span(MirExpr::Literal(span(Literal::Int(42))))),
360        }));
361        let eliminated = dead_code(one_fn_program(body));
362        assert!(
363            matches!(body_of(&eliminated), MirExpr::Let(_)),
364            "unused Let with impure (Call) value must stay — could be an effect"
365        );
366    }
367
368    #[test]
369    fn div_purity_classification_respects_trapping_divisor() {
370        use super::super::super::expr::MirLocal;
371        let div = |l: MirExpr, r: MirExpr| {
372            span(MirExpr::BinOp(span(MirBinOp {
373                op: BinOp::Div,
374                lhs: Box::new(span(l)),
375                rhs: Box::new(span(r)),
376            })))
377        };
378        let int = |n| MirExpr::Literal(span(Literal::Int(n)));
379        let flt = |f| MirExpr::Literal(span(Literal::Float(f)));
380
381        // Integer `/` by a zero literal traps at runtime → must be impure.
382        assert!(!is_pure(&div(int(5), int(0))), "5 / 0 traps → impure");
383        // Integer `/` by a proven non-zero literal cannot trap → pure.
384        assert!(is_pure(&div(int(10), int(2))), "10 / 2 cannot trap → pure");
385        // Float `/` is total (`x / 0.0` is Infinity) → pure.
386        assert!(
387            is_pure(&div(flt(1.0), flt(0.0))),
388            "float div is total → pure"
389        );
390        // A variable divisor could be zero at runtime → conservatively impure.
391        assert!(
392            !is_pure(&div(int(5), MirExpr::Local(span(MirLocal::at(LocalId(0)))))),
393            "variable divisor could be zero → impure"
394        );
395    }
396
397    #[test]
398    fn dce_keeps_unused_integer_div_by_zero() {
399        // Regression: a dead `5 / 0` binding must NOT be eliminated — the
400        // division traps, and dropping it would silently turn a trapping
401        // program into a non-trapping one.
402        let value = MirExpr::BinOp(span(MirBinOp {
403            op: BinOp::Div,
404            lhs: Box::new(span(MirExpr::Literal(span(Literal::Int(5))))),
405            rhs: Box::new(span(MirExpr::Literal(span(Literal::Int(0))))),
406        }));
407        let body = MirExpr::Let(span(MirLet {
408            binding: LocalId(0),
409            binding_name: String::new(),
410            value: Box::new(span(value)),
411            body: Box::new(span(MirExpr::Literal(span(Literal::Int(42))))),
412        }));
413        let eliminated = dead_code(one_fn_program(body));
414        assert!(
415            matches!(body_of(&eliminated), MirExpr::Let(_)),
416            "unused `5 / 0` Let must stay — eliding it would drop a runtime trap"
417        );
418    }
419
420    #[test]
421    fn dce_drops_nested_unused_pure_let_chains() {
422        let inner = MirExpr::Let(span(MirLet {
423            binding: LocalId(1),
424            binding_name: "b".to_string(),
425            value: Box::new(span(MirExpr::Literal(span(Literal::Int(2))))),
426            body: Box::new(span(MirExpr::Literal(span(Literal::Int(99))))),
427        }));
428        let outer = MirExpr::Let(span(MirLet {
429            binding: LocalId(0),
430            binding_name: "a".to_string(),
431            value: Box::new(span(MirExpr::Literal(span(Literal::Int(1))))),
432            body: Box::new(span(inner)),
433        }));
434        let eliminated = dead_code(one_fn_program(outer));
435        assert!(
436            matches!(body_of(&eliminated), MirExpr::Literal(s) if matches!(s.node, Literal::Int(99))),
437            "two stacked dead pure Lets should both collapse"
438        );
439    }
440
441    #[test]
442    fn const_fold_then_dce_composes() {
443        let value = MirExpr::BinOp(span(MirBinOp {
444            op: BinOp::Add,
445            lhs: Box::new(span(MirExpr::Literal(span(Literal::Int(1))))),
446            rhs: Box::new(span(MirExpr::Literal(span(Literal::Int(2))))),
447        }));
448        let body = MirExpr::Let(span(MirLet {
449            binding: LocalId(0),
450            binding_name: "x".to_string(),
451            value: Box::new(span(value)),
452            body: Box::new(span(MirExpr::Literal(span(Literal::Int(99))))),
453        }));
454        let optimized = dead_code(const_fold(one_fn_program(body)));
455        assert!(
456            matches!(body_of(&optimized), MirExpr::Literal(s) if matches!(s.node, Literal::Int(99))),
457            "fold→dce should collapse the whole Let to the body literal"
458        );
459    }
460}