Skip to main content

rucc_opt/
licm.rs

1//! Moves a computation whose operands do not change in a loop to the block in front of it.
2//!
3//! Design: `spec/optimizer/27-licm.md`, with 27.1 for the legality answer, 27.2 for the cost, 27.5
4//! for what is built and 27.6 for the ways it is wrong.
5//!
6//! The oldest loop optimization and the one whose interesting part is not the move. Taking an
7//! instruction out of a block and putting it in another is a dozen lines. The three questions in
8//! front of it are the pass.
9//!
10//! # Invariance is one walk, because the IR is in SSA
11//!
12//! A value does not change in a loop when what defines it is outside the loop, or when everything
13//! it reads does not change. The second clause looks like a fixpoint and is not: a definition
14//! dominates its uses, so walking the loop's blocks in reverse postorder reaches every definition
15//! before every use of it, and one pass gives the transitive answer.
16//!
17//! Memory is the part section 27.5 says needs a fixpoint, and what this does instead is ask a
18//! smaller question. The IR can thread memory through the instructions that touch it as an operand
19//! of type `mem`, and where it does, a load whose memory operand is defined outside the loop is a
20//! load nothing in the loop wrote before, which the same operand walk settles with nothing added.
21//! Where it does not, and none of the pipelines this pass runs in do, a load has only its address
22//! for an operand and an unchanging address says nothing at all about what is behind it.
23//!
24//! So a load in a loop that writes memory anywhere is left where it is. Not because it is not
25//! invariant, but because nothing here can tell. That is coarse and it is honest, and the two ways
26//! out of it are the same one: give the pass the memory chain, or give it the module so it can ask
27//! [`crate::alias`]. A load in a loop that writes nothing is invariant on the address alone, and
28//! that is most of the loops that read a global in the first place.
29//!
30//! # The three way answer, and why header copying comes first
31//!
32//! Section 27.1's enum: a store or a call may not move at all, pure arithmetic may move anywhere,
33//! and in between are the instructions that are fine to move as long as they were going to run.
34//! A load faults on a bad address and a division traps on a zero divisor, so moving one in front of
35//! a loop that runs zero times is a program that crashes where the original returned.
36//!
37//! What settles it is [`crate::PostDominators`]: an instruction in a block the header cannot get
38//! past without entering runs on every entry to the loop, so working it out in front of the loop is
39//! working it out exactly when it was going to be worked out anyway. In an unrotated `while` the
40//! only such block is the header itself. In the `do-while` that [`crate::header_copy`] leaves, it
41//! is the whole body. That is section 27.1's point made concrete: **header copying is a
42//! prerequisite for this pass being useful, not a separate nicety.**
43//!
44//! Post-dominance is only half of that, and the other half is what the instructions in front do.
45//! A block the header cannot get past is still a block the program never arrives at if something
46//! on the way stops it, and a call is the thing that stops it: a callee may exit, may loop forever
47//! or may jump out, and what a call does comes from the module, which a pass holding one function
48//! does not have. So a call ends the guarantee for everything behind it in the same turn round the
49//! loop, and so does a trapping instruction, for the same reason from the other side. That is
50//! `goes_on`, and `gcc.c-torture/execute/pr38819.c` is the program that says why: its loop body
51//! calls a function that calls `exit` and then divides by zero, so a pass that asked only about
52//! post-dominance would hoist the division and crash a program that returns.
53//!
54//! An infinite loop is the exception, and it is why the fake exits are consulted. Post-dominance
55//! over a loop with no way out is answered against an edge document 06.8's analysis invented, so a
56//! block that post-dominates the header there might still be one an infinite path avoids. This
57//! declines those loops rather than believing an invented edge.
58//!
59//! # Where it puts things, and the two shapes it will not touch
60//!
61//! The preheader, in front of its terminator. That placement is always legal and the argument is
62//! short: a value defined outside the loop dominates the header, the header's immediate dominator
63//! is the preheader, so the definition dominates the preheader too. A loop without a preheader is
64//! left alone, since there is nowhere to put anything, and section 26 owns making one.
65//!
66//! That is also the whole of the answer to section 27.6's irreducible region, which has several
67//! ways in and therefore no preheader. It does not get that far here: [`crate::loops`] reports an
68//! irreducible region separately from the natural loops and this pass is only handed the natural
69//! ones, so a region with two entries is not a loop it can see rather than a loop it declines.
70//!
71//! The preheader is also the block [`speculate`] is asked about, rather than the block the
72//! instruction is in, and the difference between those two is a miscompilation. A division under
73//! `if (d)` has a divisor the ranges know is not zero, because a range is narrowed by the branches
74//! that dominate the block it is asked about. Ask where the division is and the answer is that it
75//! may go anywhere. Ask where it would go and the answer is that it may not, which is the true one,
76//! since the guard that made it safe is not in front of the preheader.
77//!
78//! # The cost, which is a register rather than an instruction
79//!
80//! Moving a computation out of a loop is not free and section 27.2 is blunt about why: the value is
81//! now live across the whole loop, and a loop that ran in registers and now spills is paying a load
82//! and a store per iteration to save an add per iteration. So the question is not whether the
83//! computation is expensive, it is whether it is more expensive than a register.
84//!
85//! The answer is document 40.6's pressure model, which is a count rather than an estimate, and
86//! [`heuristics::LICM_EXPENSIVE`], which is GCC's line between the two. Where the loop already
87//! holds as many values as the machine has registers, less document 40.6's margin, only the
88//! genuinely expensive operations move and the rest stay where they are. Each move made in a loop
89//! is one more value live across it, so the room left is counted down as the pass spends it.
90//!
91//! A constant and the address of a symbol are free, and a free value moves only as a passenger of
92//! something that is not. That is arranged in `trim`, which is also where the reason it cannot
93//! simply be refused up front is written down.
94//!
95//! # What this does not do yet
96//!
97//! Store motion, section 27.3, which turns a store to an unchanging address into a load in front of
98//! the loop and a store after it. It needs an alias query against every memory access in the loop,
99//! and an alias query needs the module, which a pass holding one function does not have. It is the
100//! half with the risk and it should arrive with the measurement section 27.7 asks for.
101//!
102//! Hoisting a call, for the same reason from the other side: a call is safe to move when it is
103//! `const`, and what a call is comes from the attributes on the callee, which live in the module.
104//! [`crate::purity`] has the answer and nothing hands it to a pass.
105
106use std::collections::HashSet;
107
108use rucc_cost::heuristics;
109use rucc_ir::{Block, Flags, Func, Inst, Opcode, Value};
110
111use crate::cfg::Cfg;
112use crate::dom::{Dominators, PostDominators};
113use crate::live::Liveness;
114use crate::loops::{LoopId, Loops};
115use crate::machine::Machine;
116use crate::pressure::{Class, Pressure, class_of};
117use crate::range::query::Ranges;
118use crate::{Analyses, Analysis, Fuel, Pass, Preserved, Stats, speculate};
119
120const HOISTED: &str = "computation moved in front of the loop, nothing in the loop changes it";
121const SPECULATIVE: &str =
122    "left in the loop, it does not run on every entry and working it out early could fault";
123const EFFECTS: &str = "left in the loop, moving it would change what the program does";
124const PRESSURE: &str = "left in the loop, it is cheaper than the register holding it would cost";
125const MEMORY: &str = "left in the loop, the loop writes memory and nothing here says which memory";
126const NO_PREHEADER: &str = "loop left as it was, it has not been canonicalized";
127const SPINS: &str = "loop left as it was, it has no way out, so nothing in it is known to run";
128const NO_FUEL: &str = "loop left as it was, the pass ran out of fuel";
129
130/// Section 27.5's pass.
131#[derive(Debug)]
132pub struct Licm;
133
134/// The one instance, which is what the pipelines name.
135pub static LICM: Licm = Licm;
136
137impl Pass for Licm {
138    fn name(&self) -> &'static str {
139        "licm"
140    }
141
142    fn describe(&self) -> &'static str {
143        "moves a computation whose operands do not change in a loop in front of the loop"
144    }
145
146    fn preserves(&self) -> Preserved {
147        // No edge moves and no block appears, so everything about the shape of the function is
148        // what it was. What changes is where values are live, and that is not a side effect of
149        // the transformation, it is the transformation.
150        Preserved::ALL.without(Analysis::Liveness)
151    }
152
153    fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
154        let mut stats = Stats::new();
155        if func.entry().is_none() {
156            return stats;
157        }
158        let machine = an.machine();
159        let cfg = an.cfg(func).clone();
160        let loops = an.loops(func).clone();
161        if loops.count() == 0 {
162            return stats;
163        }
164        let dom = an.dominators(func).clone();
165        let post = an.post_dominators(func).clone();
166        let invented: HashSet<Block> = post.fake_exits().iter().copied().collect();
167
168        // Innermost first, so a value hoisted out of an inner loop lands in the outer loop's body
169        // and is looked at again on the outer loop's turn. That is what carries a computation all
170        // the way out of a nest in one run rather than one level per run.
171        let mut order: Vec<LoopId> = loops.all().collect();
172        order.sort_by_key(|&id| std::cmp::Reverse(loops.depth(id)));
173
174        let mut pressure = Pressure::of(func, &cfg, &Liveness::of(func, &cfg));
175        for id in order {
176            let job = Job {
177                machine,
178                cfg: &cfg,
179                dom: &dom,
180                post: &post,
181                loops: &loops,
182                invented: &invented,
183            };
184            if job.run(func, &pressure, id, fuel, &mut stats) {
185                // The counts inside the loop just changed and the next loop out is about to be
186                // asked what it holds. Recomputing is linear in the function and the alternative
187                // is deciding the outer loop against a number the inner loop invalidated.
188                pressure = Pressure::of(func, &cfg, &Liveness::of(func, &cfg));
189            }
190        }
191        stats
192    }
193}
194
195/// Section 27.1's three way legality answer.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197enum Move {
198    /// It may go anywhere its operands reach.
199    Anywhere,
200    /// It may go only where it was going to run anyway.
201    IfItWasGoingToRun,
202    /// It stays.
203    Nowhere,
204}
205
206/// What one loop is being looked at against, gathered once so the walk below reads.
207struct Job<'a> {
208    machine: Machine,
209    cfg: &'a Cfg,
210    dom: &'a Dominators,
211    post: &'a PostDominators,
212    loops: &'a Loops,
213    invented: &'a HashSet<Block>,
214}
215
216impl Job<'_> {
217    /// Hoists what this loop will let go of, and says whether anything moved.
218    fn run(
219        &self,
220        func: &mut Func,
221        pressure: &Pressure,
222        id: LoopId,
223        fuel: &mut Fuel,
224        stats: &mut Stats,
225    ) -> bool {
226        let Some(preheader) = self.loops.preheader(self.cfg, id) else {
227            stats.missed(NO_PREHEADER);
228            return false;
229        };
230        let Some(landing) = func.terminator(preheader) else {
231            return false;
232        };
233        let plan = self.plan(func, pressure, id, preheader, fuel, stats);
234        for inst in &plan {
235            // Unlink and relink, in the order the plan was made, which is dominator order, so an
236            // operand hoisted with its user arrives in front of it. Section 27.6 names the other
237            // order as the way a chain comes out wrong.
238            func.remove_inst(*inst);
239            func.insert_before(*inst, landing);
240            stats.optimized(HOISTED);
241        }
242        !plan.is_empty()
243    }
244
245    /// Which instructions of this loop are worth moving and legal to move, in the order to move
246    /// them in.
247    ///
248    /// Separate from the moving because the range query holds the function and the moving needs it
249    /// back. That is Rust noticing something real: deciding against a function while changing it is
250    /// how a pass ends up reading an answer about a program that no longer exists.
251    ///
252    /// `preheader` is where everything in the plan is going, and it is passed in rather than worked
253    /// out here because it is what the safety question is asked about. A fact that holds inside the
254    /// loop is not a fact in front of it.
255    fn plan(
256        &self,
257        func: &Func,
258        pressure: &Pressure,
259        id: LoopId,
260        preheader: Block,
261        fuel: &mut Fuel,
262        stats: &mut Stats,
263    ) -> Vec<Inst> {
264        let header = self.loops.header(id);
265        let inside: HashSet<Block> = self.loops.blocks(id).iter().copied().collect();
266        // A loop with a way out has its post-dominance answered against edges the program has.
267        // One without does not, so it is declined rather than decided on an invented edge.
268        let spins = self.loops.blocks(id).iter().any(|block| self.invented.contains(block));
269        if spins {
270            stats.missed(SPINS);
271        }
272        // Asked once for the loop rather than once per load, because the answer is about the loop.
273        let writes = self
274            .loops
275            .blocks(id)
276            .iter()
277            .any(|block| func.insts(*block).any(|inst| func[inst].opcode.writes_memory()));
278        let ends = self
279            .loops
280            .blocks(id)
281            .iter()
282            .any(|block| func.insts(*block).any(|inst| ends_a_lifetime(func, inst)));
283        let mut ranges = Ranges::new(func, self.cfg, self.dom);
284        let mut plan = Vec::new();
285        // The ones in the plan that are only in it because something after them might want them.
286        // A cheap computation under pressure is worth moving when it is a link in a chain that
287        // ends in something expensive and is not worth moving on its own, and which of those it
288        // is cannot be known at the point it is read, because what reads it has not been read
289        // yet. So it goes in provisionally and [`trim`] takes it out again, which is the same
290        // answer a cost free instruction already gets and for the same reason.
291        let mut passengers: HashSet<Inst> = HashSet::new();
292        let mut moved: HashSet<Value> = HashSet::new();
293        // Every value moved out is one more live across the loop, which is one register less to
294        // decide the next one against. Taking it off the allocatable count says that once instead
295        // of at each of the comparisons below, and it is per bank because a value moved into a
296        // floating point register does not take an integer one.
297        //
298        // The two banks do not start at the same number, because the general purpose one gives up
299        // the stack pointer and the frame pointer and the vector one gives up neither. A target
300        // with no cost table answers nothing, and nothing here means no room at all rather than a
301        // number invented on its behalf: the pressure test then refuses every hoist that is not
302        // free, which is the same thing this pass does under real pressure.
303        let mut room = [0; Class::COUNT];
304        for class in Class::ALL {
305            room[class.index()] = self.machine.allocatable(class).unwrap_or(0);
306        }
307
308        // Whether the program is still known to be on its way to what comes next. It starts true
309        // at the header and goes false at the first instruction the program might not come back
310        // from, and it never goes true again, because reverse postorder is the order one turn
311        // round the loop runs its blocks in and a block seen later cannot run earlier.
312        let mut reaching = !spins;
313
314        for block in self.cfg.reverse_postorder() {
315            if !inside.contains(&block) {
316                continue;
317            }
318            let entered = self.post.post_dominates(block, header);
319            for inst in func.insts(block) {
320                // Both halves are needed and neither implies the other. The block being one the
321                // header cannot get past says every entry to the loop arrives here. `reaching`
322                // says nothing in front of it stops the program on the way.
323                let runs = reaching && entered;
324                if !goes_on(func, inst, &mut ranges, block) {
325                    reaching = false;
326                }
327                if func.is_terminator(inst) {
328                    continue;
329                }
330                let Some(result) = func[inst].results().next() else {
331                    continue;
332                };
333                let Some(class) = class_of(func[result].ty) else {
334                    continue;
335                };
336                if !self.unchanging(func, id, inst, &moved) {
337                    continue;
338                }
339                // The address does not change, which is not the question. What is behind it is,
340                // and asking that needs either the memory chain, which is not in this function, or
341                // the module, which is not handed to a pass. Both are absent, so anything that
342                // reads memory stays in a loop that writes any.
343                //
344                // A question about an allocation is not a question about what is in it, which is
345                // why [`asks_the_plane`] is allowed past this. The loop still has to be one that
346                // does not end a lifetime, and that is [`ends_a_lifetime`] rather than `writes`.
347                let settled = asks_the_plane(func[inst].opcode) && !ends;
348                if writes
349                    && !settled
350                    && func[inst].opcode.touches_memory()
351                    && func.mem_in(inst).is_none()
352                {
353                    stats.missed(MEMORY);
354                    continue;
355                }
356                let cost = cost(func, inst);
357                match movement(speculate::why_not(func, inst, &mut ranges, preheader)) {
358                    Move::Anywhere => (),
359                    Move::IfItWasGoingToRun if runs => (),
360                    Move::IfItWasGoingToRun => {
361                        stats.missed(SPECULATIVE);
362                        continue;
363                    }
364                    Move::Nowhere => {
365                        stats.missed(EFFECTS);
366                        continue;
367                    }
368                }
369                let bank = class.index();
370                // A free instruction is not asked to pay, because it is only in the plan as a
371                // passenger and [`trim`] takes it out again if nothing else in the plan wanted it.
372                if cost > 0 {
373                    let tight = pressure.is_tight(self.loops, id, class, room[bank]);
374                    if tight && cost < heuristics::LICM_EXPENSIVE {
375                        passengers.insert(inst);
376                    }
377                    if !fuel.take() {
378                        stats.missed(NO_FUEL);
379                        return trim(func, plan, &passengers, stats);
380                    }
381                    room[bank] = room[bank].saturating_sub(1);
382                }
383                moved.extend(func[inst].results());
384                plan.push(inst);
385            }
386        }
387        trim(func, plan, &passengers, stats)
388    }
389
390    /// Whether nothing in the loop changes what this instruction reads.
391    ///
392    /// The memory operand is one of the operands, so a load of memory the loop wrote is answered
393    /// here along with everything else and needs no separate walk.
394    fn unchanging(&self, func: &Func, id: LoopId, inst: Inst, moved: &HashSet<Value>) -> bool {
395        func[func[inst].args]
396            .iter()
397            .all(|arg| self.loops.is_invariant(func, id, *arg) || moved.contains(arg))
398    }
399}
400
401/// Takes the instructions nothing else in the plan needed back out of it.
402///
403/// Two kinds ride along. A constant or the address of a symbol costs nothing to work out again, so
404/// moving one out of a loop on its own buys nothing and costs a register held for the length of the
405/// loop. It still has to be in the plan while the plan is being made, because a load of a global is
406/// only invariant once the address it reads is going with it, and refusing the address up front
407/// would refuse the load as well. The other kind is `passengers`, the ones a full loop would have
408/// turned down on their own: a cheap computation under pressure is worth moving when something
409/// expensive downstream is waiting on it and is not worth moving otherwise, and what reads it has
410/// not been read yet at the point it is decided. Both come out here if nobody boarded behind them.
411///
412/// Backwards, because the plan is in dependency order and a passenger is wanted by something after
413/// it. One walk answers the whole chain for the same reason the invariance walk does, and a chain
414/// of ten cheap links ending in nothing comes out in that one walk rather than one link per run.
415///
416/// The pressure miss is counted here rather than where it is decided, because an instruction that
417/// went on to carry an expensive one out of the loop was not left in the loop and reporting it as
418/// missed would say the opposite of what happened.
419fn trim(func: &Func, plan: Vec<Inst>, passengers: &HashSet<Inst>, stats: &mut Stats) -> Vec<Inst> {
420    let mut wanted: HashSet<Value> = HashSet::new();
421    let mut keep = Vec::with_capacity(plan.len());
422    for inst in plan.into_iter().rev() {
423        if !func[inst].results().any(|value| wanted.contains(&value)) {
424            if cost(func, inst) == 0 {
425                continue;
426            }
427            if passengers.contains(&inst) {
428                stats.missed(PRESSURE);
429                continue;
430            }
431        }
432        wanted.extend(func[func[inst].args].iter().copied());
433        keep.push(inst);
434    }
435    keep.reverse();
436    keep
437}
438
439/// Section 27.1's enum, read off why the value may not be worked out early.
440///
441/// The reason matters rather than the opcode. A load whose address is proved good may go anywhere
442/// and a volatile load may go nowhere, and both of them are loads.
443fn movement(why: Option<&'static str>) -> Move {
444    match why {
445        None => Move::Anywhere,
446        // The three that are only a problem on a run that was not going to reach them.
447        Some(speculate::BY_ZERO | speculate::OVERFLOW | speculate::ADDRESS) => {
448            Move::IfItWasGoingToRun
449        }
450        Some(_) => Move::Nowhere,
451    }
452}
453
454/// Whether the program, having started this instruction, is certain to go on to the next one.
455///
456/// Post-dominance answers a question about the shape of the function and this answers the other
457/// half, which is about what the instructions in front do. A block the header cannot get past is
458/// still a block the program never arrives at if something on the way stops it, and there are two
459/// ways to stop it. One is a call, since a callee may exit, may loop forever or may jump out, and
460/// what a call does comes from the module, which a pass holding one function does not have, so
461/// every call is one that might not come back. The other is an instruction that traps, which is
462/// exactly the instruction this pass is careful about moving, read here at the block it is in
463/// rather than at the preheader because the question is whether it traps where it stands.
464///
465/// `gcc.c-torture/execute/pr38819.c` is the program that says why. Its loop body calls a function
466/// that calls `exit` and then divides by zero, and the division is invariant, so a pass that asked
467/// only whether the body post-dominates the header would work it out in front of the loop and
468/// crash a program that returns.
469fn goes_on(func: &Func, inst: Inst, ranges: &mut Ranges<'_>, at: Block) -> bool {
470    match func[inst].opcode {
471        Opcode::Call | Opcode::CallIndirect | Opcode::TailCall => false,
472        // A promise that control does not get here, so nothing after it runs either.
473        Opcode::UnreachableHint => false,
474        Opcode::SDiv | Opcode::SRem | Opcode::UDiv | Opcode::URem | Opcode::Load => {
475            movement(speculate::why_not(func, inst, ranges, at)) != Move::IfItWasGoingToRun
476        }
477        _ => true,
478    }
479}
480
481/// Whether this asks the allocator about an object rather than reading what is in one.
482///
483/// `cap_extent` and `cap_extent_back` read the planes and nothing else does. What they answer is
484/// how much room there is from a pointer to the end of whatever holds it, and a store through a
485/// pointer does not change that, so the loop writing memory is not the question for them the way it
486/// is for a load. What is the question is whether the loop ends the allocation, which
487/// [`ends_a_lifetime`] answers.
488///
489/// This is not an exception to the rule above it so much as the rule being asked about the right
490/// memory. The comment there says the pass would need a memory chain or the module to know what is
491/// behind an address, and for these two it needs neither: `spec/safe-memory/05-representation.md`
492/// puts the planes somewhere the program cannot reach and section 6.2.4 calls the checks
493/// `readonly`, so the set of things that can change the answer is small enough to list.
494const fn asks_the_plane(opcode: Opcode) -> bool {
495    matches!(opcode, Opcode::CapExtent | Opcode::CapExtentBack)
496}
497
498/// Whether this could end the lifetime of something a plane holds a row for.
499///
500/// The same list `crate::split` refuses a loop for, and for the same reason: a call that might free
501/// changes what the planes say, and so does assembly nobody can read and the two instructions that
502/// end a lifetime by saying so. A call the module summary calls `nofree` is not one of them, which
503/// is what makes this worth asking at all, since a loop with a `memcpy` in it is still a loop whose
504/// allocations stay where they are.
505fn ends_a_lifetime(func: &Func, inst: Inst) -> bool {
506    match func[inst].opcode {
507        Opcode::Call | Opcode::CallIndirect | Opcode::TailCall => {
508            !func[inst].flags.contains(Flags::NOFREE)
509        }
510        Opcode::InlineAsm | Opcode::MetaEnd | Opcode::MetaTransfer => true,
511        _ => false,
512    }
513}
514
515/// Section 27.2's table, which GCC's `stmt_cost` opens by admitting is ad hoc.
516///
517/// The numbers are not prices. They sort instructions into three groups: the ones there is no point
518/// moving, the ones worth moving when there is room, and the ones worth moving even when there is
519/// not. What makes the table worth copying rather than inventing is the reasoning behind two of its
520/// entries. A conditional is expensive here because moving it in front of the loop is what lets
521/// document 30 split the loop on it, so the cost model is encoding a pass interaction rather than a
522/// price. Anything touching memory is expensive because, as GCC puts it, hoisting memory references
523/// out should almost surely be a win.
524fn cost(func: &Func, inst: Inst) -> u32 {
525    match func[inst].opcode {
526        // Worked out again wherever it is wanted, so there is nothing to move. The address of a
527        // symbol is in here with the constants because that is what it is on the only target there
528        // is: one instruction reading the program counter and a link time constant, with no
529        // operands, so a copy of it costs what recomputing it costs and holding one across a loop
530        // costs a register for nothing.
531        Opcode::IConst | Opcode::FConst | Opcode::GlobalAddr | Opcode::BlockAddr => 0,
532        // `crate::pass` never sees one of these reach the back end: `rucc_safety::lower` removes
533        // every `cap_of` once the checks that read it have become calls. So it holds no register
534        // and a copy of it costs nothing, which is what the four above have in common.
535        Opcode::CapOf => 0,
536        Opcode::Load
537        | Opcode::Select
538        | Opcode::Call
539        | Opcode::CallIndirect
540        | Opcode::Mul
541        | Opcode::SDiv
542        | Opcode::UDiv
543        | Opcode::SRem
544        | Opcode::URem
545        | Opcode::FMul
546        | Opcode::FDiv
547        | Opcode::FRem
548        | Opcode::Shl
549        | Opcode::LShr
550        | Opcode::AShr
551        | Opcode::ICmp
552        | Opcode::FCmp => heuristics::LICM_EXPENSIVE,
553        // Both become a call to the runtime in `rucc_safety::lower`, and the census in
554        // `spec/safe-memory/13-performance.md` section 13.1 measured one at about 377 instructions.
555        // Left at the default they price as an add, and then the pressure test throws them out of
556        // exactly the loops where they cost the most.
557        Opcode::CapExtent | Opcode::CapExtentBack => heuristics::LICM_EXPENSIVE,
558        _ => 1,
559    }
560}
561
562#[cfg(test)]
563mod tests {
564    use rucc_base::{Interner, Symbol};
565    use rucc_ir::{
566        Block, Builder, Def, Extra, Flags, Func, Global, Inst, InstData, IntPred, MemInfo,
567        MemOrder, Module, Opcode, Restrict, Signature, Type, Value, verify_func,
568    };
569    use rucc_target::{TargetInfo, Triple};
570
571    use super::{
572        EFFECTS, HOISTED, LICM, MEMORY, NO_FUEL, NO_PREHEADER, PRESSURE, SPECULATIVE, SPINS,
573    };
574    use crate::canon::Canon;
575    use crate::header_copy::SPEED;
576    use crate::stats::Kind;
577    use crate::{Fuel, Pass, Stats};
578
579    /// Runs the pass over the function as it stands.
580    fn hoist(func: &mut Func, fuel: &mut Fuel) -> Stats {
581        LICM.run(func, &mut crate::machine::fixtures::analyses(), fuel)
582    }
583
584    /// Insists the function is one the rest of the compiler may believe.
585    ///
586    /// Moving a definition is the edit that breaks a definition's dominance over its uses, so this
587    /// is where most of the strength of these tests is.
588    fn sound(func: &Func, names: &mut Interner) {
589        checked(func, names, &[]);
590    }
591
592    /// The same, in a module that declares those globals.
593    fn checked(func: &Func, names: &mut Interner, globals: &[Symbol]) {
594        let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
595        let mut module = Module::new(names.intern("t.c"), &target);
596        for name in globals {
597            module.add_global(Global::new(*name, 16, 8));
598        }
599        if let Err(errors) = verify_func(&module, func, names) {
600            panic!("{errors:#?}");
601        }
602    }
603
604    /// The instruction that worked that value out.
605    fn made(func: &Func, value: Value) -> Inst {
606        match func[value].def {
607            Def::Result { inst, .. } => inst,
608            other => panic!("{other:?} is not something an instruction worked out"),
609        }
610    }
611
612    /// Which block that value is worked out in now.
613    fn lives_in(func: &Func, value: Value) -> Block {
614        func.block_of(made(func, value)).expect("it is in a block")
615    }
616
617    /// Where in its block that value is worked out, counting from the top.
618    fn position(func: &Func, value: Value) -> usize {
619        let inst = made(func, value);
620        let block = func.block_of(inst).expect("it is in a block");
621        func.insts(block).position(|other| other == inst).expect("it is in that block")
622    }
623
624    /// Moves whatever the caller appended after a block's terminator to in front of it.
625    ///
626    /// A builder appends, and a block that already ends in a jump has nowhere to append to that is
627    /// legal. Writing the loop first and the body second reads better than the other order, so the
628    /// tests do that and this puts the instructions back where they belong, in the order they were
629    /// written in.
630    fn tucked(func: &mut Func, block: Block) {
631        let term = func
632            .insts(block)
633            .find(|inst| func.is_terminator(*inst))
634            .expect("the block ends in something");
635        let stragglers: Vec<Inst> =
636            func.insts(block).skip_while(|inst| *inst != term).skip(1).collect();
637        for inst in stragglers {
638            func.remove_inst(inst);
639            func.insert_before(inst, term);
640        }
641    }
642
643    /// A memory record of that many bytes.
644    fn record(size: u64) -> MemInfo {
645        MemInfo {
646            size,
647            align: 8,
648            order: MemOrder::NotAtomic,
649            tbaa: None,
650            owns: 0,
651            restrict: Restrict::NONE,
652        }
653    }
654
655    /// A counted loop that tests at the top, which is what `while (i < n)` lowers to.
656    ///
657    /// ```text
658    /// entry: jump head(0)
659    /// head(i): t = i < n; br t -> body, done
660    /// body: next = i + 1; jump head(next)
661    /// done: ret i + the spares
662    /// ```
663    ///
664    /// The spare parameters are added up after the loop and used nowhere else, which is how a test
665    /// makes the loop hold values without putting anything in it. The pointer is there because the
666    /// only address the function cannot say anything about is one it was handed.
667    struct Counted {
668        names: Interner,
669        func: Func,
670        entry: Block,
671        head: Block,
672        body: Block,
673        limit: Value,
674        pointer: Value,
675    }
676
677    fn counted(spare: usize) -> Counted {
678        let mut names = Interner::new();
679        let mut types = vec![Type::int(32); spare + 1];
680        types.push(Type::PTR);
681        let signature = Signature::new().with_params(&types).with_returns(&[Type::int(32)]);
682        let mut func = Func::new(names.intern("f"), signature);
683        let entry = func.create_block();
684        let head = func.create_block();
685        let body = func.create_block();
686        let done = func.create_block();
687        let handed: Vec<Value> =
688            types.iter().map(|ty| func.append_param(entry, *ty)).collect::<Vec<_>>();
689        let limit = handed[0];
690        let pointer = *handed.last().expect("the pointer is the last of them");
691        let i = func.append_param(head, Type::int(32));
692        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
693        Builder::new(&mut func, entry).jump(head, &[zero]);
694        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
695        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
696        let one = Builder::new(&mut func, body).iconst(Type::int(32), 1);
697        let next = Builder::new(&mut func, body).binary(Opcode::Add, i, one, Flags::NONE);
698        Builder::new(&mut func, body).jump(head, &[next]);
699        let mut build = Builder::new(&mut func, done);
700        let mut total = i;
701        for value in &handed[1..=spare] {
702            total = build.binary(Opcode::Add, total, *value, Flags::NONE);
703        }
704        build.ret(&[total]);
705        Counted { names, func, entry, head, body, limit, pointer }
706    }
707
708    #[test]
709    fn an_invariant_computation_moves_in_front_of_the_loop() {
710        let mut it = counted(0);
711        let product = Builder::new(&mut it.func, it.body).binary(
712            Opcode::Mul,
713            it.limit,
714            it.limit,
715            Flags::NONE,
716        );
717        tucked(&mut it.func, it.body);
718
719        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
720        assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
721        assert_eq!(lives_in(&it.func, product), it.entry, "it is in front of the loop now");
722        sound(&it.func, &mut it.names);
723    }
724
725    #[test]
726    fn a_computation_the_loop_changes_stays_where_it_is() {
727        let mut it = counted(0);
728        let i = it.func[it.head].params[0];
729        let square = Builder::new(&mut it.func, it.body).binary(Opcode::Mul, i, i, Flags::NONE);
730        tucked(&mut it.func, it.body);
731
732        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
733        assert_eq!(stats.count(Kind::Optimized, HOISTED), 0);
734        assert_eq!(lives_in(&it.func, square), it.body);
735        sound(&it.func, &mut it.names);
736    }
737
738    #[test]
739    fn a_loop_with_nothing_invariant_in_it_is_left_alone() {
740        // The counter, the constant one and the comparison are the whole of the loop, and the
741        // constant is the case the cost table gives nothing to, since it is worked out again
742        // wherever it is wanted.
743        let mut it = counted(0);
744        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
745        assert_eq!(stats.count(Kind::Optimized, HOISTED), 0);
746        sound(&it.func, &mut it.names);
747    }
748
749    #[test]
750    fn a_load_the_loop_might_not_reach_stays_where_it_is() {
751        let mut it = counted(0);
752        let read = Builder::new(&mut it.func, it.body).load(
753            Type::int(32),
754            it.pointer,
755            record(4),
756            Flags::NONE,
757        );
758        tucked(&mut it.func, it.body);
759
760        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
761        assert_eq!(stats.count(Kind::Optimized, HOISTED), 0);
762        assert_eq!(stats.count(Kind::Missed, SPECULATIVE), 1);
763        assert_eq!(lives_in(&it.func, read), it.body, "the loop may run zero times");
764        sound(&it.func, &mut it.names);
765    }
766
767    #[test]
768    fn the_same_load_moves_once_the_loop_tests_at_the_bottom() {
769        // Section 27.1's claim that header copying is a prerequisite rather than a nicety, run as
770        // a test. Nothing about the load changed. What changed is that the body now runs on every
771        // entry to the loop, so working it out in front is working it out when it was going to be.
772        // The three passes in front of it are the order the pipeline runs them in, and the second
773        // canonicalization is not spare: the copy leaves the rotated loop entered from a block
774        // that also leaves it, and making a preheader out of that is what canonicalization does.
775        let mut it = counted(0);
776        let read = Builder::new(&mut it.func, it.body).load(
777            Type::int(32),
778            it.pointer,
779            record(4),
780            Flags::NONE,
781        );
782        tucked(&mut it.func, it.body);
783        let mut an = crate::machine::fixtures::analyses();
784        Canon.run(&mut it.func, &mut an, &mut Fuel::unlimited());
785        SPEED.run(&mut it.func, &mut an, &mut Fuel::unlimited());
786        Canon.run(&mut it.func, &mut an, &mut Fuel::unlimited());
787
788        let stats = LICM.run(&mut it.func, &mut an, &mut Fuel::unlimited());
789        assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
790        assert_ne!(lives_in(&it.func, read), it.body, "it left the body");
791        sound(&it.func, &mut it.names);
792    }
793
794    #[test]
795    fn something_that_could_trap_moves_when_it_runs_on_every_entry() {
796        // The header of an unrotated loop is the one block that does, which is why this is the
797        // only hoist an uncanonicalized `while` gets out of the pass.
798        let mut it = counted(0);
799        let share = Builder::new(&mut it.func, it.head).binary(
800            Opcode::SDiv,
801            it.limit,
802            it.limit,
803            Flags::NONE,
804        );
805        tucked(&mut it.func, it.head);
806
807        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
808        assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
809        assert_eq!(lives_in(&it.func, share), it.entry);
810        sound(&it.func, &mut it.names);
811    }
812
813    #[test]
814    fn something_that_could_trap_stays_behind_a_call_that_might_not_come_back() {
815        // The shape of `gcc.c-torture/execute/pr38819.c`, which is what found this. The head runs
816        // on every entry to the loop and the division is invariant, and neither of those is the
817        // question. The call in front of it may exit, so the division is not something the program
818        // was going to work out, and hoisting it crashes a program that returns.
819        let mut it = counted(0);
820        let callee = it.names.intern("g");
821        let mut build = Builder::new(&mut it.func, it.head);
822        let signature = build.func().add_signature(Signature::new());
823        build.call(callee, signature, &[]);
824        let share = Builder::new(&mut it.func, it.head).binary(
825            Opcode::SDiv,
826            it.limit,
827            it.limit,
828            Flags::NONE,
829        );
830        tucked(&mut it.func, it.head);
831
832        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
833        assert_eq!(stats.count(Kind::Missed, SPECULATIVE), 1);
834        assert_eq!(lives_in(&it.func, share), it.head, "the call in front is what keeps it there");
835        sound(&it.func, &mut it.names);
836    }
837
838    #[test]
839    fn the_same_division_moves_when_the_call_is_behind_it() {
840        // The other half of the rule, and the reason it is not a count of the calls in the loop.
841        // A call after the division says nothing about whether the division ran.
842        let mut it = counted(0);
843        let callee = it.names.intern("g");
844        let share = Builder::new(&mut it.func, it.head).binary(
845            Opcode::SDiv,
846            it.limit,
847            it.limit,
848            Flags::NONE,
849        );
850        let mut build = Builder::new(&mut it.func, it.head);
851        let signature = build.func().add_signature(Signature::new());
852        build.call(callee, signature, &[]);
853        tucked(&mut it.func, it.head);
854
855        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
856        assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
857        assert_eq!(lives_in(&it.func, share), it.entry);
858        sound(&it.func, &mut it.names);
859    }
860
861    #[test]
862    fn a_call_in_one_block_keeps_something_in_a_later_one_where_it_is() {
863        // The flag has to outlive the block it went false in, because the blocks of one turn round
864        // the loop run in the order this walks them and the call is still in front of everything
865        // behind it. A `do-while` written out by hand, so that both blocks of the loop post-dominate
866        // its header and the division would be a hoist this pass makes if it looked at that alone.
867        let mut names = Interner::new();
868        let signature =
869            Signature::new().with_params(&[Type::int(32)]).with_returns(&[Type::int(32)]);
870        let mut func = Func::new(names.intern("f"), signature);
871        let callee = names.intern("g");
872        let entry = func.create_block();
873        let head = func.create_block();
874        let rest = func.create_block();
875        let done = func.create_block();
876        let limit = func.append_param(entry, Type::int(32));
877        let i = func.append_param(head, Type::int(32));
878        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
879        Builder::new(&mut func, entry).jump(head, &[zero]);
880        let mut build = Builder::new(&mut func, head);
881        let taken = build.func().add_signature(Signature::new());
882        build.call(callee, taken, &[]);
883        Builder::new(&mut func, head).jump(rest, &[]);
884        let share = Builder::new(&mut func, rest).binary(Opcode::SDiv, limit, limit, Flags::NONE);
885        let one = Builder::new(&mut func, rest).iconst(Type::int(32), 1);
886        let next = Builder::new(&mut func, rest).binary(Opcode::Add, i, one, Flags::NONE);
887        let test = Builder::new(&mut func, rest).icmp(IntPred::Slt, next, limit);
888        Builder::new(&mut func, rest).br_if(test, head, &[next], done, &[]);
889        Builder::new(&mut func, done).ret(&[i]);
890
891        let stats = hoist(&mut func, &mut Fuel::unlimited());
892        assert_eq!(stats.count(Kind::Missed, SPECULATIVE), 1);
893        assert_eq!(lives_in(&func, share), rest, "the call is in front of it in the same turn");
894        sound(&func, &mut names);
895    }
896
897    #[test]
898    fn a_division_a_test_inside_the_loop_made_safe_stays_inside_that_test() {
899        // The one that looks safe and is not. Inside `if (limit)` the ranges know the divisor is
900        // not zero, so asking about the division where it stands gets a yes. The preheader is not
901        // inside that test and the same question there gets a no, which is the question the pass
902        // has to be asking, because the preheader is where the answer would be used.
903        let mut names = Interner::new();
904        let signature =
905            Signature::new().with_params(&[Type::int(32)]).with_returns(&[Type::int(32)]);
906        let mut func = Func::new(names.intern("f"), signature);
907        let entry = func.create_block();
908        let head = func.create_block();
909        let body = func.create_block();
910        let safe = func.create_block();
911        let latch = func.create_block();
912        let done = func.create_block();
913        let limit = func.append_param(entry, Type::int(32));
914        let i = func.append_param(head, Type::int(32));
915        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
916        Builder::new(&mut func, entry).jump(head, &[zero]);
917        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
918        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
919        let guard = Builder::new(&mut func, body).icmp(IntPred::Ne, limit, zero);
920        Builder::new(&mut func, body).br_if(guard, safe, &[], latch, &[]);
921        let share = Builder::new(&mut func, safe).binary(Opcode::SDiv, limit, limit, Flags::NONE);
922        Builder::new(&mut func, safe).jump(latch, &[]);
923        let one = Builder::new(&mut func, latch).iconst(Type::int(32), 1);
924        let next = Builder::new(&mut func, latch).binary(Opcode::Add, i, one, Flags::NONE);
925        Builder::new(&mut func, latch).jump(head, &[next]);
926        Builder::new(&mut func, done).ret(&[i]);
927
928        let stats = hoist(&mut func, &mut Fuel::unlimited());
929        assert_eq!(stats.count(Kind::Missed, SPECULATIVE), 1);
930        assert_eq!(lives_in(&func, share), safe, "the guard is what made it safe");
931        // The test itself is invariant and does come out, which is worth asserting because it is
932        // the difference between the pass declining this division and the pass declining the loop.
933        assert_eq!(lives_in(&func, guard), entry);
934        assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
935        sound(&func, &mut names);
936    }
937
938    #[test]
939    fn the_same_division_in_the_body_stays() {
940        let mut it = counted(0);
941        let share = Builder::new(&mut it.func, it.body).binary(
942            Opcode::SDiv,
943            it.limit,
944            it.limit,
945            Flags::NONE,
946        );
947        tucked(&mut it.func, it.body);
948
949        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
950        assert_eq!(stats.count(Kind::Missed, SPECULATIVE), 1);
951        assert_eq!(lives_in(&it.func, share), it.body, "the divisor could be zero");
952        sound(&it.func, &mut it.names);
953    }
954
955    #[test]
956    fn a_volatile_load_stays_even_where_it_runs_on_every_entry() {
957        let mut it = counted(0);
958        let read = Builder::new(&mut it.func, it.head).load(
959            Type::int(32),
960            it.pointer,
961            record(4),
962            Flags::VOLATILE,
963        );
964        tucked(&mut it.func, it.head);
965
966        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
967        assert_eq!(stats.count(Kind::Missed, EFFECTS), 1);
968        assert_eq!(lives_in(&it.func, read), it.head, "one access per iteration is the point");
969        sound(&it.func, &mut it.names);
970    }
971
972    #[test]
973    fn a_chain_comes_out_in_the_order_it_was_written_in() {
974        // Section 27.6's fourth way of getting it wrong. The sum reads the product, so the product
975        // has to arrive in front of it and not merely arrive.
976        let mut it = counted(0);
977        let mut build = Builder::new(&mut it.func, it.body);
978        let product = build.binary(Opcode::Mul, it.limit, it.limit, Flags::NONE);
979        let sum = build.binary(Opcode::Mul, product, it.limit, Flags::NONE);
980        tucked(&mut it.func, it.body);
981
982        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
983        assert_eq!(stats.count(Kind::Optimized, HOISTED), 2);
984        assert_eq!(lives_in(&it.func, product), it.entry);
985        assert_eq!(lives_in(&it.func, sum), it.entry);
986        assert!(position(&it.func, product) < position(&it.func, sum));
987        sound(&it.func, &mut it.names);
988    }
989
990    #[test]
991    fn the_pass_stops_where_the_fuel_runs_out() {
992        let mut it = counted(0);
993        let mut build = Builder::new(&mut it.func, it.body);
994        let product = build.binary(Opcode::Mul, it.limit, it.limit, Flags::NONE);
995        build.binary(Opcode::Mul, product, it.limit, Flags::NONE);
996        tucked(&mut it.func, it.body);
997
998        let stats = hoist(&mut it.func, &mut Fuel::of(1));
999        assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
1000        assert_eq!(stats.count(Kind::Missed, NO_FUEL), 1);
1001        sound(&it.func, &mut it.names);
1002    }
1003
1004    #[test]
1005    fn a_cheap_computation_stays_where_the_loop_is_already_full() {
1006        // Fourteen values arriving and nothing in the loop to spare, so section 27.2's line is
1007        // what decides. The add is cheaper than the register it would want and the multiply is
1008        // not.
1009        let mut it = counted(14);
1010        let mut build = Builder::new(&mut it.func, it.body);
1011        let sum = build.binary(Opcode::Add, it.limit, it.limit, Flags::NONE);
1012        let product = build.binary(Opcode::Mul, it.limit, it.limit, Flags::NONE);
1013        tucked(&mut it.func, it.body);
1014
1015        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
1016        assert_eq!(stats.count(Kind::Missed, PRESSURE), 1);
1017        assert_eq!(lives_in(&it.func, sum), it.body);
1018        assert_eq!(lives_in(&it.func, product), it.entry);
1019        sound(&it.func, &mut it.names);
1020    }
1021
1022    #[test]
1023    fn a_cheap_link_moves_when_it_is_carrying_an_expensive_one_out() {
1024        // The same full loop and the same add, with the multiply now reading it. On its own the
1025        // add is not worth a register and the test above is that. Here it is the only thing
1026        // between the multiply and the front of the loop, and refusing it refuses the multiply
1027        // too, silently, because an instruction whose operand stayed behind is not invariant.
1028        let mut it = counted(14);
1029        let mut build = Builder::new(&mut it.func, it.body);
1030        let sum = build.binary(Opcode::Add, it.limit, it.limit, Flags::NONE);
1031        let product = build.binary(Opcode::Mul, sum, it.limit, Flags::NONE);
1032        tucked(&mut it.func, it.body);
1033
1034        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
1035        assert_eq!(stats.count(Kind::Missed, PRESSURE), 0);
1036        assert_eq!(lives_in(&it.func, sum), it.entry, "it is carrying the multiply");
1037        assert_eq!(lives_in(&it.func, product), it.entry);
1038        sound(&it.func, &mut it.names);
1039    }
1040
1041    #[test]
1042    fn a_chain_of_cheap_links_that_carries_nothing_stays_where_it_is() {
1043        // Every link rides along while the plan is being made, since what reads it has not been
1044        // read yet, and the whole chain comes back out in one walk when the end of it turns out
1045        // to be nothing. Two links, so the walk has to answer the second one before the first.
1046        let mut it = counted(14);
1047        let mut build = Builder::new(&mut it.func, it.body);
1048        let first = build.binary(Opcode::Add, it.limit, it.limit, Flags::NONE);
1049        let second = build.binary(Opcode::Add, first, it.limit, Flags::NONE);
1050        tucked(&mut it.func, it.body);
1051
1052        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
1053        assert_eq!(stats.count(Kind::Missed, PRESSURE), 2);
1054        assert_eq!(stats.count(Kind::Optimized, HOISTED), 0);
1055        assert_eq!(lives_in(&it.func, first), it.body);
1056        assert_eq!(lives_in(&it.func, second), it.body);
1057        sound(&it.func, &mut it.names);
1058    }
1059
1060    #[test]
1061    fn the_same_add_moves_when_the_loop_has_room() {
1062        let mut it = counted(0);
1063        let sum = Builder::new(&mut it.func, it.body).binary(
1064            Opcode::Add,
1065            it.limit,
1066            it.limit,
1067            Flags::NONE,
1068        );
1069        tucked(&mut it.func, it.body);
1070
1071        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
1072        assert_eq!(stats.count(Kind::Missed, PRESSURE), 0);
1073        assert_eq!(lives_in(&it.func, sum), it.entry);
1074        sound(&it.func, &mut it.names);
1075    }
1076
1077    #[test]
1078    fn a_loop_with_two_ways_in_is_left_alone() {
1079        // No preheader means nowhere to put anything, and section 26 owns making one.
1080        let mut names = Interner::new();
1081        let signature = Signature::new().with_params(&[Type::I1, Type::int(32)]);
1082        let mut func = Func::new(names.intern("f"), signature);
1083        let entry = func.create_block();
1084        let low = func.create_block();
1085        let high = func.create_block();
1086        let head = func.create_block();
1087        let body = func.create_block();
1088        let done = func.create_block();
1089        let either = func.append_param(entry, Type::I1);
1090        let n = func.append_param(entry, Type::int(32));
1091        let i = func.append_param(head, Type::int(32));
1092        Builder::new(&mut func, entry).br_if(either, low, &[], high, &[]);
1093        let zero = Builder::new(&mut func, low).iconst(Type::int(32), 0);
1094        Builder::new(&mut func, low).jump(head, &[zero]);
1095        let one = Builder::new(&mut func, high).iconst(Type::int(32), 1);
1096        Builder::new(&mut func, high).jump(head, &[one]);
1097        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, n);
1098        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
1099        let product = Builder::new(&mut func, body).binary(Opcode::Mul, n, n, Flags::NONE);
1100        let next = Builder::new(&mut func, body).binary(Opcode::Add, i, product, Flags::NONE);
1101        Builder::new(&mut func, body).jump(head, &[next]);
1102        Builder::new(&mut func, done).ret(&[]);
1103
1104        let stats = hoist(&mut func, &mut Fuel::unlimited());
1105        assert_eq!(stats.count(Kind::Missed, NO_PREHEADER), 1);
1106        assert_eq!(lives_in(&func, product), body);
1107        sound(&func, &mut names);
1108    }
1109
1110    #[test]
1111    fn a_loop_with_no_way_out_gets_the_pure_hoist_and_not_the_other_one() {
1112        // Post-dominance in here is answered against an edge document 06.8's analysis invented, so
1113        // nothing in the loop counts as running and only what may move anywhere moves.
1114        let mut names = Interner::new();
1115        let signature = Signature::new().with_params(&[Type::int(32), Type::PTR]);
1116        let mut func = Func::new(names.intern("f"), signature);
1117        let entry = func.create_block();
1118        let head = func.create_block();
1119        let n = func.append_param(entry, Type::int(32));
1120        let pointer = func.append_param(entry, Type::PTR);
1121        Builder::new(&mut func, entry).jump(head, &[]);
1122        let mut build = Builder::new(&mut func, head);
1123        let product = build.binary(Opcode::Mul, n, n, Flags::NONE);
1124        let read = build.load(Type::int(32), pointer, record(4), Flags::NONE);
1125        build.jump(head, &[]);
1126
1127        let stats = hoist(&mut func, &mut Fuel::unlimited());
1128        assert_eq!(stats.count(Kind::Missed, SPINS), 1);
1129        assert_eq!(stats.count(Kind::Missed, SPECULATIVE), 1);
1130        assert_eq!(lives_in(&func, product), entry, "arithmetic is safe anywhere");
1131        assert_eq!(lives_in(&func, read), head, "the address is still one nobody has vouched for");
1132        sound(&func, &mut names);
1133    }
1134
1135    #[test]
1136    fn an_invariant_comes_all_the_way_out_of_a_nest_in_one_run() {
1137        // Innermost first, so the inner loop leaves the product in the outer loop's preheader,
1138        // which is a block of the outer loop, and the outer loop's turn takes it the rest of the
1139        // way.
1140        let mut names = Interner::new();
1141        let signature = Signature::new().with_params(&[Type::int(32)]);
1142        let mut func = Func::new(names.intern("f"), signature);
1143        let entry = func.create_block();
1144        let outer = func.create_block();
1145        let ready = func.create_block();
1146        let inner = func.create_block();
1147        let deep = func.create_block();
1148        let latch = func.create_block();
1149        let done = func.create_block();
1150        let n = func.append_param(entry, Type::int(32));
1151        let i = func.append_param(outer, Type::int(32));
1152        let j = func.append_param(inner, Type::int(32));
1153        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
1154        Builder::new(&mut func, entry).jump(outer, &[zero]);
1155        let outer_test = Builder::new(&mut func, outer).icmp(IntPred::Slt, i, n);
1156        Builder::new(&mut func, outer).br_if(outer_test, ready, &[], done, &[]);
1157        let start = Builder::new(&mut func, ready).iconst(Type::int(32), 0);
1158        Builder::new(&mut func, ready).jump(inner, &[start]);
1159        let inner_test = Builder::new(&mut func, inner).icmp(IntPred::Slt, j, n);
1160        Builder::new(&mut func, inner).br_if(inner_test, deep, &[], latch, &[]);
1161        let mut build = Builder::new(&mut func, deep);
1162        let product = build.binary(Opcode::Mul, n, n, Flags::NONE);
1163        let one = build.iconst(Type::int(32), 1);
1164        let next_j = build.binary(Opcode::Add, j, one, Flags::NONE);
1165        build.jump(inner, &[next_j]);
1166        let mut build = Builder::new(&mut func, latch);
1167        let step = build.iconst(Type::int(32), 1);
1168        let next_i = build.binary(Opcode::Add, i, step, Flags::NONE);
1169        build.jump(outer, &[next_i]);
1170        Builder::new(&mut func, done).ret(&[]);
1171
1172        let stats = hoist(&mut func, &mut Fuel::unlimited());
1173        assert_eq!(stats.count(Kind::Optimized, HOISTED), 2, "one level and then the other");
1174        assert_eq!(lives_in(&func, product), entry);
1175        sound(&func, &mut names);
1176    }
1177
1178    #[test]
1179    fn a_function_with_no_loop_in_it_is_untouched() {
1180        let mut names = Interner::new();
1181        let mut func = Func::new(names.intern("f"), Signature::new());
1182        let entry = func.create_block();
1183        Builder::new(&mut func, entry).ret(&[]);
1184
1185        let stats = hoist(&mut func, &mut Fuel::unlimited());
1186        assert!(!stats.changed());
1187        sound(&func, &mut names);
1188    }
1189
1190    #[test]
1191    fn the_address_of_a_global_moves_only_when_something_that_reads_it_moves() {
1192        // The load is what is worth hoisting and the address is free, so the address goes with it
1193        // and would have gone nowhere on its own. Getting this wrong in the other direction is
1194        // what refusing a free value up front does: the load reads an address defined in the loop,
1195        // so refusing the address makes the load look like something the loop changes.
1196        let mut it = counted(0);
1197        let grid = it.names.intern("grid");
1198        let mut build = Builder::new(&mut it.func, it.head);
1199        let at = build.value(
1200            InstData { extra: Extra::Symbol(grid), ..InstData::new(Opcode::GlobalAddr) },
1201            Type::PTR,
1202        );
1203        let read = build.load(Type::int(32), at, record(4), Flags::NONE);
1204        tucked(&mut it.func, it.head);
1205
1206        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
1207        assert_eq!(stats.count(Kind::Optimized, HOISTED), 2, "the load and its address");
1208        assert_eq!(lives_in(&it.func, at), it.entry);
1209        assert_eq!(lives_in(&it.func, read), it.entry);
1210        assert!(position(&it.func, at) < position(&it.func, read));
1211        checked(&it.func, &mut it.names, &[grid]);
1212    }
1213
1214    #[test]
1215    fn the_address_of_a_global_on_its_own_stays_where_it_is() {
1216        // Nothing to carry, so it is a register held for the length of the loop to save an
1217        // instruction that costs what a copy of it costs.
1218        let mut it = counted(0);
1219        let grid = it.names.intern("grid");
1220        let at = Builder::new(&mut it.func, it.body).value(
1221            InstData { extra: Extra::Symbol(grid), ..InstData::new(Opcode::GlobalAddr) },
1222            Type::PTR,
1223        );
1224        tucked(&mut it.func, it.body);
1225
1226        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
1227        assert_eq!(stats.count(Kind::Optimized, HOISTED), 0);
1228        assert_eq!(lives_in(&it.func, at), it.body);
1229        checked(&it.func, &mut it.names, &[grid]);
1230    }
1231
1232    /// An alloca is here so the load below has an address the function can vouch for.
1233    #[test]
1234    fn the_same_load_stays_once_the_loop_writes_anything_at_all() {
1235        // The address is the same address and the storage is the same four bytes, and the store
1236        // is to somewhere else entirely. It does not matter: this function does not carry the
1237        // memory chain, so there is nothing to read that says the store and the load are apart,
1238        // and a load in a loop that writes is a load that stays. Coarse on purpose, and the
1239        // remark says which of the reasons it was rather than leaving it to be guessed at.
1240        let mut it = counted(0);
1241        let mem = it.func.add_mem(record(4));
1242        let slot = Builder::new(&mut it.func, it.entry)
1243            .value(InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) }, Type::PTR);
1244        tucked(&mut it.func, it.entry);
1245        let mut build = Builder::new(&mut it.func, it.body);
1246        let read = build.load(Type::int(32), slot, record(4), Flags::NONE);
1247        build.store(read, it.pointer, record(4), Flags::NONE);
1248        tucked(&mut it.func, it.body);
1249
1250        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
1251        assert_eq!(stats.count(Kind::Missed, MEMORY), 1);
1252        assert_eq!(lives_in(&it.func, read), it.body);
1253        sound(&it.func, &mut it.names);
1254    }
1255
1256    /// Builds `cap_extent` of the function's pointer in `block`, with the `cap_of` it reads.
1257    fn extent(func: &mut Func, block: Block, pointer: Value) -> Value {
1258        let mut build = Builder::new(func, block);
1259        let want = build.iconst(Type::int(64), i128::from(i64::MAX));
1260        let args = build.func().push_values(&[pointer]);
1261        let of = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
1262        let args = build.func().push_values(&[of, pointer, want]);
1263        build.value(InstData { args, ..InstData::new(Opcode::CapExtent) }, Type::int(64))
1264    }
1265
1266    #[test]
1267    fn asking_how_big_an_object_is_moves_out_of_a_loop_that_writes_to_it() {
1268        // The loop writes through the very pointer being asked about, and the answer is the same
1269        // every time round all the same: how much room is left from a pointer is a fact about the
1270        // allocation rather than about what is in it. A load here would stay, and the test above
1271        // is that load.
1272        let mut it = counted(0);
1273        let asked = extent(&mut it.func, it.body, it.pointer);
1274        let mut build = Builder::new(&mut it.func, it.body);
1275        let byte = build.iconst(Type::int(32), 0);
1276        build.store(byte, it.pointer, record(4), Flags::NONE);
1277        tucked(&mut it.func, it.body);
1278
1279        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
1280        assert_eq!(stats.count(Kind::Missed, MEMORY), 0);
1281        assert_eq!(lives_in(&it.func, asked), it.entry, "it is in front of the loop now");
1282        sound(&it.func, &mut it.names);
1283    }
1284
1285    #[test]
1286    fn asking_how_big_an_object_is_stays_in_a_loop_that_calls_something_that_could_free() {
1287        // A call the module has nothing to say about could be `free`, and then the answer before
1288        // the call and the answer after it are different numbers. `crate::split` refuses a loop
1289        // for the same call and says so in the same words.
1290        let mut it = counted(0);
1291        let asked = extent(&mut it.func, it.body, it.pointer);
1292        let signature = it.func.add_signature(Signature::new().with_params(&[Type::PTR]));
1293        let callee = it.names.intern("might_free");
1294        Builder::new(&mut it.func, it.body).call(callee, signature, &[it.pointer]);
1295        tucked(&mut it.func, it.body);
1296
1297        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
1298        assert_eq!(stats.count(Kind::Missed, MEMORY), 1);
1299        assert_eq!(lives_in(&it.func, asked), it.body);
1300        sound(&it.func, &mut it.names);
1301    }
1302
1303    #[test]
1304    fn asking_how_big_an_object_is_moves_past_a_call_the_summary_says_cannot_free() {
1305        // The same loop with the same call, marked `nofree` by `crate::nofree`. That flag is the
1306        // whole difference between this test and the one above it, and a loop with a `memcpy` in
1307        // it is the shape it is about.
1308        let mut it = counted(0);
1309        let asked = extent(&mut it.func, it.body, it.pointer);
1310        let signature = it.func.add_signature(Signature::new().with_params(&[Type::PTR]));
1311        let callee = it.names.intern("cannot_free");
1312        let call = Builder::new(&mut it.func, it.body).call(callee, signature, &[it.pointer]);
1313        it.func[call].flags |= Flags::NOFREE;
1314        tucked(&mut it.func, it.body);
1315
1316        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
1317        assert_eq!(stats.count(Kind::Missed, MEMORY), 0);
1318        assert_eq!(lives_in(&it.func, asked), it.entry, "it is in front of the loop now");
1319        sound(&it.func, &mut it.names);
1320    }
1321
1322    #[test]
1323    fn a_load_of_a_local_the_loop_does_not_write_moves_out_of_the_body() {
1324        let mut it = counted(0);
1325        let mem = it.func.add_mem(record(4));
1326        let slot = Builder::new(&mut it.func, it.entry)
1327            .value(InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) }, Type::PTR);
1328        tucked(&mut it.func, it.entry);
1329        let read =
1330            Builder::new(&mut it.func, it.body).load(Type::int(32), slot, record(4), Flags::NONE);
1331        tucked(&mut it.func, it.body);
1332
1333        let stats = hoist(&mut it.func, &mut Fuel::unlimited());
1334        assert_eq!(stats.count(Kind::Optimized, HOISTED), 1);
1335        assert_eq!(lives_in(&it.func, read), it.entry, "four bytes of four are always there");
1336        sound(&it.func, &mut it.names);
1337    }
1338}