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 {
273 size: 0,
274 align,
275 order: MemOrder::NotAtomic,
276 tbaa: None,
277 owns: 0,
278 restrict: Restrict::NONE,
279 }
280 }
281
282 /// An `alloca` of thirty-two bytes, which is an address nothing outside the function knows.
283 fn local(build: &mut Builder<'_>) -> Value {
284 let mem = build.func().add_mem(MemInfo { size: 32, ..plain(8) });
285 build.value(InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) }, Type::PTR)
286 }
287
288 /// Runs the pass over the function with as much fuel as it wants.
289 fn run(func: &mut Func) -> Stats {
290 Number.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
291 }
292
293 /// How many instructions of that opcode are left in the function.
294 fn count(func: &Func, opcode: Opcode) -> usize {
295 func.blocks()
296 .flat_map(|block| func.insts(block).collect::<Vec<Inst>>())
297 .filter(|&inst| func[inst].opcode == opcode)
298 .count()
299 }
300
301 /// What the return statement hands back, after the pass has pointed it somewhere.
302 fn returned(func: &Func) -> Vec<Value> {
303 let block = func.blocks().last().expect("the function has a block");
304 let inst = func.terminator(block).expect("the block has a terminator");
305 func[func[inst].args].to_vec()
306 }
307
308 /// The operands of whatever instruction produced that value.
309 fn operands(func: &Func, value: Value) -> Vec<Value> {
310 let Def::Result { inst, .. } = func[value].def else { panic!("not an instruction result") };
311 func[func[inst].args].to_vec()
312 }
313
314 #[test]
315 fn the_same_arithmetic_on_the_same_operands_twice_is_one_instruction() {
316 let (mut func, block) = blank();
317 let mut build = Builder::new(&mut func, block);
318 let left = build.iconst(Type::int(64), 3);
319 let right = build.iconst(Type::int(64), 5);
320 let first = build.binary(Opcode::Add, left, right, Flags::NONE);
321 let second = build.binary(Opcode::Add, left, right, Flags::NONE);
322 build.ret(&[first, second]);
323
324 let stats = run(&mut func);
325 assert_eq!(stats.count(Kind::Optimized, MERGED), 1);
326 assert_eq!(count(&func, Opcode::Add), 1);
327 assert_eq!(returned(&func), vec![first, first]);
328 }
329
330 #[test]
331 fn a_commutative_pair_matches_with_its_operands_the_other_way_round() {
332 let (mut func, block) = blank();
333 let mut build = Builder::new(&mut func, block);
334 let left = build.iconst(Type::int(64), 3);
335 let right = build.iconst(Type::int(64), 5);
336 let first = build.binary(Opcode::Add, left, right, Flags::NONE);
337 let second = build.binary(Opcode::Add, right, left, Flags::NONE);
338 build.ret(&[first, second]);
339
340 let stats = run(&mut func);
341 assert_eq!(stats.count(Kind::Optimized, MERGED), 1);
342 assert_eq!(returned(&func), vec![first, first]);
343 }
344
345 #[test]
346 fn a_subtraction_the_other_way_round_is_a_different_answer() {
347 let (mut func, block) = blank();
348 let mut build = Builder::new(&mut func, block);
349 let left = build.iconst(Type::int(64), 3);
350 let right = build.iconst(Type::int(64), 5);
351 let first = build.binary(Opcode::Sub, left, right, Flags::NONE);
352 let second = build.binary(Opcode::Sub, right, left, Flags::NONE);
353 build.ret(&[first, second]);
354
355 let stats = run(&mut func);
356 assert!(!stats.changed(), "three minus five is not five minus three");
357 assert_eq!(count(&func, Opcode::Sub), 2);
358 }
359
360 #[test]
361 fn two_adds_that_promise_different_things_stay_two_adds() {
362 let (mut func, block) = blank();
363 let mut build = Builder::new(&mut func, block);
364 let left = build.iconst(Type::int(64), 3);
365 let right = build.iconst(Type::int(64), 5);
366 let first = build.binary(Opcode::Add, left, right, Flags::NSW);
367 let second = build.binary(Opcode::Add, left, right, Flags::NONE);
368 build.ret(&[first, second]);
369
370 // Merging them onto the first hands the second a promise nobody made about it, and
371 // merging them onto the second throws away a promise somebody did make.
372 let stats = run(&mut func);
373 assert!(!stats.changed());
374 assert_eq!(count(&func, Opcode::Add), 2);
375 }
376
377 #[test]
378 fn a_chain_collapses_all_the_way_up_and_not_just_at_the_bottom() {
379 let (mut func, block) = blank();
380 let mut build = Builder::new(&mut func, block);
381 let index = build.iconst(Type::int(64), 2);
382 let scale = build.iconst(Type::int(64), 8);
383 let first = build.binary(Opcode::Mul, index, scale, Flags::NONE);
384 let second = build.binary(Opcode::Mul, index, scale, Flags::NONE);
385 let up = build.binary(Opcode::Add, first, scale, Flags::NONE);
386 let down = build.binary(Opcode::Add, second, scale, Flags::NONE);
387 build.ret(&[up, down]);
388
389 // The second add's operand is a value on its way out, so it has to be compared as the
390 // value it is on its way out in favour of. Without that the pass takes the multiply and
391 // stops, which on a subscript is the bottom instruction of three or four.
392 let stats = run(&mut func);
393 assert_eq!(stats.count(Kind::Optimized, MERGED), 2);
394 assert_eq!(count(&func, Opcode::Mul), 1);
395 assert_eq!(count(&func, Opcode::Add), 1);
396 assert_eq!(returned(&func), vec![up, up]);
397 }
398
399 #[test]
400 fn the_same_constant_written_twice_is_one_constant() {
401 let (mut func, block) = blank();
402 let mut build = Builder::new(&mut func, block);
403 let first = build.iconst(Type::int(64), 7);
404 let second = build.iconst(Type::int(64), 7);
405 let narrow = build.iconst(Type::int(32), 7);
406 build.ret(&[first, second, narrow]);
407
408 // The immediate table is not interned, so the two sevens are two entries in it and only
409 // reading the bits back out finds that they are the same seven. The third is the same bits
410 // at another width, which is another value.
411 let stats = run(&mut func);
412 assert_eq!(stats.count(Kind::Optimized, MERGED), 1);
413 assert_eq!(count(&func, Opcode::IConst), 2);
414 assert_eq!(returned(&func), vec![first, first, narrow]);
415 }
416
417 #[test]
418 fn two_allocas_are_two_addresses_however_alike_they_look() {
419 let (mut func, block) = blank();
420 let mut build = Builder::new(&mut func, block);
421 let one = local(&mut build);
422 let two = local(&mut build);
423 build.ret(&[one, two]);
424
425 // An `alloca` has effects for exactly this reason. Two of them are two objects and the
426 // program can tell, by comparing their addresses if by nothing else.
427 let stats = run(&mut func);
428 assert!(!stats.changed());
429 assert_eq!(count(&func, Opcode::Alloca), 2);
430 }
431
432 #[test]
433 fn two_loads_of_one_address_are_left_to_the_pass_that_knows_about_memory() {
434 let (mut func, block) = blank();
435 let mut build = Builder::new(&mut func, block);
436 let slot = local(&mut build);
437 let first = build.load(Type::int(64), slot, plain(8), Flags::NONE);
438 let second = build.load(Type::int(64), slot, plain(8), Flags::NONE);
439 build.ret(&[first, second]);
440
441 // A load is not pure, because what it answers depends on what has been written since. It
442 // is `crate::load` that knows whether anything has been, and this pass never touches one.
443 let stats = run(&mut func);
444 assert!(!stats.changed());
445 assert_eq!(count(&func, Opcode::Load), 2);
446 }
447
448 #[test]
449 fn what_one_block_computes_does_not_reach_the_next_one() {
450 let (mut func, entry) = blank();
451 let next = func.create_block();
452 let mut build = Builder::new(&mut func, entry);
453 let left = build.iconst(Type::int(64), 3);
454 let right = build.iconst(Type::int(64), 5);
455 let first = build.binary(Opcode::Add, left, right, Flags::NONE);
456 build.jump(next, &[]);
457 let mut build = Builder::new(&mut func, next);
458 let second = build.binary(Opcode::Add, left, right, Flags::NONE);
459 build.ret(&[first, second]);
460
461 // The first add dominates the second and the version over the dominator tree takes it.
462 // Section 16.1 is where the argument for this being enough for now lives.
463 let stats = run(&mut func);
464 assert!(!stats.changed());
465 assert_eq!(count(&func, Opcode::Add), 2);
466 }
467
468 #[test]
469 fn one_name_for_the_address_is_what_lets_the_load_be_forwarded() {
470 let (mut func, block) = blank();
471 let mut build = Builder::new(&mut func, block);
472 let base = local(&mut build);
473 let index = build.iconst(Type::int(64), 2);
474 let scale = build.iconst(Type::int(64), 8);
475 let wrote = build.iconst(Type::int(64), 7);
476 let to = build.binary(Opcode::Mul, index, scale, Flags::NONE);
477 let to = build.binary(Opcode::PtrAdd, base, to, Flags::NONE);
478 build.store(wrote, to, plain(8), Flags::NONE);
479 let from = build.binary(Opcode::Mul, index, scale, Flags::NONE);
480 let from = build.binary(Opcode::PtrAdd, base, from, Flags::NONE);
481 let read = build.load(Type::int(64), from, plain(8), Flags::NONE);
482 build.ret(&[read]);
483
484 // This is `a[2] = 7; total += a[2];` as the front end emits it, with the subscript lowered
485 // twice because lowering it does not know it has been lowered already. Before this pass
486 // the store's address and the load's address are two values and `crate::load` compares
487 // addresses by identity, so it refuses. Afterwards they are one value and it forwards.
488 let stats = run(&mut func);
489 assert_eq!(stats.count(Kind::Optimized, ADDRESS), 1);
490 assert_eq!(stats.count(Kind::Optimized, MERGED), 1);
491 assert_eq!(count(&func, Opcode::PtrAdd), 1);
492
493 let mut analyses = crate::machine::fixtures::analyses();
494 let stats = crate::load::LoadForward.run(&mut func, &mut analyses, &mut Fuel::unlimited());
495 assert!(stats.changed(), "the two addresses are one value now");
496 assert_eq!(count(&func, Opcode::Load), 0);
497 assert_eq!(returned(&func), vec![wrote]);
498 }
499
500 #[test]
501 fn an_instruction_with_three_operands_is_matched_on_all_three() {
502 let (mut func, block) = blank();
503 let mut build = Builder::new(&mut func, block);
504 let left = build.iconst(Type::int(64), 3);
505 let right = build.iconst(Type::int(64), 5);
506 let which = build.icmp(IntPred::Slt, left, right);
507 let args = build.func().push_values(&[which, left, right]);
508 let pick = InstData { args, ..InstData::new(Opcode::Select) };
509 let first = build.value(pick, Type::int(64));
510 let second = build.value(pick, Type::int(64));
511 let args = build.func().push_values(&[which, right, left]);
512 let other = InstData { args, ..InstData::new(Opcode::Select) };
513 let other = build.value(other, Type::int(64));
514 build.ret(&[first, second, other]);
515
516 let stats = run(&mut func);
517 assert_eq!(stats.count(Kind::Optimized, MERGED), 1, "the arms the other way round differ");
518 assert_eq!(count(&func, Opcode::Select), 2);
519 assert_eq!(returned(&func), vec![first, first, other]);
520 }
521
522 #[test]
523 fn a_repeated_global_address_is_counted_as_an_address() {
524 let (mut func, block) = blank();
525 let mut names = rucc_base::Interner::new();
526 let global = names.intern("g");
527 let mut build = Builder::new(&mut func, block);
528 let named = InstData { extra: Extra::Symbol(global), ..InstData::new(Opcode::GlobalAddr) };
529 let first = build.value(named, Type::PTR);
530 let second = build.value(named, Type::PTR);
531 let offset = build.iconst(Type::int(64), 8);
532 let one = build.binary(Opcode::PtrAdd, first, offset, Flags::NONE);
533 let two = build.binary(Opcode::PtrAdd, second, offset, Flags::NONE);
534 build.ret(&[one, two]);
535
536 let stats = run(&mut func);
537 assert_eq!(stats.count(Kind::Optimized, ADDRESS), 2);
538 assert_eq!(count(&func, Opcode::GlobalAddr), 1);
539 assert_eq!(operands(&func, one), vec![first, offset]);
540 }
541
542 #[test]
543 fn without_fuel_the_duplicate_stays_and_the_chance_is_still_counted() {
544 let (mut func, block) = blank();
545 let mut build = Builder::new(&mut func, block);
546 let left = build.iconst(Type::int(64), 3);
547 let right = build.iconst(Type::int(64), 5);
548 let first = build.binary(Opcode::Add, left, right, Flags::NONE);
549 let second = build.binary(Opcode::Add, left, right, Flags::NONE);
550 build.ret(&[first, second]);
551
552 let stats =
553 Number.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::of(0));
554 assert!(!stats.changed());
555 assert_eq!(stats.count(Kind::Missed, NO_FUEL), 1);
556 assert_eq!(count(&func, Opcode::Add), 2);
557 }
558}