Skip to main content

rucc_opt/
number.rs

1//! Two instructions in a block that compute the same thing from the same things are one value.
2//!
3//! Design: `spec/optimizer/16-gvn-and-pre.md` section 16.1. This is the other half of that
4//! document, the half [`crate::load`] deliberately did not do. Section 16.2 is candid that value
5//! numbering over arithmetic is worth less on C than people expect, because the front end does not
6//! generate the same expression twice and the programmer does not write it twice. That is true of
7//! the arithmetic somebody wrote. It is not true of the arithmetic the front end emits underneath
8//! it, and the address of a subscript is the case that matters: `a[i] = v; total += a[i];` is one
9//! subscript written twice in C and two separate runs of the same multiply and add in the IR,
10//! because lowering a subscript does not know it has lowered that subscript already.
11//!
12//! So this pass mostly does not pay for itself in what it removes. It pays for itself in what it
13//! lets the pass after it see. [`crate::load`] compares addresses by identity, so until the store's
14//! address and the load's address have one name it cannot forward a store to the load that reads it
15//! straight back, which is the shape it was written for. Giving them one name is this.
16//!
17//! # Block local, and why that is the whole of it
18//!
19//! One table per block, thrown away at the end of it. Inside a block an earlier instruction
20//! dominates a later one because there is no other way to reach the later one, so program order is
21//! the whole of the dominance question and there is no dominator tree here.
22//!
23//! The version over the dominator tree finds strictly more, and section 16.1 is where the argument
24//! for not writing it lives: under arms B and C of the e-graph experiment, hash-consing gives the
25//! acyclic case for nothing, and what is left over is the cyclic case, which wants Tarjan's
26//! algorithm over the SSA graph and belongs after the e-graph is built rather than before it. What
27//! is wanted before that exists is the part that makes the address of one subscript one value, and
28//! both halves of a subscript are in the block the subscript is in.
29//!
30//! # What counts as the same thing
31//!
32//! The opcode, the flags, the result type, whatever the instruction carries besides its operands,
33//! and the operands. All five, and a difference in any of them is two values.
34//!
35//! The flags are in the key rather than being merged or intersected. Two adds of the same pair
36//! where one of them says its result cannot wrap and the other says nothing are two entries, and
37//! the program keeps both. Merging them onto the one that promises more would hand the weaker
38//! instruction a promise nobody made about it, and merging them onto the one that promises less
39//! throws away something a later pass wanted. Keeping them apart costs an instruction that is
40//! rarely there and is the answer that needs no argument.
41//!
42//! The operands are looked up through what this pass has already decided, so a value that has been
43//! redirected onto an earlier one is compared as the earlier one. That is what lets a chain work:
44//! once two multiplies are one, the two adds on top of them have the same operands and become one
45//! too, and so does the address on top of those. A subscript is three or four instructions deep,
46//! so without this the pass would collapse the bottom of it and stop.
47//!
48//! An operand is a value and not an expression, so `(a + b) + c` and `a + (b + c)` are two values
49//! here. Making them one is reassociation, which is document 19 and a different pass.
50//!
51//! # What is allowed to move
52//!
53//! [`Opcode::has_effects`] answering no, which the IR defines as exactly the property this pass
54//! needs: an instruction that answers no can be deleted when nothing reads it, moved across a call,
55//! and merged with another one computing the same thing. It is written as a list of the pure
56//! opcodes rather than a list of the impure ones, so an opcode added to the IR later is impure
57//! until somebody says otherwise, and this pass leaves it alone.
58//!
59//! Three things beyond that are refused. `mem_entry` is pure and is not a computation, it is the
60//! name of memory on the way in, and merging two of them is a question for [`crate::memssa`] rather
61//! than an arithmetic identity. Anything producing other than exactly one result is refused, which
62//! is the checked arithmetic, whose second result would need redirecting alongside the first and
63//! which is not common enough to be worth the shape. Anything carrying something this pass cannot
64//! compare by value is refused, which in practice is `blockaddr` and nothing else, every other
65//! payload being on an opcode that has effects anyway.
66//!
67//! Division is on the allowed list and that is deliberate. Removing the second of two identical
68//! divisions is safe for a reason that is only true block locally: the first one is in the same
69//! block, so it has already run, and if it was going to trap the second one was never reached.
70//!
71//! # What it does not do
72//!
73//! Nothing crosses a block boundary, nothing goes through memory, and no instruction moves. A
74//! duplicate is removed where it stands and its readers are pointed at the first one, which is
75//! always above it. That means a computation in two arms of a branch stays in two arms: hoisting it
76//! to the common predecessor is [`crate::hoist`], and it wants the profitability question this pass
77//! does not ask.
78
79use std::collections::HashMap;
80
81use rucc_base::Symbol;
82use rucc_ir::{Block, Extra, Flags, FloatPred, Func, Inst, IntPred, Opcode, Type, Value};
83
84use crate::uses::substitute;
85use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats};
86
87/// Recorded for a removed address computation, which is what the pass is here for.
88const ADDRESS: &str = "address removed, an earlier one in the block computes the same address";
89
90/// Recorded for any other removed duplicate.
91const MERGED: &str = "instruction removed, an earlier one in the block computes the same thing";
92
93/// Recorded for a duplicate that would have gone if there had been fuel for it.
94const NO_FUEL: &str = "duplicate instruction kept, the pass ran out of fuel";
95
96/// The most operands any pure opcode has, which is the three of `select` and `fma`.
97const OPERANDS: usize = 3;
98
99/// The pass.
100#[derive(Debug)]
101pub struct Number;
102
103impl Pass for Number {
104    fn name(&self) -> &'static str {
105        "number"
106    }
107
108    fn describe(&self) -> &'static str {
109        "two instructions in a block computing the same thing from the same things are one value"
110    }
111
112    fn preserves(&self) -> Preserved {
113        // The shape of the function. No block is added, none is removed, no edge moves, and what
114        // is removed is pure, which no terminator is.
115        //
116        // The liveness is the one thing that does move, for the reason `crate::simplify` gives:
117        // pointing every reader of one value at another is one more place the second is live and
118        // one fewer the first is.
119        Preserved::ALL.without(Analysis::Liveness)
120    }
121
122    fn run(&self, func: &mut Func, _an: &mut Analyses, fuel: &mut Fuel) -> Stats {
123        let mut stats = Stats::new();
124        // What each removed instruction's result is read as. It is also what an operand is looked
125        // up through while the block is being walked, which is why it is built as the walk goes
126        // and applied to the function once at the end rather than either one alone.
127        let mut same: HashMap<Value, Value> = HashMap::new();
128        let mut gone: Vec<Inst> = Vec::new();
129
130        for block in func.blocks().collect::<Vec<Block>>() {
131            let mut seen: HashMap<Key, Value> = HashMap::new();
132            for inst in func.insts(block).collect::<Vec<Inst>>() {
133                let Some((key, result)) = key(func, &same, inst) else { continue };
134                let Some(&first) = seen.get(&key) else {
135                    seen.insert(key, result);
136                    continue;
137                };
138                if !fuel.take() {
139                    // Out of fuel, which is a request to stop transforming and not to stop
140                    // looking. The walk goes on so that the count of what could have gone is the
141                    // same at every fuel setting, which is what makes a bisection over it
142                    // monotonic. The table is left as it is, so the next duplicate of this same
143                    // thing is counted against the same first instruction.
144                    stats.missed(NO_FUEL);
145                    continue;
146                }
147                same.insert(result, first);
148                gone.push(inst);
149                stats.optimized(if is_address(func[inst].opcode) { ADDRESS } else { MERGED });
150            }
151        }
152
153        for inst in gone {
154            func.remove_inst(inst);
155        }
156        if !same.is_empty() {
157            substitute(func, &same);
158        }
159        stats
160    }
161}
162
163/// Whether an opcode is one of the two that compute an address.
164///
165/// Only for the counters, which want the two numbers apart because they answer different
166/// questions. The address count is what feeds [`crate::load`] and is the reason the pass exists.
167/// The other count is whatever else happened to be written twice, which on real C is not much.
168fn is_address(opcode: Opcode) -> bool {
169    matches!(opcode, Opcode::PtrAdd | Opcode::GlobalAddr)
170}
171
172/// What an instruction computes, as something two instructions can be equal on.
173///
174/// Fixed size and `Copy`, because a hash table entry per pure instruction in the program is enough
175/// work without an allocation for each of them.
176#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
177struct Key {
178    /// Which instruction it is.
179    opcode: Opcode,
180    /// What the optimizer was told it may assume about this one, which is not the same question as
181    /// what it may assume about another one of the same shape.
182    flags: Flags,
183    /// The type of its one result, which is what tells two casts of the same value apart.
184    ty: Type,
185    /// Whatever it carries besides operands, compared by value rather than by where it is stored.
186    tag: Tag,
187    /// Its operands, resolved, padded with `None`, and put in order if the opcode does not care.
188    args: [Option<Value>; OPERANDS],
189}
190
191/// An instruction's payload, as far as one can be compared with another.
192///
193/// [`Extra`] holds most of its payloads as an index into a side table, and two equal payloads
194/// written at two times are two indices, so an equality on the index would answer no to a question
195/// this pass is asking. This is the payload itself for the shapes a pure opcode has.
196#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
197enum Tag {
198    /// Nothing, which is all of the arithmetic.
199    None,
200    /// A constant's bits, for `iconst`, `fconst` and `splat`. The immediate table is not interned,
201    /// so this is the only reading under which two of the same constant are the same constant.
202    Bits(u128),
203    /// A name, for `global_addr`.
204    Symbol(Symbol),
205    /// Which comparison, for `icmp`.
206    IntPred(IntPred),
207    /// Which comparison, for `fcmp`.
208    FloatPred(FloatPred),
209}
210
211/// What an instruction computes and where its answer is, or nothing if it is not a candidate.
212///
213/// `same` is what the block has decided so far, and every operand goes through it, so an operand
214/// naming an instruction this pass is removing is compared as the instruction it is being removed
215/// in favour of. One lookup is the whole resolution rather than the first step of one: a value is
216/// either a key of `same`, meaning it is on its way out, or a value in `seen`, meaning it is
217/// staying, and it cannot be both, because a value only enters `same` on a hit and a hit never
218/// touches what the table already holds.
219fn key(func: &Func, same: &HashMap<Value, Value>, inst: Inst) -> Option<(Key, Value)> {
220    let data = &func[inst];
221    if data.opcode.has_effects() || data.opcode == Opcode::MemEntry {
222        return None;
223    }
224    let mut results = data.results();
225    let (Some(result), None) = (results.next(), results.next()) else { return None };
226    let tag = match data.extra {
227        Extra::None => Tag::None,
228        Extra::Imm(at) => Tag::Bits(func[at].bits()),
229        Extra::Symbol(name) => Tag::Symbol(name),
230        Extra::IntPred(pred) => Tag::IntPred(pred),
231        Extra::FloatPred(pred) => Tag::FloatPred(pred),
232        _ => return None,
233    };
234    let operands = &func[data.args];
235    if operands.len() > OPERANDS {
236        return None;
237    }
238    let mut args = [None; OPERANDS];
239    for (slot, &arg) in args.iter_mut().zip(operands) {
240        *slot = Some(same.get(&arg).copied().unwrap_or(arg));
241    }
242    // Two operands the opcode reads in either order are put in one order, so that `a + b` written
243    // once and `b + a` written once are one add. Sorting is by the position of the value in the
244    // function, which is an order that exists for no other reason and is fine because the only
245    // thing asked of it is that the two sides agree on it.
246    if data.opcode.is_commutative() && operands.len() == 2 {
247        args[..2].sort_unstable();
248    }
249    Some((Key { opcode: data.opcode, flags: data.flags, ty: func[result].ty, tag, args }, result))
250}
251
252#[cfg(test)]
253mod tests {
254    use rucc_ir::{
255        Block, Builder, Def, Extra, InstData, MemInfo, MemOrder, Restrict, Signature, Type,
256    };
257
258    use super::*;
259    use crate::stats::Kind;
260
261    /// An empty function with one block, which is where every test below builds.
262    fn blank() -> (Func, Block) {
263        let mut names = rucc_base::Interner::new();
264        let name = names.intern("f");
265        let mut func = Func::new(name, Signature::new().with_returns(&[Type::int(64)]));
266        let block = func.create_block();
267        (func, block)
268    }
269
270    /// An ordinary access of that alignment, with nothing said about its type.
271    fn plain(align: u32) -> MemInfo {
272        MemInfo { size: 0, align, order: MemOrder::NotAtomic, tbaa: None, restrict: Restrict::NONE }
273    }
274
275    /// An `alloca` of thirty-two bytes, which is an address nothing outside the function knows.
276    fn local(build: &mut Builder<'_>) -> Value {
277        let mem = build.func().add_mem(MemInfo { size: 32, ..plain(8) });
278        build.value(InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) }, Type::PTR)
279    }
280
281    /// Runs the pass over the function with as much fuel as it wants.
282    fn run(func: &mut Func) -> Stats {
283        Number.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
284    }
285
286    /// How many instructions of that opcode are left in the function.
287    fn count(func: &Func, opcode: Opcode) -> usize {
288        func.blocks()
289            .flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
290            .filter(|&inst| func[inst].opcode == opcode)
291            .count()
292    }
293
294    /// What the return statement hands back, after the pass has pointed it somewhere.
295    fn returned(func: &Func) -> Vec<Value> {
296        let block = func.blocks().last().expect("the function has a block");
297        let inst = func.terminator(block).expect("the block has a terminator");
298        func[func[inst].args].to_vec()
299    }
300
301    /// The operands of whatever instruction produced that value.
302    fn operands(func: &Func, value: Value) -> Vec<Value> {
303        let Def::Result { inst, .. } = func[value].def else { panic!("not an instruction result") };
304        func[func[inst].args].to_vec()
305    }
306
307    #[test]
308    fn the_same_arithmetic_on_the_same_operands_twice_is_one_instruction() {
309        let (mut func, block) = blank();
310        let mut build = Builder::new(&mut func, block);
311        let left = build.iconst(Type::int(64), 3);
312        let right = build.iconst(Type::int(64), 5);
313        let first = build.binary(Opcode::Add, left, right, Flags::NONE);
314        let second = build.binary(Opcode::Add, left, right, Flags::NONE);
315        build.ret(&[first, second]);
316
317        let stats = run(&mut func);
318        assert_eq!(stats.count(Kind::Optimized, MERGED), 1);
319        assert_eq!(count(&func, Opcode::Add), 1);
320        assert_eq!(returned(&func), vec![first, first]);
321    }
322
323    #[test]
324    fn a_commutative_pair_matches_with_its_operands_the_other_way_round() {
325        let (mut func, block) = blank();
326        let mut build = Builder::new(&mut func, block);
327        let left = build.iconst(Type::int(64), 3);
328        let right = build.iconst(Type::int(64), 5);
329        let first = build.binary(Opcode::Add, left, right, Flags::NONE);
330        let second = build.binary(Opcode::Add, right, left, Flags::NONE);
331        build.ret(&[first, second]);
332
333        let stats = run(&mut func);
334        assert_eq!(stats.count(Kind::Optimized, MERGED), 1);
335        assert_eq!(returned(&func), vec![first, first]);
336    }
337
338    #[test]
339    fn a_subtraction_the_other_way_round_is_a_different_answer() {
340        let (mut func, block) = blank();
341        let mut build = Builder::new(&mut func, block);
342        let left = build.iconst(Type::int(64), 3);
343        let right = build.iconst(Type::int(64), 5);
344        let first = build.binary(Opcode::Sub, left, right, Flags::NONE);
345        let second = build.binary(Opcode::Sub, right, left, Flags::NONE);
346        build.ret(&[first, second]);
347
348        let stats = run(&mut func);
349        assert!(!stats.changed(), "three minus five is not five minus three");
350        assert_eq!(count(&func, Opcode::Sub), 2);
351    }
352
353    #[test]
354    fn two_adds_that_promise_different_things_stay_two_adds() {
355        let (mut func, block) = blank();
356        let mut build = Builder::new(&mut func, block);
357        let left = build.iconst(Type::int(64), 3);
358        let right = build.iconst(Type::int(64), 5);
359        let first = build.binary(Opcode::Add, left, right, Flags::NSW);
360        let second = build.binary(Opcode::Add, left, right, Flags::NONE);
361        build.ret(&[first, second]);
362
363        // Merging them onto the first hands the second a promise nobody made about it, and
364        // merging them onto the second throws away a promise somebody did make.
365        let stats = run(&mut func);
366        assert!(!stats.changed());
367        assert_eq!(count(&func, Opcode::Add), 2);
368    }
369
370    #[test]
371    fn a_chain_collapses_all_the_way_up_and_not_just_at_the_bottom() {
372        let (mut func, block) = blank();
373        let mut build = Builder::new(&mut func, block);
374        let index = build.iconst(Type::int(64), 2);
375        let scale = build.iconst(Type::int(64), 8);
376        let first = build.binary(Opcode::Mul, index, scale, Flags::NONE);
377        let second = build.binary(Opcode::Mul, index, scale, Flags::NONE);
378        let up = build.binary(Opcode::Add, first, scale, Flags::NONE);
379        let down = build.binary(Opcode::Add, second, scale, Flags::NONE);
380        build.ret(&[up, down]);
381
382        // The second add's operand is a value on its way out, so it has to be compared as the
383        // value it is on its way out in favour of. Without that the pass takes the multiply and
384        // stops, which on a subscript is the bottom instruction of three or four.
385        let stats = run(&mut func);
386        assert_eq!(stats.count(Kind::Optimized, MERGED), 2);
387        assert_eq!(count(&func, Opcode::Mul), 1);
388        assert_eq!(count(&func, Opcode::Add), 1);
389        assert_eq!(returned(&func), vec![up, up]);
390    }
391
392    #[test]
393    fn the_same_constant_written_twice_is_one_constant() {
394        let (mut func, block) = blank();
395        let mut build = Builder::new(&mut func, block);
396        let first = build.iconst(Type::int(64), 7);
397        let second = build.iconst(Type::int(64), 7);
398        let narrow = build.iconst(Type::int(32), 7);
399        build.ret(&[first, second, narrow]);
400
401        // The immediate table is not interned, so the two sevens are two entries in it and only
402        // reading the bits back out finds that they are the same seven. The third is the same bits
403        // at another width, which is another value.
404        let stats = run(&mut func);
405        assert_eq!(stats.count(Kind::Optimized, MERGED), 1);
406        assert_eq!(count(&func, Opcode::IConst), 2);
407        assert_eq!(returned(&func), vec![first, first, narrow]);
408    }
409
410    #[test]
411    fn two_allocas_are_two_addresses_however_alike_they_look() {
412        let (mut func, block) = blank();
413        let mut build = Builder::new(&mut func, block);
414        let one = local(&mut build);
415        let two = local(&mut build);
416        build.ret(&[one, two]);
417
418        // An `alloca` has effects for exactly this reason. Two of them are two objects and the
419        // program can tell, by comparing their addresses if by nothing else.
420        let stats = run(&mut func);
421        assert!(!stats.changed());
422        assert_eq!(count(&func, Opcode::Alloca), 2);
423    }
424
425    #[test]
426    fn two_loads_of_one_address_are_left_to_the_pass_that_knows_about_memory() {
427        let (mut func, block) = blank();
428        let mut build = Builder::new(&mut func, block);
429        let slot = local(&mut build);
430        let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
431        let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
432        build.ret(&[first, second]);
433
434        // A load is not pure, because what it answers depends on what has been written since. It
435        // is `crate::load` that knows whether anything has been, and this pass never touches one.
436        let stats = run(&mut func);
437        assert!(!stats.changed());
438        assert_eq!(count(&func, Opcode::Load), 2);
439    }
440
441    #[test]
442    fn what_one_block_computes_does_not_reach_the_next_one() {
443        let (mut func, entry) = blank();
444        let next = func.create_block();
445        let mut build = Builder::new(&mut func, entry);
446        let left = build.iconst(Type::int(64), 3);
447        let right = build.iconst(Type::int(64), 5);
448        let first = build.binary(Opcode::Add, left, right, Flags::NONE);
449        build.jump(next, &[]);
450        let mut build = Builder::new(&mut func, next);
451        let second = build.binary(Opcode::Add, left, right, Flags::NONE);
452        build.ret(&[first, second]);
453
454        // The first add dominates the second and the version over the dominator tree takes it.
455        // Section 16.1 is where the argument for this being enough for now lives.
456        let stats = run(&mut func);
457        assert!(!stats.changed());
458        assert_eq!(count(&func, Opcode::Add), 2);
459    }
460
461    #[test]
462    fn one_name_for_the_address_is_what_lets_the_load_be_forwarded() {
463        let (mut func, block) = blank();
464        let mut build = Builder::new(&mut func, block);
465        let base = local(&mut build);
466        let index = build.iconst(Type::int(64), 2);
467        let scale = build.iconst(Type::int(64), 8);
468        let wrote = build.iconst(Type::int(64), 7);
469        let to = build.binary(Opcode::Mul, index, scale, Flags::NONE);
470        let to = build.binary(Opcode::PtrAdd, base, to, Flags::NONE);
471        build.store(wrote, to, plain(8), Flags::NONE);
472        let from = build.binary(Opcode::Mul, index, scale, Flags::NONE);
473        let from = build.binary(Opcode::PtrAdd, base, from, Flags::NONE);
474        let read = build.load(Type::int(64), from, plain(8), Flags::NONE);
475        build.ret(&[read]);
476
477        // This is `a[2] = 7; total += a[2];` as the front end emits it, with the subscript lowered
478        // twice because lowering it does not know it has been lowered already. Before this pass
479        // the store's address and the load's address are two values and `crate::load` compares
480        // addresses by identity, so it refuses. Afterwards they are one value and it forwards.
481        let stats = run(&mut func);
482        assert_eq!(stats.count(Kind::Optimized, ADDRESS), 1);
483        assert_eq!(stats.count(Kind::Optimized, MERGED), 1);
484        assert_eq!(count(&func, Opcode::PtrAdd), 1);
485
486        let mut analyses = crate::machine::fixtures::analyses();
487        let stats = crate::load::LoadForward.run(&mut func, &mut analyses, &mut Fuel::unlimited());
488        assert!(stats.changed(), "the two addresses are one value now");
489        assert_eq!(count(&func, Opcode::Load), 0);
490        assert_eq!(returned(&func), vec![wrote]);
491    }
492
493    #[test]
494    fn an_instruction_with_three_operands_is_matched_on_all_three() {
495        let (mut func, block) = blank();
496        let mut build = Builder::new(&mut func, block);
497        let left = build.iconst(Type::int(64), 3);
498        let right = build.iconst(Type::int(64), 5);
499        let which = build.icmp(IntPred::Slt, left, right);
500        let args = build.func().push_values(&[which, left, right]);
501        let pick = InstData { args, ..InstData::new(Opcode::Select) };
502        let first = build.value(pick, Type::int(64));
503        let second = build.value(pick, Type::int(64));
504        let args = build.func().push_values(&[which, right, left]);
505        let other = InstData { args, ..InstData::new(Opcode::Select) };
506        let other = build.value(other, Type::int(64));
507        build.ret(&[first, second, other]);
508
509        let stats = run(&mut func);
510        assert_eq!(stats.count(Kind::Optimized, MERGED), 1, "the arms the other way round differ");
511        assert_eq!(count(&func, Opcode::Select), 2);
512        assert_eq!(returned(&func), vec![first, first, other]);
513    }
514
515    #[test]
516    fn a_repeated_global_address_is_counted_as_an_address() {
517        let (mut func, block) = blank();
518        let mut names = rucc_base::Interner::new();
519        let global = names.intern("g");
520        let mut build = Builder::new(&mut func, block);
521        let named = InstData { extra: Extra::Symbol(global), ..InstData::new(Opcode::GlobalAddr) };
522        let first = build.value(named, Type::PTR);
523        let second = build.value(named, Type::PTR);
524        let offset = build.iconst(Type::int(64), 8);
525        let one = build.binary(Opcode::PtrAdd, first, offset, Flags::NONE);
526        let two = build.binary(Opcode::PtrAdd, second, offset, Flags::NONE);
527        build.ret(&[one, two]);
528
529        let stats = run(&mut func);
530        assert_eq!(stats.count(Kind::Optimized, ADDRESS), 2);
531        assert_eq!(count(&func, Opcode::GlobalAddr), 1);
532        assert_eq!(operands(&func, one), vec![first, offset]);
533    }
534
535    #[test]
536    fn without_fuel_the_duplicate_stays_and_the_chance_is_still_counted() {
537        let (mut func, block) = blank();
538        let mut build = Builder::new(&mut func, block);
539        let left = build.iconst(Type::int(64), 3);
540        let right = build.iconst(Type::int(64), 5);
541        let first = build.binary(Opcode::Add, left, right, Flags::NONE);
542        let second = build.binary(Opcode::Add, left, right, Flags::NONE);
543        build.ret(&[first, second]);
544
545        let stats =
546            Number.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::of(0));
547        assert!(!stats.changed());
548        assert_eq!(stats.count(Kind::Missed, NO_FUEL), 1);
549        assert_eq!(count(&func, Opcode::Add), 2);
550    }
551}