rucc_opt/dce.rs
1//! Dead code elimination: an instruction nothing uses and nothing depends on goes away.
2//!
3//! The other half of [`crate::fold`]. Folding rewrites an instruction in place and leaves its
4//! operands behind, used by nothing, so a function that folds well is a function whose printed IR
5//! grows a tail of arithmetic that computes numbers nobody reads. Every later pass will do the
6//! same thing, because a rewrite that has to clean up after itself is a rewrite that has to know
7//! what else was using what it replaced, and that is the knowledge this pass exists to hold in one
8//! place.
9//!
10//! It is not primarily an optimization. The backend materializes a constant where it is wanted
11//! rather than where the IR wrote it, so most of what this removes was already costing nothing in
12//! the output. What it buys is that a dump reads like the program, that the passes after it see a
13//! function whose size is the size of the work in it, and that a rule which fires on a dead
14//! instruction is a rule that fired on nothing rather than a rule that fired.
15//!
16//! # How it decides
17//!
18//! An instruction goes when it is not a terminator, when every value it produces is used by
19//! nothing, and when it does not happen for a reason of its own. [`Opcode::has_effects`] is the
20//! predicate for the last of those and it answers one question for two different things: it means
21//! both that an instruction writes memory or does something the program can observe, and that it
22//! reads memory. An allocation and a `va_arg` are the first and stay. A plain load is only the
23//! second, and it goes.
24//!
25//! A call is the one instruction where the opcode is not the answer, because what a call does is
26//! what the function it calls does. [`crate::purity`] is who works that out and the cache carries
27//! it here, so a call whose result nothing reads goes away when the callee reads memory at most
28//! and comes back. That is section 34.6 of `spec/optimizer/34-ipa.md` naming this pass as one of
29//! the four consumers the analysis was written for. Where nothing worked the purity out, which is
30//! `-O0` and every caller that builds an analysis cache by hand, every call stays.
31//!
32//! Removing a dead load needs no memory analysis, which is why it does not wait for one. It cannot
33//! change what any byte holds, it cannot change what another load sees, and nothing after it can
34//! tell that it did not happen. The only thing it changes is whether the program faults on an
35//! address it was never going to use the bytes of, and that is what a compiler is for. What does
36//! stay is a load the program asked to happen, which is a `volatile` one, and a load other threads
37//! can see the order of, which is an atomic one at any strength.
38//!
39//! An allocation nothing addresses is still removable and still here, and that one does want a
40//! memory analysis, because whether anything addresses it is the question.
41//!
42//! # Why it is a worklist
43//!
44//! Removing an instruction can kill the one that fed it, and that one can kill its own operand, so
45//! a single walk in any order finds a fraction of what is there. The counts are built once, and
46//! removing an instruction decrements what its operands were used for, and an operand that reaches
47//! zero puts its own definition back on the list. That reaches the same fixpoint a repeated walk
48//! would and touches each instruction about once.
49//!
50//! Uses are counted per occurrence rather than per instruction, because `x + x` uses `x` twice and
51//! removing one adder should not make `x` look dead.
52//!
53//! # What it does not remove
54//!
55//! Not a block parameter. A parameter nothing reads is dead in exactly the same sense, and taking
56//! it out means rewriting the argument list of every branch that arrives at the block, which is
57//! worth doing and is a different transformation from this one. The loop carried case is the
58//! interesting one there and it is the reason to do it separately: a parameter whose only use is
59//! the argument it passes to itself is dead, and seeing that needs the cycle broken rather than a
60//! count driven to zero.
61//!
62//! Not an unreachable block. A block no branch names is dead code by any definition, and removing
63//! it is control flow work rather than value work. It belongs with the branch folding that creates
64//! most of it.
65
66use rucc_ir::{Block, Def, Extra, Flags, Func, Inst, MemOrder, Opcode};
67
68use crate::purity::{Callee, Facts};
69use crate::uses::{count, operands};
70use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
71
72/// Recorded once for each instruction taken out.
73const REMOVED: &str = "instruction with no effects and no users removed";
74
75/// Recorded for a call taken out, which is a different thing from the line above it.
76///
77/// Worth its own line in the remarks because it is the only removal here that rests on something
78/// other than the opcode. Everything else this pass takes out is dead by inspection; a call is
79/// dead because [`crate::purity`] worked out what the callee does, and somebody reading a remark
80/// about a call that went away wants to know which of those two it was.
81const REMOVED_CALL: &str =
82 "call whose result nothing reads removed, the callee does nothing the caller can tell";
83
84/// Recorded for an instruction that would have gone if there had been fuel for it.
85const NO_FUEL: &str = "dead instruction kept, the pass ran out of fuel";
86
87/// Recorded once for a function that has an instruction this pass is not allowed to look at.
88///
89/// The honest miss of this pass, and the one worth reading. A store nothing can read again and an
90/// allocation nothing addresses are both removable once there is a memory analysis to say so, and
91/// both of them stay. A function with none of these is a function where this pass found everything
92/// there was. A call that stays is counted here as well, and what it is waiting on is not a memory
93/// analysis but a body: a call to a function this unit cannot see is opaque and will stay opaque
94/// until there is cross module summary information, which is document 35's.
95const NEEDS_MEMORY_ANALYSIS: &str =
96 "instruction with effects left alone, removing it needs a memory analysis";
97
98/// What this pass is called, for the lists in [`crate::pipeline`] that name it.
99pub const NAME: &str = "dce";
100
101/// The pass. It holds nothing, because the counts are per function and live in [`Pass::run`].
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub struct Dce;
104
105impl Pass for Dce {
106 fn name(&self) -> &'static str {
107 NAME
108 }
109
110 fn describe(&self) -> &'static str {
111 "an instruction with no effects whose results nothing uses is removed"
112 }
113
114 fn preserves(&self) -> Preserved {
115 // Instructions go and blocks do not. A terminator is never dead, because it has an
116 // effect, so no block loses the thing that gives it its edges. What does go is a use, and
117 // the last use of a value is the end of its live range, so the liveness is not what it
118 // was and neither is anything counted off it. Nothing had caught this because no pass
119 // before this one in any pipeline builds the liveness, and an analysis nobody has built
120 // is an analysis nobody can be wrong about.
121 Preserved::ALL.without(Analysis::Liveness)
122 }
123
124 fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
125 dce_in(func, an.purity(), fuel)
126 }
127}
128
129/// The pass over one function, with the purity handed in rather than read off an analysis cache.
130///
131/// [`crate::ipasra`] wants this. It works a module at a time, and what it leaves behind after it
132/// takes a parameter out is the argument the caller was computing, which is read by nothing now.
133/// There is no per function cache where that pass stands, and building one to ask a single question
134/// would build every other analysis the cache holds along with it.
135pub(crate) fn dce_in(func: &mut Func, facts: &Facts, fuel: &mut Fuel) -> Stats {
136 let mut stats = Stats::new();
137 let mut uses = count(func);
138 let mut work: Vec<Inst> = Vec::new();
139 for block in func.blocks().collect::<Vec<Block>>() {
140 for inst in func.insts(block) {
141 match verdict(func, inst, &uses, facts) {
142 Verdict::Dead => work.push(inst),
143 // Nothing reads it and it stays anyway, which is the one thing this pass
144 // gives up on rather than the thousands of instructions that are simply
145 // live. Counted here, in the one walk that sees every instruction, so the
146 // number is per function and not per visit of the worklist.
147 Verdict::Effects => stats.missed(NEEDS_MEMORY_ANALYSIS),
148 Verdict::Used | Verdict::Terminator => {}
149 }
150 }
151 }
152 while let Some(inst) = work.pop() {
153 // A worklist can name the same instruction twice, once from the first walk and once
154 // from an operand reaching zero, and the second visit finds it already gone.
155 if func.block_of(inst).is_none() {
156 continue;
157 }
158 if verdict(func, inst, &uses, facts) != Verdict::Dead {
159 continue;
160 }
161 if !fuel.take() {
162 // Out of fuel, which stops the transforming and not the looking, the same way
163 // folding treats it. Draining the rest of the list without removing anything
164 // costs one pass over what is left and keeps the walk's shape independent of
165 // where the fuel ran out.
166 stats.missed(NO_FUEL);
167 continue;
168 }
169 operands(func, inst, |value| {
170 let count = &mut uses[value.index()];
171 *count -= 1;
172 if *count == 0 {
173 if let Def::Result { inst: def, .. } = func[value].def {
174 work.push(def);
175 }
176 }
177 });
178 let was_a_call = Callee::of(func, inst).is_some();
179 func.remove_inst(inst);
180 stats.optimized(if was_a_call { REMOVED_CALL } else { REMOVED });
181 }
182 stats
183}
184
185/// Whether this instruction can go, and when it cannot, what kept it.
186///
187/// The reason is separated out from the answer because two of the three reasons are ordinary and
188/// one of them is worth reporting. Nearly every instruction in a function is [`Verdict::Used`],
189/// which says nothing. [`Verdict::Effects`] is reached only by an instruction nothing reads, and
190/// there are few of those and every one of them is a thing this pass would take if it knew more.
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192enum Verdict {
193 /// Nothing reads it, nothing depends on it happening, and it can go.
194 Dead,
195 /// Something reads one of its results.
196 Used,
197 /// It ends a block, so the block goes with it or neither does.
198 Terminator,
199 /// Nothing reads it and it happens anyway, as far as this pass can tell.
200 Effects,
201}
202
203/// Whether this instruction only reads memory, so that not doing it is something nothing can tell.
204///
205/// A plain load and nothing else. A `volatile` load is an access the program asked for by name and
206/// happens whether or not anybody wanted the value. An atomic load is part of an order other
207/// threads can see, at every strength and not only at the fence-like ones, and there is no reason
208/// to argue about the weak end of that until something is waiting on the answer.
209fn reads_only(func: &Func, inst: Inst) -> bool {
210 let data = &func[inst];
211 if data.opcode != Opcode::Load || data.flags.contains(Flags::VOLATILE) {
212 return false;
213 }
214 let Extra::Mem(mem) = data.extra else { return false };
215 func[mem].order == MemOrder::NotAtomic
216}
217
218/// Whether this is a call that can go when nothing reads what it returned.
219///
220/// Both halves of that are [`crate::Purity::can_be_deleted_when_unused`] and both are needed: a
221/// call that writes memory does something even when the result is thrown away, and a call that may
222/// not come back does something by not coming back. Which leaves `const` and `pure`, and a `pure`
223/// call is removable for the same reason a load is, since not reading memory is not something
224/// anything can tell happened.
225///
226/// A tail call never reaches here, because it is a terminator and the verdict says so first.
227/// Inline assembly reaches here and is [`crate::Purity::Opaque`], which is what keeps the
228/// assertion below true.
229fn does_nothing(func: &Func, inst: Inst, facts: &Facts) -> bool {
230 Callee::of(func, inst)
231 .is_some_and(|callee| facts.purity_of(callee).can_be_deleted_when_unused())
232}
233
234/// Whether not doing this instruction is something nothing can tell.
235///
236/// The [`Verdict::Effects`] half of the verdict below, asked on its own because
237/// [`crate::loop_delete`] wants it without the use counts. Every instruction in a loop it is
238/// looking at is used by something else in that loop, so a count driven to zero is not the
239/// question there and this is.
240pub(crate) fn removable(func: &Func, inst: Inst, facts: &Facts) -> bool {
241 !func[inst].opcode.has_effects() || reads_only(func, inst) || does_nothing(func, inst, facts)
242}
243
244/// What to do with this instruction.
245fn verdict(func: &Func, inst: Inst, uses: &[u32], facts: &Facts) -> Verdict {
246 let data = &func[inst];
247 // `is_terminator` on the function rather than on the opcode, because `asm goto` branches
248 // and its opcode does not say so. Inline assembly has effects either way, so this is belt
249 // and braces, and it is the cheaper of the two mistakes to make.
250 if func.is_terminator(inst) {
251 return Verdict::Terminator;
252 }
253 if !data.results().all(|value| uses[value.index()] == 0) {
254 return Verdict::Used;
255 }
256 if !removable(func, inst, facts) {
257 return Verdict::Effects;
258 }
259 debug_assert!(
260 data.opcode != Opcode::InlineAsm,
261 "inline assembly has effects and cannot reach here"
262 );
263 Verdict::Dead
264}
265
266#[cfg(test)]
267mod tests {
268 use std::sync::Arc;
269
270 use rucc_base::Interner;
271 use rucc_ir::{
272 AttrSet, Block, Builder, Flags, Func, FuncId, MemInfo, MemOrder, Module, Opcode, Pic,
273 Restrict, Signature, Type,
274 };
275 use rucc_target::{TargetInfo, Triple};
276
277 use crate::purity::{Facts, infer};
278 use crate::stats::Kind;
279 use crate::{Analyses, Analysis, CallGraph, Fuel, Pass, dce::Dce};
280
281 /// A module where `f` calls `g` and throws away what came back, with `g` built as asked and
282 /// the purity worked out over the pair.
283 ///
284 /// The call is the last instruction before the return, so a test that wants to know whether it
285 /// went away counts what is left in the block.
286 fn caller(named: &str, attrs: AttrSet, body: fn(&mut Func)) -> (Module, FuncId, Analyses) {
287 let mut names = Interner::new();
288 let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
289 let mut module = Module::new(names.intern("t.c"), &target);
290 let mut callee = Func::new(names.intern(named), Signature::new());
291 callee.attrs.set = attrs;
292 body(&mut callee);
293 module.add_func(callee);
294 let mut func = Func::new(names.intern("f"), Signature::new());
295 let block = func.create_block();
296 let mut build = Builder::new(&mut func, block);
297 let signature = build.func().add_signature(Signature::new());
298 build.call(names.intern(named), signature, &[]);
299 build.ret(&[]);
300 let id = module.add_func(func);
301 let mut facts = Facts::of_module(&module, &names);
302 infer(&module, &CallGraph::of(&module, Pic::Executable), &mut facts);
303 let an = crate::machine::fixtures::analyses().calling(Arc::new(facts));
304 (module, id, an)
305 }
306
307 /// A body that returns at once.
308 fn nothing(func: &mut Func) {
309 let block = func.create_block();
310 Builder::new(func, block).ret(&[]);
311 }
312
313 /// No body at all.
314 fn none(_: &mut Func) {}
315
316 /// How many instructions are left in the one block of `f`.
317 fn left_in_f(module: &Module, id: FuncId) -> usize {
318 let func = &module[id];
319 func.blocks().map(|block| func.insts(block).count()).sum()
320 }
321
322 #[test]
323 fn a_call_whose_result_nothing_reads_goes_away_when_the_callee_does_nothing() {
324 let (mut module, id, mut an) = caller("g", AttrSet::NONE, nothing);
325 assert_eq!(left_in_f(&module, id), 2);
326 let stats = Dce.run(&mut module[id], &mut an, &mut Fuel::unlimited());
327 assert!(stats.changed());
328 assert_eq!(left_in_f(&module, id), 1);
329 assert_eq!(stats.count(Kind::Optimized, super::REMOVED_CALL), 1);
330 assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 0);
331 }
332
333 #[test]
334 fn a_call_to_something_nobody_can_see_the_body_of_stays() {
335 let (mut module, id, mut an) = caller("g", AttrSet::NONE, none);
336 assert!(!Dce.run(&mut module[id], &mut an, &mut Fuel::unlimited()).changed());
337 assert_eq!(left_in_f(&module, id), 2);
338 }
339
340 #[test]
341 fn a_call_to_a_function_that_may_not_come_back_stays() {
342 // Its result depends on nothing and the call still does something, which is not come
343 // back. This is the whole reason the looping levels are in the enum.
344 let (mut module, id, mut an) =
345 caller("g", AttrSet::READNONE.union(AttrSet::NORETURN), none);
346 assert!(!Dce.run(&mut module[id], &mut an, &mut Fuel::unlimited()).changed());
347 assert_eq!(left_in_f(&module, id), 2);
348 }
349
350 #[test]
351 fn a_call_stays_when_nothing_worked_the_purity_out() {
352 // Which is the `-O0` pipeline, and every caller that builds an analysis cache by hand.
353 // A pass has to be correct against the empty facts, because that is what it is handed
354 // until somebody fills them in.
355 let (mut module, id, _) = caller("g", AttrSet::NONE, nothing);
356 let mut an = crate::machine::fixtures::analyses();
357 assert!(!Dce.run(&mut module[id], &mut an, &mut Fuel::unlimited()).changed());
358 assert_eq!(left_in_f(&module, id), 2);
359 }
360
361 /// A function with one block, ready to have instructions appended to it.
362 fn blank() -> (Interner, Func, Block) {
363 let mut names = Interner::new();
364 let name = names.intern("f");
365 let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(32)]));
366 let block = func.create_block();
367 (names, func, block)
368 }
369
370 /// A four byte access of that strength, with nothing else said about it.
371 fn plain(order: MemOrder) -> MemInfo {
372 MemInfo { size: 4, align: 4, owns: 4, order, tbaa: None, restrict: Restrict::NONE }
373 }
374
375 /// How many instructions are left in a block.
376 fn left(func: &Func, block: Block) -> usize {
377 func.insts(block).count()
378 }
379
380 #[test]
381 fn arithmetic_nothing_reads_goes_away() {
382 let (_, mut func, block) = blank();
383 let mut build = Builder::new(&mut func, block);
384 let a = build.iconst(Type::int(32), 2);
385 let b = build.iconst(Type::int(32), 3);
386 build.binary(Opcode::Add, a, b, Flags::NONE);
387 build.ret(&[a]);
388 assert!(
389 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
390 .changed()
391 );
392 // The add, and then the constant that only it read. A single walk in this order would
393 // have removed the add and left the three behind, which is what the worklist is for.
394 assert_eq!(left(&func, block), 2);
395 }
396
397 #[test]
398 fn the_counts_in_the_cache_go_with_the_uses_that_were_removed() {
399 let (_, mut func, block) = blank();
400 let mut build = Builder::new(&mut func, block);
401 let a = build.iconst(Type::int(32), 2);
402 let b = build.iconst(Type::int(32), 3);
403 build.binary(Opcode::Add, a, b, Flags::NONE);
404 build.ret(&[a]);
405 let mut an = crate::machine::fixtures::analyses();
406 // Two values are live where the add is and one is live once it has gone, which is the
407 // fact this pass used to say it had left standing.
408 an.pressure(&func);
409 assert!(Dce.run(&mut func, &mut an, &mut Fuel::unlimited()).changed());
410 assert!(an.settle(&func, Dce.preserves(), true).is_empty(), "the pass was caught out");
411 assert!(!an.holds(Analysis::Pressure), "a stale count was left for the next pass to read");
412 }
413
414 #[test]
415 fn arithmetic_something_reads_stays() {
416 let (_, mut func, block) = blank();
417 let mut build = Builder::new(&mut func, block);
418 let a = build.iconst(Type::int(32), 2);
419 let b = build.iconst(Type::int(32), 3);
420 let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
421 build.ret(&[sum]);
422 assert!(
423 !Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
424 .changed()
425 );
426 assert_eq!(left(&func, block), 4);
427 }
428
429 #[test]
430 fn a_value_used_twice_is_not_dead_when_one_use_goes() {
431 let (_, mut func, block) = blank();
432 let mut build = Builder::new(&mut func, block);
433 let x = build.iconst(Type::int(32), 7);
434 let kept = build.binary(Opcode::Add, x, x, Flags::NONE);
435 build.binary(Opcode::Add, x, x, Flags::NONE);
436 build.ret(&[kept]);
437 assert!(
438 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
439 .changed()
440 );
441 // Only the second add. Counting a use per instruction rather than per position would
442 // have driven the constant to zero and taken it out from under the first one.
443 assert_eq!(left(&func, block), 3);
444 }
445
446 #[test]
447 fn a_store_stays_however_dead_it_looks() {
448 let (_, mut func, block) = blank();
449 let mut build = Builder::new(&mut func, block);
450 let value = build.iconst(Type::int(32), 1);
451 let address = build.iconst(Type::int(64), 0);
452 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
453 let info = MemInfo {
454 size: 4,
455 align: 4,
456 order: MemOrder::NotAtomic,
457 tbaa: None,
458 owns: 0,
459 restrict: Restrict::NONE,
460 };
461 build.store(value, address, info, Flags::NONE);
462 build.ret(&[value]);
463 let stats =
464 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
465 assert!(!stats.changed());
466 assert_eq!(left(&func, block), 5);
467 // The store is the one instruction here that nothing reads and that stays anyway, so it
468 // is the one this pass reports as a miss. That count is the honest size of what a memory
469 // analysis would buy, per function, without anybody having to guess at it.
470 assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 1);
471 }
472
473 /// A plain load nothing reads goes, which is the one thing here that does not wait for a
474 /// memory analysis. Removing it cannot change what any byte holds or what another load sees.
475 #[test]
476 fn a_load_nothing_reads_goes_away() {
477 let (_, mut func, block) = blank();
478 let mut build = Builder::new(&mut func, block);
479 let address = build.iconst(Type::int(64), 0);
480 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
481 build.load(Type::int(32), address, plain(MemOrder::NotAtomic), Flags::NONE);
482 let kept = build.iconst(Type::int(32), 1);
483 build.ret(&[kept]);
484 let stats =
485 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
486 assert!(stats.changed());
487 // The load, then the cast and the constant that only it read, so what is left is the
488 // constant the return reads and the return.
489 assert_eq!(left(&func, block), 2);
490 assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 0);
491 }
492
493 /// A `volatile` load stays. It is an access the program asked for by name, and it happens
494 /// whether or not anybody wanted the value it produced.
495 #[test]
496 fn a_volatile_load_nothing_reads_stays() {
497 let (_, mut func, block) = blank();
498 let mut build = Builder::new(&mut func, block);
499 let address = build.iconst(Type::int(64), 0);
500 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
501 build.load(Type::int(32), address, plain(MemOrder::NotAtomic), Flags::VOLATILE);
502 let kept = build.iconst(Type::int(32), 1);
503 build.ret(&[kept]);
504 let stats =
505 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
506 assert!(!stats.changed());
507 assert_eq!(left(&func, block), 5);
508 assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 1);
509 }
510
511 /// An atomic load stays at every strength, because what it is part of is an order other
512 /// threads can see rather than the value it hands back.
513 #[test]
514 fn an_atomic_load_nothing_reads_stays_however_weak_it_is() {
515 for order in [MemOrder::Relaxed, MemOrder::Acquire, MemOrder::SeqCst] {
516 let (_, mut func, block) = blank();
517 let mut build = Builder::new(&mut func, block);
518 let address = build.iconst(Type::int(64), 0);
519 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
520 build.load(Type::int(32), address, plain(order), Flags::NONE);
521 let kept = build.iconst(Type::int(32), 1);
522 build.ret(&[kept]);
523 let stats = Dce.run(
524 &mut func,
525 &mut crate::machine::fixtures::analyses(),
526 &mut Fuel::unlimited(),
527 );
528 assert!(!stats.changed(), "{order:?}");
529 assert_eq!(left(&func, block), 5, "{order:?}");
530 }
531 }
532
533 #[test]
534 fn a_value_a_branch_passes_on_is_used_by_the_branch() {
535 let (_, mut func, block) = blank();
536 let target = func.create_block();
537 let param = func.append_param(target, Type::int(32));
538 let mut build = Builder::new(&mut func, block);
539 let x = build.iconst(Type::int(32), 9);
540 build.jump(target, &[x]);
541 let mut build = Builder::new(&mut func, target);
542 build.ret(&[param]);
543 assert!(
544 !Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
545 .changed()
546 );
547 // The constant is read by nothing in its own block and is not dead, because the only
548 // use an instruction can have that its argument list does not hold is this one.
549 assert_eq!(left(&func, block), 2);
550 }
551
552 #[test]
553 fn a_result_a_removed_instruction_read_is_looked_at_again() {
554 let (_, mut func, block) = blank();
555 let mut build = Builder::new(&mut func, block);
556 let a = build.iconst(Type::int(32), 2);
557 let b = build.iconst(Type::int(32), 3);
558 let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
559 let doubled = build.binary(Opcode::Add, sum, sum, Flags::NONE);
560 build.unary(Opcode::SExt, doubled, Type::int(64));
561 let kept = build.iconst(Type::int(32), 1);
562 build.ret(&[kept]);
563 assert!(
564 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
565 .changed()
566 );
567 // A chain five long, dead from the far end, and all of it goes in one run. This is the
568 // case a walk in program order finds one instruction of per run.
569 assert_eq!(left(&func, block), 2);
570 }
571
572 #[test]
573 fn fuel_stops_the_removing_and_not_the_looking() {
574 let (_, mut func, block) = blank();
575 let mut build = Builder::new(&mut func, block);
576 let a = build.iconst(Type::int(32), 2);
577 let b = build.iconst(Type::int(32), 3);
578 build.binary(Opcode::Add, a, b, Flags::NONE);
579 build.ret(&[a]);
580 let mut fuel = Fuel::of(1);
581 let stats = Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut fuel);
582 assert!(stats.changed());
583 // The add and nothing after it, so the constant the add was keeping alive stays. One
584 // unit of fuel is one transformation, which is what makes a bisection over it land on
585 // a single site.
586 assert_eq!(left(&func, block), 3);
587 assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
588 assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
589 }
590}