Skip to main content

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}