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/// 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}