Skip to main content

rucc_opt/
prune.rs

1//! What the ranges prove cannot happen, taken out of the graph.
2//!
3//! Two transformations, and they are here together because section 24.4 says so: the one on
4//! switches "belongs in the same pass as the range-based branch simplification of document 21".
5//! Both ask document 10's machinery one question and act on a yes, and neither of them can do
6//! anything the other could not have set up, so one walk asking both is cheaper than two walks
7//! asking one each.
8//!
9//! # The branch the ranges decide
10//!
11//! Section 21.1's branch simplification, the third of the three forms it takes. A branch whose
12//! two arms go to the same place is a jump, and [`crate::simplify_cfg`] does that one. A branch on
13//! a constant is a jump, and that one is there too. A branch on something that is not a constant
14//! but that cannot come out any way other than one is a jump as well, and that is this one, because
15//! it is the form that needs an analysis rather than a look at the operand.
16//!
17//! ```c
18//! if (x > 10) { if (x > 5) { f(); } }
19//! ```
20//!
21//! The inner condition is not a constant and no rewrite rule can see it is settled, because what
22//! settles it is the edge the block was reached by rather than anything in the expression. The
23//! ranges see it: on the edge out of `x > 10` where the branch was taken, `x` is in
24//! `[11, INT_MAX]`, and `x > 5` over that range is always true. So the inner branch is a jump to
25//! its taken arm, and the other arm stops being reachable and goes with it.
26//!
27//! # Why this is not jump threading
28//!
29//! [`crate::thread`] asks a related question and gets a different answer. It asks whether the
30//! branch at the end of a block is settled by which edge control arrived on, and its answer is per
31//! edge: an edge whose arrival settles the branch is redirected past it, and the other edges into
32//! the same block are left alone. This asks whether the branch is settled at the block whatever
33//! edge control arrived on, and its answer is per block. Neither subsumes the other. Threading
34//! handles the case where one predecessor knows something the others do not, and pays for it with
35//! a redirected edge or a copied block. This handles the case where the fact holds on every path
36//! in, and pays nothing, but it will not fire where only one path establishes the fact.
37//!
38//! The two also read facts from different distances. Threading looks at the block control came
39//! from. The ranges walk up the dominator tree collecting what every branch above narrowed, so the
40//! `if (x > 10)` above can be any number of blocks away from the `if (x > 5)` and the answer is the
41//! same.
42//!
43//! # The case the ranges rule out
44//!
45//! Section 24.4's one middle end transformation on switches, and the reason document 24 keeps a
46//! `switch` whole through the entire middle end rather than lowering it early. A switch that
47//! survives is a single node whose operand has one range, and a case value outside that range
48//! names an arm nothing can reach.
49//!
50//! ```c
51//! switch (x & 3) { case 0: ...; case 2: ...; case 7: ...; }
52//! ```
53//!
54//! The operand is in `[0, 3]`, so `case 7` is dead. What that buys is more than the compare it
55//! removes. Document 24's lowering decides between a walk, a binary search, a bit test and a jump
56//! table by how dense the case values are, and dropping the outlier is what turns a switch that
57//! looked sparse into one that is dense enough for a table. Section 24.4 puts it this way: it "can
58//! turn a sparse switch into a dense one and change the lowering decision entirely".
59//!
60//! A switch every one of whose cases is ruled out becomes a jump to its default, which is the
61//! same thing happening to the whole node rather than to one arm of it.
62//!
63//! # Answers first, then rewrites
64//!
65//! [`Ranges`] borrows the function, so nothing can be changed while it is alive. The walk
66//! therefore collects every answer, drops the oracle, and then applies them all, rather than the
67//! block at a time shape [`crate::phiopt`] uses.
68//!
69//! That is not only a borrow checker accommodation, it is also cheaper, and the reason it is
70//! sound is worth stating. An answer here is a fact that holds at a block. Applying another answer
71//! removes a branch, which removes edges, and a value's range at a block is the union over the
72//! paths that reach it, so removing a path can only narrow a range and never widen one. A fact
73//! proved before the rewrites is therefore still a fact after them. What can happen is that a
74//! block an answer was about stops being reachable, and an answer about a block nothing reaches is
75//! harmless because the block is about to be swept.
76//!
77//! # What it refuses
78//!
79//! A condition that is already a constant, because that is [`crate::simplify_cfg`]'s branch fold
80//! and two passes doing the same rewrite is two answers to check rather than one. A branch whose
81//! arms go to the same block, for the same reason.
82//!
83//! Everything else it refuses is the oracle saying it does not know, which is not a refusal so
84//! much as the answer, and it is recorded as a miss so that `-fopt-info-all` shows how often the
85//! question was asked and came back empty.
86//!
87//! # Which level
88//!
89//! `-O1` and above. Section 24.4 calls the switch half "cheap, it uses machinery that exists", and
90//! the branch half asks one question per conditional branch rather than one per value, so the
91//! query count is bounded by the number of branches rather than by the size of the function. Both
92//! halves only ever remove code, so `-Os` and `-Oz` want them as much as `-O2` does.
93//!
94//! It runs after [`crate::thread`] and [`crate::phiopt`] and before [`crate::simplify_cfg`], which
95//! is where it has to be at both ends. After, because both of those change the graph and the facts
96//! this reads are about the graph. Before, because what this leaves is a jump where a branch was
97//! and a block with one predecessor where there were two, and forwarding the first and merging the
98//! second is [`crate::simplify_cfg`]'s work rather than a second copy of it here.
99//!
100//! The blocks that stop being reachable are this pass's own problem rather than the cleanup
101//! pass's, because section 6.5 puts that obligation on whichever pass stranded them and the
102//! verifier holds every pass to it. So the walk that takes them out is called from here, and it is
103//! [`crate::simplify_cfg`]'s walk rather than a second one written next door.
104
105use rucc_ir::{Block, BlockCall, Def, Extra, Func, Imm, Inst, IntPred, Opcode, SwitchInfo, Value};
106
107use crate::fold::constant;
108use crate::range::ops::Truth;
109use crate::range::query::Ranges;
110use crate::simplify_cfg;
111use crate::{Analyses, Fuel, Pass, Preserved, Stats};
112
113/// Recorded once for each branch the ranges settled.
114const BRANCH_DECIDED: &str =
115    "branch the value ranges settle whichever way control reached it replaced by a jump";
116
117/// Recorded once for each case value the ranges ruled out.
118const CASE_REMOVED: &str =
119    "case whose value the switched value cannot hold taken out of the switch";
120
121/// Recorded once for a switch none of whose cases can be reached.
122const SWITCH_REMOVED: &str =
123    "switch none of whose cases the switched value can reach replaced by a jump to its default";
124
125/// Recorded once for each branch the ranges were asked about and could not settle.
126const BRANCH_UNDECIDED: &str = "branch kept, the value ranges do not settle which way it goes";
127
128/// Recorded once for each switch the ranges ruled no case out of.
129const NO_CASE_REMOVED: &str = "switch kept whole, the value ranges rule none of its cases out";
130const NO_FUEL: &str = "branch or switch kept, the pass ran out of fuel";
131
132/// The pass.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct Prune;
135
136impl Pass for Prune {
137    fn name(&self) -> &'static str {
138        "prune"
139    }
140
141    fn describe(&self) -> &'static str {
142        "a branch the ranges settle becomes a jump, and a case they rule out leaves its switch"
143    }
144
145    fn preserves(&self) -> Preserved {
146        // Nothing. An arm that stops being reachable is an edge that stops existing, so every
147        // analysis built on the graph was built on a different graph.
148        Preserved::NONE
149    }
150
151    fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
152        let mut stats = Stats::new();
153        if func.entry().is_none() {
154            return stats;
155        }
156        let plan = answers(func, an, &mut stats);
157        if plan.branches.is_empty() && plan.switches.is_empty() {
158            return stats;
159        }
160        'apply: {
161            for (term, call) in plan.branches {
162                if !fuel.take() {
163                    stats.missed(NO_FUEL);
164                    break 'apply;
165                }
166                simplify_cfg::jump_to(func, term, call);
167                stats.optimized(BRANCH_DECIDED);
168            }
169            for (term, keeping) in plan.switches {
170                if !fuel.take() {
171                    stats.missed(NO_FUEL);
172                    break 'apply;
173                }
174                let removed = shrink(func, term, &keeping);
175                for _ in 0..removed {
176                    stats.optimized(CASE_REMOVED);
177                }
178                if keeping.is_empty() {
179                    stats.optimized(SWITCH_REMOVED);
180                }
181            }
182        }
183        // The graph was about the function as it was a moment ago, and the manager clears the
184        // cache after the pass returns, which is too late for the pass itself.
185        an.clear();
186        // Section 6.5 makes taking the stranded blocks out an obligation of whichever pass
187        // stranded them rather than a favour the cleanup pass does, and the verifier holds every
188        // pass to it under `-fverify-each`. An arm that stops being reachable is exactly that, so
189        // the walk is here, and it is [`crate::simplify_cfg`]'s walk rather than a second one
190        // written next door, because two answers about what reachable means is two compilers.
191        simplify_cfg::sweep(func, an, &mut stats);
192        stats
193    }
194}
195
196/// Every rewrite the ranges license, worked out against the function as it stands.
197#[derive(Debug, Default)]
198struct Plan {
199    /// The branches that only go one way, and the edge each of them goes by.
200    branches: Vec<(Inst, BlockCall)>,
201    /// The switches that lose a case, and which of their cases each of them keeps.
202    switches: Vec<(Inst, Vec<usize>)>,
203}
204
205/// Everything the ranges license, worked out against the function as it stands.
206///
207/// One [`Ranges`] for the whole walk rather than one per block, because the oracle caches what it
208/// has worked out and a second one would start from nothing.
209fn answers(func: &Func, an: &mut Analyses, stats: &mut Stats) -> Plan {
210    let mut plan = Plan::default();
211    let cfg = an.cfg(func).clone();
212    let dom = an.dominators(func).clone();
213    let mut ranges = Ranges::new(func, &cfg, &dom);
214    for block in func.blocks() {
215        if !cfg.reaches(block) {
216            continue;
217        }
218        let Some(term) = func.terminator(block) else { continue };
219        match func[term].opcode {
220            Opcode::BrIf => match decided(func, &mut ranges, block, term) {
221                Answer::Jump(call) => plan.branches.push((term, call)),
222                Answer::Unsettled => stats.missed(BRANCH_UNDECIDED),
223                Answer::NotAsked => (),
224            },
225            Opcode::Switch => match reachable(func, &mut ranges, block, term) {
226                Some(keeping) => plan.switches.push((term, keeping)),
227                None => stats.missed(NO_CASE_REMOVED),
228            },
229            _ => (),
230        }
231    }
232    plan
233}
234
235/// What came back about a conditional branch.
236///
237/// Three outcomes rather than two, because a branch this pass declines to look at and a branch it
238/// looked at and could not settle say different things under `-fopt-info-all`. The first is not a
239/// missed optimization at all, it is another pass's fold, and counting it as one would put a
240/// remark on every constant branch in the program saying the ranges failed at something they were
241/// never asked.
242enum Answer {
243    /// The one edge the branch takes.
244    Jump(BlockCall),
245    /// Asked, and the oracle does not know.
246    Unsettled,
247    /// Not this pass's question.
248    NotAsked,
249}
250
251/// The one edge a conditional branch takes, when the ranges say it only has one.
252///
253/// The first target is the one taken when the condition is one, which is what `Builder::br_if`
254/// writes and what the printer reads back, so a condition that always holds takes target zero.
255fn decided(func: &Func, ranges: &mut Ranges<'_>, block: Block, term: Inst) -> Answer {
256    let data = &func[term];
257    let Extra::Targets(targets) = data.extra else { return Answer::NotAsked };
258    let Some(&cond) = func[data.args].first() else { return Answer::NotAsked };
259    // Already a constant, or both arms in one place. Both are [`crate::simplify_cfg`]'s fold and
260    // it runs right after this one, so answering them here would be a second answer to the same
261    // question rather than an answer to one nothing else has.
262    if constant(func, cond).is_some() {
263        return Answer::NotAsked;
264    }
265    let calls = &func[targets];
266    let together =
267        |two: &[BlockCall]| two[0].block == two[1].block && func[two[0].args] == func[two[1].args];
268    if calls.len() == 2 && together(calls) {
269        return Answer::NotAsked;
270    }
271    let arm = match settled(func, ranges, block, cond) {
272        Some(true) => 0,
273        Some(false) => 1,
274        None => return Answer::Unsettled,
275    };
276    func[targets].get(arm).copied().map_or(Answer::NotAsked, Answer::Jump)
277}
278
279/// Whether this condition can only come out one way at this block, and which way that is.
280///
281/// A comparison is asked about through [`Ranges::compare`], which is the entry point section 10.3
282/// puts the relational oracle behind, so a branch on `a < b` under a dominating `a < b` is settled
283/// even where neither value is pinned down to a range that settles it. Anything else is asked as a
284/// range: a one bit value that cannot be zero is true and one that can only be zero is false.
285///
286/// [`crate::header_copy`] asks this too, about the loop entry test it has just put in front of a
287/// loop, because by then this pass has run and the test it wants an answer about did not exist yet.
288/// Section 26.6 wanted that answer from document 10's ranges, and one function answering for both
289/// is what keeps the two passes from disagreeing about the same branch.
290pub(crate) fn settled(
291    func: &Func,
292    ranges: &mut Ranges<'_>,
293    block: Block,
294    cond: Value,
295) -> Option<bool> {
296    if let Some((pred, lhs, rhs)) = comparison(func, cond) {
297        return match ranges.compare(pred, lhs, rhs, block) {
298            Truth::Always => Some(true),
299            Truth::Never => Some(false),
300            Truth::Either => None,
301        };
302    }
303    let range = ranges.at(cond, block);
304    if range.nonzero() {
305        return Some(true);
306    }
307    (range.singleton() == Some(0)).then_some(false)
308}
309
310/// The comparison behind this value, if it is one.
311fn comparison(func: &Func, value: Value) -> Option<(IntPred, Value, Value)> {
312    let Def::Result { inst, .. } = func[value].def else { return None };
313    if func[inst].opcode != Opcode::ICmp {
314        return None;
315    }
316    let Extra::IntPred(pred) = func[inst].extra else { return None };
317    let &[lhs, rhs] = func[func[inst].args].first_chunk::<2>()?;
318    Some((pred, lhs, rhs))
319}
320
321/// Which of a switch's cases the switched value can still hold, when that is not all of them.
322///
323/// The answer is the places the surviving cases are in, in the order they were in, and `None` is
324/// the switch that keeps every case rather than the switch that keeps none. An empty list is the
325/// switch none of whose cases can be reached, which becomes a jump to its default.
326fn reachable(func: &Func, ranges: &mut Ranges<'_>, block: Block, term: Inst) -> Option<Vec<usize>> {
327    let Extra::Switch(at) = func[term].extra else { return None };
328    let info = func[at];
329    let arg = *func[func[term].args].first()?;
330    let range = ranges.at(arg, block);
331    if range.is_full() {
332        return None;
333    }
334    let cases = &func[info.cases];
335    let keeping: Vec<usize> =
336        (0..cases.len()).filter(|&at| range.contains(cases[at].unsigned())).collect();
337    (keeping.len() < cases.len()).then_some(keeping)
338}
339
340/// Rewrites a switch to the cases in that list, and says how many it dropped.
341///
342/// A switch left with no cases is a jump to its default, because a decision tree over nothing is
343/// the default arm and document 24's lowering would rather not be handed one.
344fn shrink(func: &mut Func, term: Inst, keeping: &[usize]) -> usize {
345    let Extra::Switch(at) = func[term].extra else { return 0 };
346    let info = func[at];
347    let all = func[info.targets].to_vec();
348    let values = func[info.cases].to_vec();
349    let removed = values.len() - keeping.len();
350    // The default is the first target and the cases follow it in the order their values are in,
351    // so the target for the case in place `at` is one past it.
352    let default = all[0];
353    if keeping.is_empty() {
354        simplify_cfg::jump_to(func, term, default);
355        return removed;
356    }
357    let targets: Vec<BlockCall> =
358        std::iter::once(default).chain(keeping.iter().map(|&at| all[at + 1])).collect();
359    let cases: Vec<Imm> = keeping.iter().map(|&at| values[at]).collect();
360    let targets = func.push_block_calls(&targets);
361    let cases = func.push_imms(&cases);
362    let fresh = func.add_switch(SwitchInfo { targets, cases });
363    func[term].extra = Extra::Switch(fresh);
364    removed
365}
366
367#[cfg(test)]
368mod tests {
369    use rucc_base::Interner;
370    use rucc_ir::{Block, Builder, Flags, Func, IntPred, Opcode, Signature, Type};
371
372    use super::Prune;
373    use crate::stats::Kind;
374    use crate::{Fuel, Pass, Stats};
375
376    /// Runs the pass with as much fuel as it wants.
377    fn prune(func: &mut Func) -> Stats {
378        Prune.run(func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited())
379    }
380
381    /// The opcode of a block's terminator.
382    fn terminator(func: &Func, block: usize) -> Opcode {
383        let block = Block::from_usize(block);
384        func[func.terminator(block).expect("every block here has one")].opcode
385    }
386
387    /// The blocks a block's terminator names, in the order it names them.
388    fn goes_to(func: &Func, block: usize) -> Vec<usize> {
389        let block = Block::from_usize(block);
390        let term = func.terminator(block).expect("every block here has one");
391        func.successors(term).map(|call| call.block.index()).collect()
392    }
393
394    /// Two nested branches on the same value, the outer one narrowing it for the inner one.
395    ///
396    /// Block 0 branches on `x <outer> bound`, block 1 branches on `x <inner> 5`, and blocks 2 and
397    /// 3 are the inner branch's two arms. Block 4 is where the outer branch goes when it does not
398    /// hold, and it is there so that the inner block has one predecessor rather than being the
399    /// entry's only successor.
400    fn nested(outer: IntPred, bound: i128, inner: IntPred) -> Func {
401        let mut names = Interner::new();
402        let ty = Type::int(32);
403        let mut func = Func::new(names.intern("f"), Signature::new().with_params(&[ty]));
404        let entry = func.create_block();
405        let middle = func.create_block();
406        let arms = [func.create_block(), func.create_block()];
407        let away = func.create_block();
408        let x = func.append_param(entry, ty);
409        let mut build = Builder::new(&mut func, entry);
410        let edge = build.iconst(ty, bound);
411        let first = build.icmp(outer, x, edge);
412        build.br_if(first, middle, &[], away, &[]);
413        let mut build = Builder::new(&mut func, middle);
414        let five = build.iconst(ty, 5);
415        let second = build.icmp(inner, x, five);
416        build.br_if(second, arms[0], &[], arms[1], &[]);
417        for block in [arms[0], arms[1], away] {
418            let mut build = Builder::new(&mut func, block);
419            build.ret(&[]);
420        }
421        func
422    }
423
424    #[test]
425    fn a_branch_the_ranges_settle_becomes_a_jump_to_the_arm_they_settle_on() {
426        // `if (x > 10) { if (x > 5) ... }`. On the edge into block 1 the value is at least 11, so
427        // the second comparison holds there whatever else is true, and the arm it does not take
428        // stops being reachable.
429        let mut func = nested(IntPred::Sgt, 10, IntPred::Sgt);
430        let stats = prune(&mut func);
431        assert!(stats.changed());
432        assert_eq!(terminator(&func, 1), Opcode::Jump);
433        assert_eq!(goes_to(&func, 1), [2]);
434        assert_eq!(stats.count(Kind::Optimized, super::BRANCH_DECIDED), 1);
435    }
436
437    #[test]
438    fn a_branch_the_ranges_settle_the_other_way_jumps_to_the_other_arm() {
439        // `if (x > 10) { if (x < 5) ... }`, where the inner comparison cannot hold. The pass has
440        // to name the second target rather than the first, and getting that backwards would build
441        // a compiler that quietly runs the wrong arm.
442        let mut func = nested(IntPred::Sgt, 10, IntPred::Slt);
443        let stats = prune(&mut func);
444        assert!(stats.changed());
445        assert_eq!(terminator(&func, 1), Opcode::Jump);
446        assert_eq!(goes_to(&func, 1), [3]);
447    }
448
449    #[test]
450    fn a_branch_the_ranges_do_not_settle_keeps_its_two_arms() {
451        // `if (x > 10) { if (x > 20) ... }` the other way round. Being over 10 says nothing about
452        // being over 20, so both arms are still reachable and the branch stays.
453        let mut func = nested(IntPred::Sgt, 3, IntPred::Sgt);
454        let stats = prune(&mut func);
455        assert!(!stats.changed());
456        assert_eq!(terminator(&func, 1), Opcode::BrIf);
457        assert_eq!(stats.count(Kind::Missed, super::BRANCH_UNDECIDED), 2);
458    }
459
460    #[test]
461    fn a_branch_on_a_constant_is_left_for_the_control_flow_pass() {
462        // Two passes writing the same rewrite is two answers to check, and this one is section
463        // 21.1's rather than section 10's. It is refused before the oracle is asked at all.
464        let mut names = Interner::new();
465        let mut func = Func::new(names.intern("f"), Signature::new());
466        let entry = func.create_block();
467        let arms = [func.create_block(), func.create_block()];
468        let mut build = Builder::new(&mut func, entry);
469        let always = build.iconst(Type::int(1), 1);
470        build.br_if(always, arms[0], &[], arms[1], &[]);
471        for block in arms {
472            let mut build = Builder::new(&mut func, block);
473            build.ret(&[]);
474        }
475        let stats = prune(&mut func);
476        assert!(!stats.changed());
477        assert_eq!(terminator(&func, 0), Opcode::BrIf);
478        assert_eq!(stats.count(Kind::Missed, super::BRANCH_UNDECIDED), 0);
479    }
480
481    /// A switch on `x & mask`, with those case values and a default.
482    fn masked(mask: i128, cases: &[i128]) -> Func {
483        let mut names = Interner::new();
484        let ty = Type::int(32);
485        let mut func = Func::new(names.intern("f"), Signature::new().with_params(&[ty]));
486        let entry = func.create_block();
487        let default = func.create_block();
488        let arms: Vec<Block> = cases.iter().map(|_| func.create_block()).collect();
489        let x = func.append_param(entry, ty);
490        let mut build = Builder::new(&mut func, entry);
491        let bits = build.iconst(ty, mask);
492        let narrowed = build.binary(Opcode::And, x, bits, Flags::NONE);
493        let pairs: Vec<(i128, Block)> =
494            cases.iter().copied().zip(arms.iter().copied()).collect::<Vec<_>>();
495        build.switch(narrowed, default, &pairs);
496        for block in std::iter::once(default).chain(arms) {
497            let mut build = Builder::new(&mut func, block);
498            build.ret(&[]);
499        }
500        func
501    }
502
503    #[test]
504    fn a_case_the_switched_value_cannot_hold_leaves_the_switch() {
505        // `switch (x & 3) { case 0: case 2: case 7: }`. The operand is in [0, 3], so the last
506        // case names an arm nothing reaches, and the two that are left are what document 24's
507        // lowering gets to decide over.
508        let mut func = masked(3, &[0, 2, 7]);
509        let stats = prune(&mut func);
510        assert!(stats.changed());
511        assert_eq!(terminator(&func, 0), Opcode::Switch);
512        // The default first and the surviving cases after it, in the order they were in.
513        assert_eq!(goes_to(&func, 0), [1, 2, 3]);
514        assert_eq!(stats.count(Kind::Optimized, super::CASE_REMOVED), 1);
515    }
516
517    #[test]
518    fn a_switch_no_case_of_which_can_be_reached_jumps_to_its_default() {
519        // Every case is outside the operand's range, so the whole node goes rather than an arm
520        // of it. What is left is the default, which is where control was always going.
521        let mut func = masked(1, &[5, 9]);
522        let stats = prune(&mut func);
523        assert!(stats.changed());
524        assert_eq!(terminator(&func, 0), Opcode::Jump);
525        assert_eq!(goes_to(&func, 0), [1]);
526        assert_eq!(stats.count(Kind::Optimized, super::SWITCH_REMOVED), 1);
527    }
528
529    #[test]
530    fn a_switch_whose_cases_the_operand_can_all_hold_is_kept_whole() {
531        let mut func = masked(7, &[0, 2, 7]);
532        let stats = prune(&mut func);
533        assert!(!stats.changed());
534        assert_eq!(terminator(&func, 0), Opcode::Switch);
535        assert_eq!(stats.count(Kind::Missed, super::NO_CASE_REMOVED), 1);
536    }
537
538    #[test]
539    fn a_branch_two_values_are_related_on_settles_without_either_being_pinned_down() {
540        // The oracle's half rather than the ranges' half, and the case section 10.3 says it is
541        // for. Nothing here says what `a` or `b` can be, so no interval settles the second
542        // comparison. What settles it is that the edge into block 1 recorded that `a < b`.
543        let mut names = Interner::new();
544        let ty = Type::int(32);
545        let mut func = Func::new(names.intern("f"), Signature::new().with_params(&[ty, ty]));
546        let entry = func.create_block();
547        let middle = func.create_block();
548        let arms = [func.create_block(), func.create_block()];
549        let away = func.create_block();
550        let a = func.append_param(entry, ty);
551        let b = func.append_param(entry, ty);
552        let mut build = Builder::new(&mut func, entry);
553        let first = build.icmp(IntPred::Slt, a, b);
554        build.br_if(first, middle, &[], away, &[]);
555        let mut build = Builder::new(&mut func, middle);
556        // A second comparison of the same two values, written again rather than reused, which is
557        // what a program with the test in two places hands the optimizer.
558        let second = build.icmp(IntPred::Sle, a, b);
559        build.br_if(second, arms[0], &[], arms[1], &[]);
560        for block in [arms[0], arms[1], away] {
561            let mut build = Builder::new(&mut func, block);
562            build.ret(&[]);
563        }
564        let stats = prune(&mut func);
565        assert!(stats.changed());
566        assert_eq!(terminator(&func, 1), Opcode::Jump);
567        assert_eq!(goes_to(&func, 1), [2]);
568    }
569
570    #[test]
571    fn a_value_that_cannot_be_zero_is_a_branch_that_always_holds() {
572        // `if (x == 5) { if ((_Bool)x) ... }`. The condition is a truncation rather than a
573        // comparison, so what answers is the range on its own: on the edge into block 1 the value
574        // is exactly five, five truncated to one bit is one, and one is not zero.
575        let mut names = Interner::new();
576        let wide = Type::int(32);
577        let mut func = Func::new(names.intern("f"), Signature::new().with_params(&[wide]));
578        let entry = func.create_block();
579        let middle = func.create_block();
580        let arms = [func.create_block(), func.create_block()];
581        let away = func.create_block();
582        let x = func.append_param(entry, wide);
583        let mut build = Builder::new(&mut func, entry);
584        let five = build.iconst(wide, 5);
585        let is_five = build.icmp(IntPred::Eq, x, five);
586        build.br_if(is_five, middle, &[], away, &[]);
587        let mut build = Builder::new(&mut func, middle);
588        let bit = build.unary(Opcode::Trunc, x, Type::int(1));
589        build.br_if(bit, arms[0], &[], arms[1], &[]);
590        for block in [arms[0], arms[1], away] {
591            let mut build = Builder::new(&mut func, block);
592            build.ret(&[]);
593        }
594        let stats = prune(&mut func);
595        assert!(stats.changed());
596        assert_eq!(terminator(&func, 1), Opcode::Jump);
597        assert_eq!(goes_to(&func, 1), [2]);
598    }
599
600    #[test]
601    fn a_function_with_no_body_is_left_alone() {
602        let mut names = Interner::new();
603        let mut func = Func::new(names.intern("f"), Signature::new());
604        let stats = prune(&mut func);
605        assert!(!stats.changed());
606    }
607
608    #[test]
609    fn no_fuel_leaves_the_branch_where_it_is() {
610        let mut func = nested(IntPred::Sgt, 10, IntPred::Sgt);
611        let stats =
612            Prune.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::of(0));
613        assert!(!stats.changed());
614        assert_eq!(terminator(&func, 1), Opcode::BrIf);
615        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
616    }
617}