Skip to main content

rucc_opt/
thread.rs

1//! Jump threading, the part of it that does not copy anything.
2//!
3//! Design: `spec/optimizer/23-jump-threading.md`. If, on the path through block A into block B, the
4//! condition B tests is already decided, then A should branch straight to the arm B was going to
5//! take and skip B's test. On real C that removes more branches than anything else in the compiler,
6//! because C is full of conditions that are redundant along some paths and not along others.
7//!
8//! It is also the pass most likely to explode, because the general form works by copying B, and a
9//! copy grows the function, and the growth compounds because each thread makes new paths on which
10//! further threading is possible. Section 23.4 is four separate limits on that growth and section
11//! 23.6 names the subset where there is none: the case where the block being threaded past does not
12//! have to be copied at all, which is pure edge redirection. That subset is what is here.
13//!
14//! # What decides a branch here, and what does not
15//!
16//! Arguments in this IR live on the edge rather than in the block, so a block parameter is a value
17//! that arrives differently depending on which way control came. Bind a block's parameters to what
18//! one edge carries and its terminator may resolve on that edge while resolving on no other, which
19//! is the whole of the path sensitivity this pass has. It covers section 23.3's example directly:
20//!
21//! ```c
22//! if (a) x = 1; else x = 2;
23//! if (x == 1) ...
24//! ```
25//!
26//! Nothing dominating the second test decides it, so the forward threader of section 23.2 cannot
27//! see it and neither can `simplify-cfg`. But `x` arrives at the second test as a block parameter,
28//! it is 1 along one edge and 2 along the other, and both edges resolve. Both are threaded, nothing
29//! is left reaching the block, and the second branch goes.
30//!
31//! What is not here is section 23.3's backward search with the path-sensitive range solver. This
32//! asks about one edge and not about a path of them, so a condition decided two blocks back and not
33//! one is a condition this does not see. The range machinery for that exists in [`crate::range`] and
34//! the search is the larger half of the document.
35//!
36//! # Why no block has to be copied
37//!
38//! Section 23.1 quotes GCC's six step surgery, whose first step is a copy of B. The copy exists so
39//! that B's side effects still happen on the threaded path and so that the values B defines are
40//! available to the arm the thread lands on. Where neither is needed, neither is the copy, and this
41//! pass threads exactly the edges where neither is needed:
42//!
43//! - Every instruction in B other than its terminator has no effects, so a path that skips them
44//!   skips nothing that had to happen. That is the same predicate [`crate::dce`] deletes an
45//!   instruction under, which is the point: an instruction it would delete outright is one a path
46//!   can walk past.
47//! - Nothing outside B reads a value B defines. Those are the values the copy would have existed to
48//!   compute, and both the arm's arguments and the blocks further down are asking for them.
49//!
50//! The second condition has to be about the whole function and not just about the arm. An argument
51//! is how a value crosses into a block that B does not dominate, but a block B does dominate reads
52//! what B defined with no argument at all, because dominance is the only permission a use needs.
53//! Threading an edge past B takes that dominance away, and the read is then of a value that was
54//! never computed on the path taken. Checking only the arm's arguments misses exactly that, which is
55//! what `a_value_the_block_defines_and_something_below_it_reads_needs_the_copy` is about.
56//!
57//! B's parameters are covered by the same rule, since a parameter is a value B defines. Along the
58//! edge being redirected they are known, so B's own reads of them are substituted rather than
59//! refused, but a read from below is a read of a value that is about to stop existing. And a value
60//! the arm carries that is defined outside B dominates the block it is being carried out of, so it
61//! dominates the predecessor as well: it is on every path to B, the predecessor has an edge to B, so
62//! it is on every path to the predecessor. That is section 23.1's "the values must still dominate",
63//! and it is the same argument `spec/optimizer/21-cfg-simplification.md` section 21.4 needs for
64//! forwarder removal.
65//!
66//! # The loop rules, which are refusals and not scores
67//!
68//! Section 23.5. Threading a path into a loop somewhere other than its header makes an irreducible
69//! loop, and document 06.4 established that rucc does not split nodes and gives up on irreducible
70//! regions instead. So the rule here is stronger than GCC's, where it is one input to a cost model:
71//! a thread that would do it is refused, at every level. A predecessor that is a latch is refused
72//! too, because moving a latch's edge is how the single latch property document 07.3 wants stops
73//! being true. And a block already in an irreducible region is left alone entirely, since the loop
74//! forest has given up on it and the two checks above would be reading an answer nobody stands
75//! behind.
76//!
77//! Because nothing is copied, no new cycle can appear. The new edge from A goes where the edge out
78//! of B went, so a path along it is a path that was already there with B taken out of the middle.
79//! Loops can therefore only be destroyed, and the loop forest is rebuilt after each thread anyway,
80//! which is what keeps the next decision honest.
81//!
82//! # Which level this runs at
83//!
84//! Every level that optimizes, including `-Os` and `-Oz`. Section 23.6 restricts threading at those
85//! two to the case where the block is empty, on the ground that it is the only part that is free,
86//! and this pass is that part generalized: a block whose instructions all have no effects and whose
87//! outgoing arguments do not come from it costs the same as an empty one, which is nothing.
88//!
89//! Once, and not to a fixed point. Threading enables threading, and section 23.7 says the answer to
90//! that is a fixed number of instances rather than a loop, because threading is the pass where
91//! adversarial input is easiest to construct. Section 23.5 asks for two instances at `-O2`, an early
92//! one and a late one after the loop pipeline and SCCP. There is one here, in the early position.
93//! The late one wants the passes that are not written yet.
94//!
95//! # What it counts
96//!
97//! Every refusal is recorded, and they are the measurement section 23.8 asks this document for.
98//! Three of them count edges that decide a branch this pass cannot thread without the copy, split by
99//! which part of the copy is in the way: something in the block that has to happen, a value the arm
100//! carries that the block worked out, and a value the block defines that a block below it reads.
101//! Together they are the size of the prize for building section 23.1's surgery, and separately they
102//! say what the surgery has to do first. The fourth counts edges refused on loop structure, which is
103//! the price of document 06.4's position on irreducible regions stated as a number rather than as an
104//! argument.
105//!
106//! On the 1461 programs of the corpus at `-O2`, 623 edges decide the branch they arrive at and one
107//! of them is threadable without a copy. So the subset that is free is close to worthless on real C,
108//! and this pass earns its place by measuring that rather than by what it removes. The 623 is the
109//! number that justifies the rest of document 23.
110//!
111//! The split says where the rest of the work is. 619 of the 623 are blocked on a value the block
112//! defines being read below it, 4 on the arm carrying one, and none at all on the block doing
113//! something that has to happen. That is one conclusion rather than three: the block being threaded
114//! past is almost never doing work that matters, it is holding a value that matters, so section
115//! 23.1's copy is there to reconstruct values and not to repeat effects. A cheaper thing than a full
116//! block copy might do it, and that is worth knowing before the surgery is written rather than
117//! after.
118
119use std::collections::HashSet;
120
121use rucc_base::Idx;
122use rucc_ir::{Block, BlockCall, Def, Func, Inst, Opcode, Value};
123
124use crate::simplify_cfg::{Bindings, Edges, incoming, sweep, taken};
125use crate::{Analyses, Fuel, Loops, Pass, Preserved, Stats, uses};
126
127/// Recorded once for each edge that was pointed past a branch it decides.
128const THREADED: &str =
129    "edge pointed straight at the arm of the branch it arrives at that it decides";
130
131/// Recorded for an edge that would have been threaded if there had been fuel for it.
132const NO_FUEL: &str = "edge left on a branch it decides, the pass ran out of fuel";
133
134/// Recorded for an edge whose block does something a path through it cannot skip.
135const WOULD_COPY_EFFECT: &str =
136    "edge decides the branch it arrives at, but something in the block has to happen on the way";
137
138/// Recorded for an edge whose block defines a value read below it.
139const WOULD_COPY_READ_BELOW: &str =
140    "edge decides the branch it arrives at, but a block below reads a value this one defines";
141
142/// Recorded for an edge whose arm carries a value the block itself computed.
143const WOULD_COPY_CARRIED: &str =
144    "edge decides the branch it arrives at, but the arm carries a value the block works out";
145
146/// Recorded for an edge that decides a branch but whose thread would spoil the loop forest.
147const WOULD_BREAK_A_LOOP: &str =
148    "edge decides the branch it arrives at, but threading it would give a loop a second way in";
149
150/// The pass.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub struct Thread;
153
154impl Pass for Thread {
155    fn name(&self) -> &'static str {
156        "thread"
157    }
158
159    fn describe(&self) -> &'static str {
160        "an edge that already decides the branch it arrives at is pointed at the arm that branch \
161         would have taken"
162    }
163
164    fn preserves(&self) -> Preserved {
165        // Nothing. An edge moves, so every analysis built on the graph was built on a different
166        // graph, which is the same answer `simplify-cfg` gives for the same reason.
167        Preserved::NONE
168    }
169
170    fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
171        let mut stats = Stats::new();
172        let Some(entry) = func.entry() else { return stats };
173        // The edges are kept here rather than asked for as a graph, because this pass moves them
174        // as it goes and a cached graph would be about the shape the function had one thread ago.
175        // A block call rather than a predecessor, since redirecting an edge wants the slot in the
176        // pool and there is no finding it again from the block the edge used to arrive at.
177        let mut edges: Edges = incoming(func);
178        let leaky = leaky(func);
179        let unbound = Bindings::new();
180        let mut threaded = false;
181        'blocks: for block in func.blocks().collect::<Vec<Block>>() {
182            if block == entry || func[block].params.is_empty() {
183                continue;
184            }
185            let Some(term) = func.terminator(block) else { continue };
186            if !matches!(func[term].opcode, Opcode::BrIf | Opcode::Switch) {
187                continue;
188            }
189            // A branch that goes the same way whichever edge control arrived on is `simplify-cfg`'s
190            // to fold, and folding it once is cheaper than pointing every edge into the block at the
191            // same arm separately.
192            if taken(func, term, &unbound).is_some() {
193                continue;
194            }
195            // Two of the three reasons section 23.1 copies B are about the block rather than about
196            // one edge into it, so they are settled once here and not once per edge below.
197            let copied = if !skippable(func, block) {
198                Some(WOULD_COPY_EFFECT)
199            } else if leaky.contains(&block) {
200                Some(WOULD_COPY_READ_BELOW)
201            } else {
202                None
203            };
204            for (from, at) in edges.get(&block).cloned().unwrap_or_default() {
205                // A block that branches to itself, where the branch resolves, is a loop that does
206                // not end, and redirecting its own edge is not a description of anything a person
207                // wrote. The block below refuses it as well, since the block is its own latch.
208                if from == block {
209                    continue;
210                }
211                let subst = bind(func, block, at);
212                let Some(call) = taken(func, term, &subst) else { continue };
213                if call.block == block {
214                    continue;
215                }
216                if let Some(reason) = copied {
217                    stats.missed(reason);
218                    continue;
219                }
220                let Some(args) = carried(func, block, call, &subst) else {
221                    stats.missed(WOULD_COPY_CARRIED);
222                    continue;
223                };
224                if !allowed(an.loops(func), from, call.block) {
225                    stats.missed(WOULD_BREAK_A_LOOP);
226                    continue;
227                }
228                if !fuel.take() {
229                    // Where the pass stops rather than where it starts skipping, because a budget
230                    // that has reached zero will not have anything in it at the next block either
231                    // and the two refusals above are the counts worth being true.
232                    stats.missed(NO_FUEL);
233                    break 'blocks;
234                }
235                let args = func.push_values(&args);
236                func.set_block_call(at, BlockCall { block: call.block, args });
237                // The record has to follow the edge, so that a block further down the walk sees the
238                // predecessor it now has. That is what lets one thread make the next one possible
239                // within the single walk this pass is.
240                if let Some(list) = edges.get_mut(&block) {
241                    list.retain(|&(_, slot)| slot != at);
242                }
243                edges.entry(call.block).or_default().push((from, at));
244                // The loop forest was about the function as it was a moment ago, and the manager
245                // clears the cache after the pass returns, which is too late for the next edge.
246                an.clear();
247                stats.optimized(THREADED);
248                threaded = true;
249            }
250        }
251        if threaded {
252            // Threading every edge into a block leaves nothing arriving at it, and section 6.5
253            // makes taking an unreachable block out the standing obligation of whichever pass
254            // stranded it rather than something the next pass tidies up. The verifier holds every
255            // pass to that, so this is not a courtesy.
256            sweep(func, an, &mut stats);
257        }
258        stats
259    }
260}
261
262/// What this block's parameters hold along one edge into it.
263fn bind(func: &Func, block: Block, at: Idx<BlockCall>) -> Bindings {
264    let args = func[at].args;
265    let params = func[block].params.iter().copied();
266    params.zip(func[args].iter().copied()).collect()
267}
268
269/// The arguments the redirected edge carries, or `None` when one of them is only computed here.
270///
271/// A parameter of the block is replaced by whatever the edge being redirected was passing for it. A
272/// value from anywhere else is passed on as it stands, because a value used in this block and
273/// defined outside it dominates the predecessor, which is the argument the module doc makes. A value
274/// defined by an instruction in this block is the case that needs section 23.1's copy, and it is the
275/// answer this returns `None` for.
276///
277/// [`leaky`] does not cover this one. An argument on the arm is read by the block's own terminator,
278/// so the value never leaves the block by that route and the block is not leaky on account of it.
279/// The two checks are about the two ways a value gets out, and both are needed.
280fn carried(func: &Func, block: Block, call: BlockCall, subst: &Bindings) -> Option<Vec<Value>> {
281    let mut out = Vec::with_capacity(func[call.args].len());
282    for &arg in &func[call.args] {
283        if let Some(&bound) = subst.get(&arg) {
284            out.push(bound);
285            continue;
286        }
287        if let Def::Result { inst, .. } = func[arg].def {
288            if func.block_of(inst) == Some(block) {
289                return None;
290            }
291        }
292        out.push(arg);
293    }
294    Some(out)
295}
296
297/// Whether a path may walk past everything this block does on the way to its terminator.
298///
299/// The predicate is [`Opcode::has_effects`], which is what [`crate::dce`] deletes an instruction
300/// under, and the terminator is exempt because the thread is what replaces it. `is_terminator` on
301/// the function rather than on the opcode, for the reason dead code elimination gives: `asm goto`
302/// branches and its opcode does not say so.
303///
304/// A load answers that it has effects, so a block with one in it is not threaded past. That is
305/// conservative rather than necessary, since skipping a load skips a value nothing on the threaded
306/// path reads, and it is most of what [`WOULD_COPY_EFFECT`] turns out to be counting.
307fn skippable(func: &Func, block: Block) -> bool {
308    func.insts(block).all(|inst| func.is_terminator(inst) || !func[inst].opcode.has_effects())
309}
310
311/// Every block that defines a value read from somewhere other than itself.
312///
313/// The other half of what section 23.1's copy is for, and the half an argument list does not show.
314/// A block the candidate dominates reads what the candidate defined with nothing carrying it across,
315/// because dominance is the only permission a use needs in this IR. Point an edge past the candidate
316/// and that dominance is gone, so the read below is of a value nothing on the new path computed.
317///
318/// One walk for the whole function rather than one per candidate block, and it is computed once and
319/// never refreshed. It only goes stale in the safe direction. Threading never adds a read of a value
320/// defined in the block it went past, since [`carried`] refuses the edge when an arm carries one and
321/// everything else it passes on was defined further up, so a block in here can only ever have
322/// belonged in here less than it did.
323fn leaky(func: &Func) -> HashSet<Block> {
324    let mut out = HashSet::new();
325    for block in func.blocks().collect::<Vec<Block>>() {
326        for inst in func.insts(block).collect::<Vec<Inst>>() {
327            uses::operands(func, inst, |value| {
328                if let Some(home) = defined_in(func, value) {
329                    if home != block {
330                        out.insert(home);
331                    }
332                }
333            });
334        }
335    }
336    out
337}
338
339/// The block a value comes from, whether it is a parameter of one or a result computed in one.
340fn defined_in(func: &Func, value: Value) -> Option<Block> {
341    match func[value].def {
342        Def::Result { inst, .. } => func.block_of(inst),
343        Def::Param { block, .. } => Some(block),
344    }
345}
346
347/// Whether the loop structure survives pointing this edge at that block.
348///
349/// Section 23.5, and every answer of `false` is a refusal rather than a cost. Entering a loop
350/// anywhere but at its header makes the loop irreducible, moving a latch's edge is how the single
351/// latch property stops holding, and a block the forest has already given up on is one there is no
352/// useful answer about.
353fn allowed(loops: &Loops, from: Block, into: Block) -> bool {
354    if loops.is_irreducible(from) || loops.is_irreducible(into) {
355        return false;
356    }
357    if loops.all().any(|id| loops.latches(id).contains(&from)) {
358        return false;
359    }
360    let mut id = loops.innermost(into);
361    while let Some(loop_id) = id {
362        // Only a loop the predecessor is outside of, because an edge that stays within a loop is
363        // not a way into it.
364        if !loops.contains(loop_id, from) && loops.header(loop_id) != into {
365            return false;
366        }
367        id = loops.parent(loop_id);
368    }
369    true
370}
371
372#[cfg(test)]
373mod tests {
374    use rucc_base::Interner;
375    use rucc_ir::{
376        Block, Builder, Flags, Func, IntPred, MemInfo, MemOrder, Restrict, Signature, Type, Value,
377    };
378
379    use super::Thread;
380    use crate::stats::Kind;
381    use crate::{Analyses, Fuel, Pass, Stats};
382
383    /// Runs the pass with as much fuel as it wants.
384    fn thread(func: &mut Func) -> Stats {
385        Thread.run(func, &mut Analyses::new(), &mut Fuel::unlimited())
386    }
387
388    /// The blocks the function still has, by number.
389    fn blocks(func: &Func) -> Vec<usize> {
390        func.blocks().map(Block::index).collect()
391    }
392
393    /// Where a block's terminator goes, as block numbers.
394    fn goes_to(func: &Func, block: usize) -> Vec<usize> {
395        let block = Block::from_usize(block);
396        let term = func.terminator(block).expect("every block here has one");
397        func.successors(term).map(|call| call.block.index()).collect()
398    }
399
400    /// What a block's terminator carries on its first edge.
401    fn carries(func: &Func, block: usize) -> Vec<Value> {
402        let block = Block::from_usize(block);
403        let term = func.terminator(block).expect("every block here has one");
404        let call = func.successors(term).next().expect("a terminator here has an edge");
405        func[call.args].to_vec()
406    }
407
408    /// Section 23.3's example: two arms set one value to two constants and a join tests it.
409    ///
410    /// Block 0 is the entry, blocks 1 and 2 are the arms carrying `left` and `right`, block 3 is
411    /// the join and takes the value as a parameter, and blocks 4 and 5 are the two ways the test
412    /// can come out. The value the arms carry comes back, so a test can say which one was
413    /// substituted into what.
414    fn diamond(left: i128, right: i128) -> (Func, [Value; 2]) {
415        let mut names = Interner::new();
416        let mut func = Func::new(names.intern("f"), Signature::new());
417        let entry = func.create_block();
418        let arms = [func.create_block(), func.create_block()];
419        let join = func.create_block();
420        let param = func.append_param(join, Type::int(32));
421        let yes = func.create_block();
422        let no = func.create_block();
423
424        let mut build = Builder::new(&mut func, entry);
425        let cond = build.iconst(Type::int(1), 1);
426        build.br_if(cond, arms[0], &[], arms[1], &[]);
427        let mut sent = Vec::new();
428        for (arm, value) in arms.iter().zip([left, right]) {
429            let mut build = Builder::new(&mut func, *arm);
430            let it = build.iconst(Type::int(32), value);
431            sent.push(it);
432            build.jump(join, &[it]);
433        }
434        let mut build = Builder::new(&mut func, join);
435        let one = build.iconst(Type::int(32), 1);
436        let test = build.icmp(IntPred::Eq, param, one);
437        build.br_if(test, yes, &[], no, &[]);
438        for block in [yes, no] {
439            let mut build = Builder::new(&mut func, block);
440            build.ret(&[]);
441        }
442        (func, [sent[0], sent[1]])
443    }
444
445    #[test]
446    fn both_edges_of_a_join_that_decides_its_test_are_threaded() {
447        let (mut func, _) = diamond(1, 2);
448        let stats = thread(&mut func);
449        assert_eq!(stats.count(Kind::Optimized, super::THREADED), 2);
450        // The arm carrying 1 goes to the true side and the arm carrying 2 to the false side, so
451        // the block that tested it has nothing left arriving at it.
452        assert_eq!(goes_to(&func, 1), vec![4]);
453        assert_eq!(goes_to(&func, 2), vec![5]);
454        // And nothing arrives at the block that tested it, so it goes with the same sweep
455        // `simplify-cfg` uses. The verifier holds a pass to that rather than letting the next one
456        // tidy up after it.
457        assert_eq!(blocks(&func), vec![0, 1, 2, 4, 5]);
458        assert_eq!(stats.count(Kind::Optimized, crate::simplify_cfg::REMOVED), 1);
459    }
460
461    #[test]
462    fn an_edge_that_does_not_decide_the_test_is_left_alone() {
463        let mut names = Interner::new();
464        let signature = Signature::new().with_params(&[Type::int(32)]);
465        let mut func = Func::new(names.intern("f"), signature);
466        let entry = func.create_block();
467        // A parameter of the function rather than a constant, so binding it to the block's
468        // parameter says nothing about the test.
469        let outside = func.append_param(entry, Type::int(32));
470        let join = func.create_block();
471        let param = func.append_param(join, Type::int(32));
472        let yes = func.create_block();
473        let no = func.create_block();
474
475        let mut build = Builder::new(&mut func, entry);
476        build.jump(join, &[outside]);
477        let mut build = Builder::new(&mut func, join);
478        let one = build.iconst(Type::int(32), 1);
479        let test = build.icmp(IntPred::Eq, param, one);
480        build.br_if(test, yes, &[], no, &[]);
481        for block in [yes, no] {
482            let mut build = Builder::new(&mut func, block);
483            build.ret(&[]);
484        }
485
486        let stats = thread(&mut func);
487        assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
488        assert_eq!(goes_to(&func, 0), vec![1]);
489    }
490
491    #[test]
492    fn a_branch_decided_whichever_way_control_arrived_is_left_to_simplify_cfg() {
493        let mut names = Interner::new();
494        let mut func = Func::new(names.intern("f"), Signature::new());
495        let entry = func.create_block();
496        let arms = [func.create_block(), func.create_block()];
497        let join = func.create_block();
498        func.append_param(join, Type::int(32));
499        let yes = func.create_block();
500        let no = func.create_block();
501
502        let mut build = Builder::new(&mut func, entry);
503        let cond = build.iconst(Type::int(1), 1);
504        build.br_if(cond, arms[0], &[], arms[1], &[]);
505        for (arm, value) in arms.iter().zip([1, 2]) {
506            let mut build = Builder::new(&mut func, *arm);
507            let it = build.iconst(Type::int(32), value);
508            build.jump(join, &[it]);
509        }
510        let mut build = Builder::new(&mut func, join);
511        // The test reads nothing the edges carry, so it comes out the same way whichever edge
512        // control arrived on and it is `simplify-cfg`'s to fold once rather than this pass's to
513        // point every edge at separately.
514        let known = build.iconst(Type::int(1), 1);
515        build.br_if(known, yes, &[], no, &[]);
516        for block in [yes, no] {
517            let mut build = Builder::new(&mut func, block);
518            build.ret(&[]);
519        }
520
521        let stats = thread(&mut func);
522        assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
523        assert_eq!(goes_to(&func, 1), vec![3]);
524        assert_eq!(goes_to(&func, 2), vec![3]);
525    }
526
527    #[test]
528    fn a_block_with_something_that_happens_in_it_needs_the_copy() {
529        let mut names = Interner::new();
530        let mut func = Func::new(names.intern("f"), Signature::new());
531        let entry = func.create_block();
532        let arms = [func.create_block(), func.create_block()];
533        let join = func.create_block();
534        let param = func.append_param(join, Type::int(32));
535        let yes = func.create_block();
536        let no = func.create_block();
537
538        let mut build = Builder::new(&mut func, entry);
539        let cond = build.iconst(Type::int(1), 1);
540        build.br_if(cond, arms[0], &[], arms[1], &[]);
541        for (arm, value) in arms.iter().zip([1, 2]) {
542            let mut build = Builder::new(&mut func, *arm);
543            let it = build.iconst(Type::int(32), value);
544            build.jump(join, &[it]);
545        }
546        let mut build = Builder::new(&mut func, join);
547        // A store above the test. It has to happen on every path that reached the block, so no
548        // path may walk past it, and threading either edge would be a path that did.
549        let what = build.iconst(Type::int(32), 7);
550        let address = build.iconst(Type::int(64), 16);
551        let address = build.unary(rucc_ir::Opcode::IntToPtr, address, Type::PTR);
552        let info = MemInfo {
553            size: 4,
554            align: 4,
555            order: MemOrder::NotAtomic,
556            tbaa: None,
557            restrict: Restrict::NONE,
558        };
559        build.store(what, address, info, Flags::NONE);
560        let one = build.iconst(Type::int(32), 1);
561        let test = build.icmp(IntPred::Eq, param, one);
562        build.br_if(test, yes, &[], no, &[]);
563        for block in [yes, no] {
564            let mut build = Builder::new(&mut func, block);
565            build.ret(&[]);
566        }
567
568        let stats = thread(&mut func);
569        assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
570        assert_eq!(stats.count(Kind::Missed, super::WOULD_COPY_EFFECT), 2);
571    }
572
573    /// The shape a clamp compiles to, which is where the corpus caught this being wrong.
574    ///
575    /// `raw < 15 ? 15 : raw` puts the value under test in a block parameter and then hands the same
576    /// parameter to the arm that did not change it. The arm reads it with nothing carrying it there,
577    /// because the join dominates the arm, and an edge threaded past the join is a path on which the
578    /// read has no value behind it. It compiled to a program that printed the wrong number.
579    #[test]
580    fn a_value_the_block_defines_and_something_below_it_reads_needs_the_copy() {
581        let mut names = Interner::new();
582        let mut func = Func::new(names.intern("f"), Signature::new());
583        let entry = func.create_block();
584        let arms = [func.create_block(), func.create_block()];
585        let join = func.create_block();
586        let param = func.append_param(join, Type::int(32));
587        let yes = func.create_block();
588        let no = func.create_block();
589
590        let mut build = Builder::new(&mut func, entry);
591        let cond = build.iconst(Type::int(1), 1);
592        build.br_if(cond, arms[0], &[], arms[1], &[]);
593        for (arm, value) in arms.iter().zip([1, 2]) {
594            let mut build = Builder::new(&mut func, *arm);
595            let it = build.iconst(Type::int(32), value);
596            build.jump(join, &[it]);
597        }
598        let mut build = Builder::new(&mut func, join);
599        let one = build.iconst(Type::int(32), 1);
600        let test = build.icmp(IntPred::Eq, param, one);
601        build.br_if(test, yes, &[], no, &[]);
602        let mut build = Builder::new(&mut func, yes);
603        build.ret(&[]);
604        // The read from below. Nothing on the edge carries the parameter here, and nothing has to,
605        // since every path to this block goes through the block that defines it.
606        let mut build = Builder::new(&mut func, no);
607        build.ret(&[param]);
608
609        let stats = thread(&mut func);
610        assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
611        assert_eq!(stats.count(Kind::Missed, super::WOULD_COPY_READ_BELOW), 2);
612        assert_eq!(goes_to(&func, 1), vec![3]);
613        assert_eq!(goes_to(&func, 2), vec![3]);
614    }
615
616    #[test]
617    fn an_arm_carrying_a_value_the_block_worked_out_needs_the_copy() {
618        let mut names = Interner::new();
619        let mut func = Func::new(names.intern("f"), Signature::new());
620        let entry = func.create_block();
621        let arms = [func.create_block(), func.create_block()];
622        let join = func.create_block();
623        let param = func.append_param(join, Type::int(32));
624        let yes = func.create_block();
625        func.append_param(yes, Type::int(32));
626        let no = func.create_block();
627
628        let mut build = Builder::new(&mut func, entry);
629        let cond = build.iconst(Type::int(1), 1);
630        build.br_if(cond, arms[0], &[], arms[1], &[]);
631        for (arm, value) in arms.iter().zip([1, 2]) {
632            let mut build = Builder::new(&mut func, *arm);
633            let it = build.iconst(Type::int(32), value);
634            build.jump(join, &[it]);
635        }
636        let mut build = Builder::new(&mut func, join);
637        let one = build.iconst(Type::int(32), 1);
638        let test = build.icmp(IntPred::Eq, param, one);
639        // The true arm carries a sum this block worked out, which is exactly the value section
640        // 23.1's copy of the block exists to make available on the threaded path.
641        let sum = build.binary(rucc_ir::Opcode::Add, param, one, Flags::NONE);
642        build.br_if(test, yes, &[sum], no, &[]);
643        for block in [yes, no] {
644            let mut build = Builder::new(&mut func, block);
645            build.ret(&[]);
646        }
647
648        let stats = thread(&mut func);
649        // The edge carrying 2 takes the false arm, which carries nothing, so it threads. The one
650        // carrying 1 takes the arm with the sum on it and is the one that would need the copy.
651        assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1);
652        assert_eq!(stats.count(Kind::Missed, super::WOULD_COPY_CARRIED), 1);
653        assert_eq!(goes_to(&func, 2), vec![5]);
654        assert_eq!(goes_to(&func, 1), vec![3]);
655    }
656
657    #[test]
658    fn the_block_parameter_is_substituted_into_what_the_arm_carries() {
659        let mut names = Interner::new();
660        let mut func = Func::new(names.intern("f"), Signature::new());
661        let entry = func.create_block();
662        let arms = [func.create_block(), func.create_block()];
663        let join = func.create_block();
664        let param = func.append_param(join, Type::int(32));
665        let yes = func.create_block();
666        func.append_param(yes, Type::int(32));
667        let no = func.create_block();
668
669        let mut build = Builder::new(&mut func, entry);
670        let cond = build.iconst(Type::int(1), 1);
671        build.br_if(cond, arms[0], &[], arms[1], &[]);
672        let mut sent = Vec::new();
673        for (arm, value) in arms.iter().zip([1, 2]) {
674            let mut build = Builder::new(&mut func, *arm);
675            let it = build.iconst(Type::int(32), value);
676            sent.push(it);
677            build.jump(join, &[it]);
678        }
679        let mut build = Builder::new(&mut func, join);
680        let one = build.iconst(Type::int(32), 1);
681        let test = build.icmp(IntPred::Eq, param, one);
682        // The arm passes the block's own parameter on, which along each edge is the constant that
683        // edge was carrying.
684        build.br_if(test, yes, &[param], no, &[]);
685        for block in [yes, no] {
686            let mut build = Builder::new(&mut func, block);
687            build.ret(&[]);
688        }
689
690        let stats = thread(&mut func);
691        assert_eq!(stats.count(Kind::Optimized, super::THREADED), 2);
692        assert_eq!(goes_to(&func, 1), vec![4]);
693        assert_eq!(carries(&func, 1), vec![sent[0]]);
694    }
695
696    #[test]
697    fn a_switch_the_edge_decides_is_threaded() {
698        let mut names = Interner::new();
699        let mut func = Func::new(names.intern("f"), Signature::new());
700        let entry = func.create_block();
701        let arms = [func.create_block(), func.create_block()];
702        let join = func.create_block();
703        let param = func.append_param(join, Type::int(32));
704        let cases = [func.create_block(), func.create_block(), func.create_block()];
705
706        let mut build = Builder::new(&mut func, entry);
707        let cond = build.iconst(Type::int(1), 1);
708        build.br_if(cond, arms[0], &[], arms[1], &[]);
709        for (arm, value) in arms.iter().zip([0, 1]) {
710            let mut build = Builder::new(&mut func, *arm);
711            let it = build.iconst(Type::int(32), value);
712            build.jump(join, &[it]);
713        }
714        let mut build = Builder::new(&mut func, join);
715        build.switch(param, cases[0], &[(0, cases[1]), (1, cases[2])]);
716        for block in cases {
717            let mut build = Builder::new(&mut func, block);
718            build.ret(&[]);
719        }
720
721        let stats = thread(&mut func);
722        assert_eq!(stats.count(Kind::Optimized, super::THREADED), 2);
723        assert_eq!(goes_to(&func, 1), vec![cases[1].index()]);
724        assert_eq!(goes_to(&func, 2), vec![cases[2].index()]);
725    }
726
727    /// A loop whose header takes a parameter, entered from outside with a constant.
728    ///
729    /// Block 0 is the entry and jumps into the header carrying 1, block 1 is the header and tests
730    /// its parameter, block 2 is the body and jumps back carrying the function's own parameter,
731    /// block 3 is the way out and is where the test's false arm goes, and block 4 is somewhere
732    /// outside the loop. Which block the true arm goes to is the caller's to choose, which is what
733    /// makes one of these a thread into the middle of the loop and the other a thread onto a block
734    /// the loop has nothing to do with.
735    ///
736    /// This is the only shape in which a thread can make a loop irreducible when nothing is copied.
737    /// The block being threaded past has to be the header itself, because otherwise the arm being
738    /// threaded onto was already a way into the loop from outside it and the loop was already
739    /// irreducible before this pass looked at it.
740    fn loop_with_a_parameter(arm: usize) -> Func {
741        let mut names = Interner::new();
742        let signature = Signature::new().with_params(&[Type::int(32)]);
743        let mut func = Func::new(names.intern("f"), signature);
744        let entry = func.create_block();
745        let outside = func.append_param(entry, Type::int(32));
746        let header = func.create_block();
747        let param = func.append_param(header, Type::int(32));
748        let body = func.create_block();
749        let out = func.create_block();
750        let elsewhere = func.create_block();
751        let taken = [entry, header, body, out, elsewhere][arm];
752
753        let mut build = Builder::new(&mut func, entry);
754        let one = build.iconst(Type::int(32), 1);
755        build.jump(header, &[one]);
756        let mut build = Builder::new(&mut func, header);
757        let lit = build.iconst(Type::int(32), 1);
758        let test = build.icmp(IntPred::Eq, param, lit);
759        build.br_if(test, taken, &[], out, &[]);
760        let mut build = Builder::new(&mut func, body);
761        // Carrying the function's own parameter, so the edge back decides nothing and each of
762        // these tests is about the one edge that comes from outside.
763        build.jump(header, &[outside]);
764        for block in [out, elsewhere] {
765            let mut build = Builder::new(&mut func, block);
766            build.ret(&[]);
767        }
768        func
769    }
770
771    #[test]
772    fn threading_into_a_loop_anywhere_but_its_header_is_refused() {
773        // The true arm is the body, so pointing the edge from outside at it would give the loop a
774        // second way in and make it irreducible.
775        let mut func = loop_with_a_parameter(2);
776        let stats = thread(&mut func);
777        assert_eq!(stats.count(Kind::Optimized, super::THREADED), 0);
778        assert_eq!(stats.count(Kind::Missed, super::WOULD_BREAK_A_LOOP), 1);
779        assert_eq!(goes_to(&func, 0), vec![1]);
780    }
781
782    #[test]
783    fn threading_onto_a_block_outside_the_loop_is_allowed() {
784        // The true arm is in no loop at all, so the edge from outside can be pointed straight at
785        // it and the loop keeps the one way in it had.
786        let mut func = loop_with_a_parameter(4);
787        let stats = thread(&mut func);
788        assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1);
789        assert_eq!(goes_to(&func, 0), vec![4]);
790    }
791
792    #[test]
793    fn threading_onto_the_header_of_a_loop_is_allowed() {
794        let mut names = Interner::new();
795        let mut func = Func::new(names.intern("f"), Signature::new());
796        let entry = func.create_block();
797        let join = func.create_block();
798        let param = func.append_param(join, Type::int(32));
799        let header = func.create_block();
800        let out = func.create_block();
801
802        let mut build = Builder::new(&mut func, entry);
803        let one = build.iconst(Type::int(32), 1);
804        build.jump(join, &[one]);
805        let mut build = Builder::new(&mut func, join);
806        let lit = build.iconst(Type::int(32), 1);
807        let test = build.icmp(IntPred::Eq, param, lit);
808        build.br_if(test, header, &[], out, &[]);
809        let mut build = Builder::new(&mut func, header);
810        // A loop of one block, so the header is its own latch and the block being threaded onto
811        // is the header itself, which is the way in the loop already has.
812        let again = build.iconst(Type::int(1), 1);
813        build.br_if(again, header, &[], out, &[]);
814        let mut build = Builder::new(&mut func, out);
815        build.ret(&[]);
816
817        let stats = thread(&mut func);
818        assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1);
819        assert_eq!(goes_to(&func, 0), vec![2]);
820    }
821
822    #[test]
823    fn fuel_stops_the_threading_where_it_stands() {
824        let (mut func, _) = diamond(1, 2);
825        let mut fuel = Fuel::of(1);
826        let stats = Thread.run(&mut func, &mut Analyses::new(), &mut fuel);
827        assert_eq!(stats.count(Kind::Optimized, super::THREADED), 1);
828        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
829        assert_eq!(goes_to(&func, 2), vec![3], "the second edge is where it was");
830    }
831}