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/// What to do with this instruction.
235fn verdict(func: &Func, inst: Inst, uses: &[u32], facts: &Facts) -> Verdict {
236 let data = &func[inst];
237 // `is_terminator` on the function rather than on the opcode, because `asm goto` branches
238 // and its opcode does not say so. Inline assembly has effects either way, so this is belt
239 // and braces, and it is the cheaper of the two mistakes to make.
240 if func.is_terminator(inst) {
241 return Verdict::Terminator;
242 }
243 if !data.results().all(|value| uses[value.index()] == 0) {
244 return Verdict::Used;
245 }
246 if data.opcode.has_effects() && !reads_only(func, inst) && !does_nothing(func, inst, facts) {
247 return Verdict::Effects;
248 }
249 debug_assert!(
250 data.opcode != Opcode::InlineAsm,
251 "inline assembly has effects and cannot reach here"
252 );
253 Verdict::Dead
254}
255
256#[cfg(test)]
257mod tests {
258 use std::sync::Arc;
259
260 use rucc_base::Interner;
261 use rucc_ir::{
262 AttrSet, Block, Builder, Flags, Func, FuncId, MemInfo, MemOrder, Module, Opcode, Pic,
263 Restrict, Signature, Type,
264 };
265 use rucc_target::{TargetInfo, Triple};
266
267 use crate::purity::{Facts, infer};
268 use crate::stats::Kind;
269 use crate::{Analyses, Analysis, CallGraph, Fuel, Pass, dce::Dce};
270
271 /// A module where `f` calls `g` and throws away what came back, with `g` built as asked and
272 /// the purity worked out over the pair.
273 ///
274 /// The call is the last instruction before the return, so a test that wants to know whether it
275 /// went away counts what is left in the block.
276 fn caller(named: &str, attrs: AttrSet, body: fn(&mut Func)) -> (Module, FuncId, Analyses) {
277 let mut names = Interner::new();
278 let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
279 let mut module = Module::new(names.intern("t.c"), &target);
280 let mut callee = Func::new(names.intern(named), Signature::new());
281 callee.attrs.set = attrs;
282 body(&mut callee);
283 module.add_func(callee);
284 let mut func = Func::new(names.intern("f"), Signature::new());
285 let block = func.create_block();
286 let mut build = Builder::new(&mut func, block);
287 let signature = build.func().add_signature(Signature::new());
288 build.call(names.intern(named), signature, &[]);
289 build.ret(&[]);
290 let id = module.add_func(func);
291 let mut facts = Facts::of_module(&module, &names);
292 infer(&module, &CallGraph::of(&module, Pic::Executable), &mut facts);
293 let an = crate::machine::fixtures::analyses().calling(Arc::new(facts));
294 (module, id, an)
295 }
296
297 /// A body that returns at once.
298 fn nothing(func: &mut Func) {
299 let block = func.create_block();
300 Builder::new(func, block).ret(&[]);
301 }
302
303 /// No body at all.
304 fn none(_: &mut Func) {}
305
306 /// How many instructions are left in the one block of `f`.
307 fn left_in_f(module: &Module, id: FuncId) -> usize {
308 let func = &module[id];
309 func.blocks().map(|block| func.insts(block).count()).sum()
310 }
311
312 #[test]
313 fn a_call_whose_result_nothing_reads_goes_away_when_the_callee_does_nothing() {
314 let (mut module, id, mut an) = caller("g", AttrSet::NONE, nothing);
315 assert_eq!(left_in_f(&module, id), 2);
316 let stats = Dce.run(&mut module[id], &mut an, &mut Fuel::unlimited());
317 assert!(stats.changed());
318 assert_eq!(left_in_f(&module, id), 1);
319 assert_eq!(stats.count(Kind::Optimized, super::REMOVED_CALL), 1);
320 assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 0);
321 }
322
323 #[test]
324 fn a_call_to_something_nobody_can_see_the_body_of_stays() {
325 let (mut module, id, mut an) = caller("g", AttrSet::NONE, none);
326 assert!(!Dce.run(&mut module[id], &mut an, &mut Fuel::unlimited()).changed());
327 assert_eq!(left_in_f(&module, id), 2);
328 }
329
330 #[test]
331 fn a_call_to_a_function_that_may_not_come_back_stays() {
332 // Its result depends on nothing and the call still does something, which is not come
333 // back. This is the whole reason the looping levels are in the enum.
334 let (mut module, id, mut an) =
335 caller("g", AttrSet::READNONE.union(AttrSet::NORETURN), 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_stays_when_nothing_worked_the_purity_out() {
342 // Which is the `-O0` pipeline, and every caller that builds an analysis cache by hand.
343 // A pass has to be correct against the empty facts, because that is what it is handed
344 // until somebody fills them in.
345 let (mut module, id, _) = caller("g", AttrSet::NONE, nothing);
346 let mut an = crate::machine::fixtures::analyses();
347 assert!(!Dce.run(&mut module[id], &mut an, &mut Fuel::unlimited()).changed());
348 assert_eq!(left_in_f(&module, id), 2);
349 }
350
351 /// A function with one block, ready to have instructions appended to it.
352 fn blank() -> (Interner, Func, Block) {
353 let mut names = Interner::new();
354 let name = names.intern("f");
355 let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(32)]));
356 let block = func.create_block();
357 (names, func, block)
358 }
359
360 /// A four byte access of that strength, with nothing else said about it.
361 fn plain(order: MemOrder) -> MemInfo {
362 MemInfo { size: 4, align: 4, owns: 4, order, tbaa: None, restrict: Restrict::NONE }
363 }
364
365 /// How many instructions are left in a block.
366 fn left(func: &Func, block: Block) -> usize {
367 func.insts(block).count()
368 }
369
370 #[test]
371 fn arithmetic_nothing_reads_goes_away() {
372 let (_, mut func, block) = blank();
373 let mut build = Builder::new(&mut func, block);
374 let a = build.iconst(Type::int(32), 2);
375 let b = build.iconst(Type::int(32), 3);
376 build.binary(Opcode::Add, a, b, Flags::NONE);
377 build.ret(&[a]);
378 assert!(
379 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
380 .changed()
381 );
382 // The add, and then the constant that only it read. A single walk in this order would
383 // have removed the add and left the three behind, which is what the worklist is for.
384 assert_eq!(left(&func, block), 2);
385 }
386
387 #[test]
388 fn the_counts_in_the_cache_go_with_the_uses_that_were_removed() {
389 let (_, mut func, block) = blank();
390 let mut build = Builder::new(&mut func, block);
391 let a = build.iconst(Type::int(32), 2);
392 let b = build.iconst(Type::int(32), 3);
393 build.binary(Opcode::Add, a, b, Flags::NONE);
394 build.ret(&[a]);
395 let mut an = crate::machine::fixtures::analyses();
396 // Two values are live where the add is and one is live once it has gone, which is the
397 // fact this pass used to say it had left standing.
398 an.pressure(&func);
399 assert!(Dce.run(&mut func, &mut an, &mut Fuel::unlimited()).changed());
400 assert!(an.settle(&func, Dce.preserves(), true).is_empty(), "the pass was caught out");
401 assert!(!an.holds(Analysis::Pressure), "a stale count was left for the next pass to read");
402 }
403
404 #[test]
405 fn arithmetic_something_reads_stays() {
406 let (_, mut func, block) = blank();
407 let mut build = Builder::new(&mut func, block);
408 let a = build.iconst(Type::int(32), 2);
409 let b = build.iconst(Type::int(32), 3);
410 let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
411 build.ret(&[sum]);
412 assert!(
413 !Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
414 .changed()
415 );
416 assert_eq!(left(&func, block), 4);
417 }
418
419 #[test]
420 fn a_value_used_twice_is_not_dead_when_one_use_goes() {
421 let (_, mut func, block) = blank();
422 let mut build = Builder::new(&mut func, block);
423 let x = build.iconst(Type::int(32), 7);
424 let kept = build.binary(Opcode::Add, x, x, Flags::NONE);
425 build.binary(Opcode::Add, x, x, Flags::NONE);
426 build.ret(&[kept]);
427 assert!(
428 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
429 .changed()
430 );
431 // Only the second add. Counting a use per instruction rather than per position would
432 // have driven the constant to zero and taken it out from under the first one.
433 assert_eq!(left(&func, block), 3);
434 }
435
436 #[test]
437 fn a_store_stays_however_dead_it_looks() {
438 let (_, mut func, block) = blank();
439 let mut build = Builder::new(&mut func, block);
440 let value = build.iconst(Type::int(32), 1);
441 let address = build.iconst(Type::int(64), 0);
442 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
443 let info = MemInfo {
444 size: 4,
445 align: 4,
446 order: MemOrder::NotAtomic,
447 tbaa: None,
448 owns: 0,
449 restrict: Restrict::NONE,
450 };
451 build.store(value, address, info, Flags::NONE);
452 build.ret(&[value]);
453 let stats =
454 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
455 assert!(!stats.changed());
456 assert_eq!(left(&func, block), 5);
457 // The store is the one instruction here that nothing reads and that stays anyway, so it
458 // is the one this pass reports as a miss. That count is the honest size of what a memory
459 // analysis would buy, per function, without anybody having to guess at it.
460 assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 1);
461 }
462
463 /// A plain load nothing reads goes, which is the one thing here that does not wait for a
464 /// memory analysis. Removing it cannot change what any byte holds or what another load sees.
465 #[test]
466 fn a_load_nothing_reads_goes_away() {
467 let (_, mut func, block) = blank();
468 let mut build = Builder::new(&mut func, block);
469 let address = build.iconst(Type::int(64), 0);
470 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
471 build.load(Type::int(32), address, plain(MemOrder::NotAtomic), Flags::NONE);
472 let kept = build.iconst(Type::int(32), 1);
473 build.ret(&[kept]);
474 let stats =
475 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
476 assert!(stats.changed());
477 // The load, then the cast and the constant that only it read, so what is left is the
478 // constant the return reads and the return.
479 assert_eq!(left(&func, block), 2);
480 assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 0);
481 }
482
483 /// A `volatile` load stays. It is an access the program asked for by name, and it happens
484 /// whether or not anybody wanted the value it produced.
485 #[test]
486 fn a_volatile_load_nothing_reads_stays() {
487 let (_, mut func, block) = blank();
488 let mut build = Builder::new(&mut func, block);
489 let address = build.iconst(Type::int(64), 0);
490 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
491 build.load(Type::int(32), address, plain(MemOrder::NotAtomic), Flags::VOLATILE);
492 let kept = build.iconst(Type::int(32), 1);
493 build.ret(&[kept]);
494 let stats =
495 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
496 assert!(!stats.changed());
497 assert_eq!(left(&func, block), 5);
498 assert_eq!(stats.count(Kind::Missed, super::NEEDS_MEMORY_ANALYSIS), 1);
499 }
500
501 /// An atomic load stays at every strength, because what it is part of is an order other
502 /// threads can see rather than the value it hands back.
503 #[test]
504 fn an_atomic_load_nothing_reads_stays_however_weak_it_is() {
505 for order in [MemOrder::Relaxed, MemOrder::Acquire, MemOrder::SeqCst] {
506 let (_, mut func, block) = blank();
507 let mut build = Builder::new(&mut func, block);
508 let address = build.iconst(Type::int(64), 0);
509 let address = build.unary(Opcode::IntToPtr, address, Type::PTR);
510 build.load(Type::int(32), address, plain(order), Flags::NONE);
511 let kept = build.iconst(Type::int(32), 1);
512 build.ret(&[kept]);
513 let stats = Dce.run(
514 &mut func,
515 &mut crate::machine::fixtures::analyses(),
516 &mut Fuel::unlimited(),
517 );
518 assert!(!stats.changed(), "{order:?}");
519 assert_eq!(left(&func, block), 5, "{order:?}");
520 }
521 }
522
523 #[test]
524 fn a_value_a_branch_passes_on_is_used_by_the_branch() {
525 let (_, mut func, block) = blank();
526 let target = func.create_block();
527 let param = func.append_param(target, Type::int(32));
528 let mut build = Builder::new(&mut func, block);
529 let x = build.iconst(Type::int(32), 9);
530 build.jump(target, &[x]);
531 let mut build = Builder::new(&mut func, target);
532 build.ret(&[param]);
533 assert!(
534 !Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
535 .changed()
536 );
537 // The constant is read by nothing in its own block and is not dead, because the only
538 // use an instruction can have that its argument list does not hold is this one.
539 assert_eq!(left(&func, block), 2);
540 }
541
542 #[test]
543 fn a_result_a_removed_instruction_read_is_looked_at_again() {
544 let (_, mut func, block) = blank();
545 let mut build = Builder::new(&mut func, block);
546 let a = build.iconst(Type::int(32), 2);
547 let b = build.iconst(Type::int(32), 3);
548 let sum = build.binary(Opcode::Add, a, b, Flags::NONE);
549 let doubled = build.binary(Opcode::Add, sum, sum, Flags::NONE);
550 build.unary(Opcode::SExt, doubled, Type::int(64));
551 let kept = build.iconst(Type::int(32), 1);
552 build.ret(&[kept]);
553 assert!(
554 Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
555 .changed()
556 );
557 // A chain five long, dead from the far end, and all of it goes in one run. This is the
558 // case a walk in program order finds one instruction of per run.
559 assert_eq!(left(&func, block), 2);
560 }
561
562 #[test]
563 fn fuel_stops_the_removing_and_not_the_looking() {
564 let (_, mut func, block) = blank();
565 let mut build = Builder::new(&mut func, block);
566 let a = build.iconst(Type::int(32), 2);
567 let b = build.iconst(Type::int(32), 3);
568 build.binary(Opcode::Add, a, b, Flags::NONE);
569 build.ret(&[a]);
570 let mut fuel = Fuel::of(1);
571 let stats = Dce.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut fuel);
572 assert!(stats.changed());
573 // The add and nothing after it, so the constant the add was keeping alive stays. One
574 // unit of fuel is one transformation, which is what makes a bisection over it land on
575 // a single site.
576 assert_eq!(left(&func, block), 3);
577 assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
578 assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
579 }
580}