1use 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
27fn 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
137fn 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
231fn 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
247pub(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 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 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 assert!(!is_pure(&div(int(5), int(0))), "5 / 0 traps → impure");
383 assert!(is_pure(&div(int(10), int(2))), "10 / 2 cannot trap → pure");
385 assert!(
387 is_pure(&div(flt(1.0), flt(0.0))),
388 "float div is total → pure"
389 );
390 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 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}