Skip to main content

rucc_opt/
split.rs

1//! Splits a loop into a run of iterations that needs no checks and the rest of it, which keeps them.
2//!
3//! Design: `spec/safe-memory/07-check-elimination.md` section 7.4, which names this and says what it
4//! is for: "Loop splitting is the general form. The checked part and the unchecked part are divided
5//! at `min(n, extent / sizeof(T))`."
6//!
7//! [`crate::hoist`] is the pass next door and it answers a different question. It puts one check in
8//! front of a loop that covers every access the loop makes, which needs the loop to make every one
9//! of them: an exact count, one way out, and a check every iteration reaches. Most loops in real code
10//! are not like that. The census on tamnd/rucc#782 says that of the roughly fifteen hundred checks
11//! SQLite still carries at `-O2`, six hundred and ninety two are in loops with a second way out and
12//! sixty four are checks an iteration can finish without reaching. Neither is a loop hoisting can say
13//! anything about, and both are loops this one can, because it never has to claim the loop reaches
14//! the end of what it might read. It only has to know a prefix that is safe.
15//!
16//! # What the two halves are
17//!
18//! The loop is copied. The original becomes the fast half and loses its checks, the copy becomes the
19//! slow half and keeps them, and a new block in front of the original decides which one runs. That
20//! block counts iterations of the fast half and hands over to the slow half once the count reaches a
21//! limit worked out in the preheader.
22//!
23//! The counter is a new one rather than the loop's own, and the test is against a limit rather than
24//! against anything the loop compares. That is what makes this work on a loop with several ways out:
25//! the fast half keeps every exit the loop had, so leaving early still leaves early, and the extra
26//! test is only ever the reason the fast half stops early and never the reason it runs longer.
27//!
28//! # Where the limit comes from
29//!
30//! For a check whose address is `first + i * step` reading `reach` bytes each time, every iteration
31//! with `i * step + reach <= extent` is one the check cannot fail on, where `extent` is how many
32//! bytes from `first` on belong to whatever owns `first`. So the limit is
33//! `(extent - reach) / step + 1`, or zero when `extent` is smaller than `reach`, and where a loop has
34//! several such checks in it the limit is the smallest of theirs.
35//!
36//! The extent is the half of that a compiler cannot work out, so it is asked at run time, through the
37//! `cap_extent` query that tamnd/rucc#792 added. The query takes a limit on how far to look and the
38//! answer is never more than that, which is why this pass still wants a trip count: what it asks for
39//! is how many bytes the loop was going to read anyway, so the walk in the runtime is bounded by work
40//! the loop is already doing. A count that is too small costs iterations in the slow half and a count
41//! that is too large costs a slightly longer walk, and neither is a wrong answer, which is why the
42//! count is read from any exit that offers one rather than from an exit that runs every time.
43//!
44//! # Why the fast half may drop a check
45//!
46//! `check_bounds` asks whether the bytes an access names lie inside one object. Every address in
47//! `[first, first + extent)` is inside the object that owns `first`, by what the query answers, and
48//! the limit is exactly the iterations whose access stays in that window. So no check in the fast
49//! half could have failed.
50//!
51//! `check_live` asks whether anything owns the address right now, and the query answered that too,
52//! since a byte belonging to the owner of `first` is a byte with an owner. Right now is the catch,
53//! and it is why nothing that could free may be in the loop. A call in the body could free the object
54//! between the question and the iteration that reads it, and then the fast half would read freed
55//! storage with nothing to say so. That is the same restriction hoisting has, for a weaker reason,
56//! and lifting it is a matter of asking `crate::nofree` about the callee rather than refusing every
57//! call.
58//!
59//! Two answers of the query carry the weight and both are argued where the query is implemented. An
60//! address no watched region covers gets the whole limit back, so a loop over a local or a global
61//! splits into a fast half that runs the whole way, which is right because no check on such an
62//! address ever fires under this milestone. An address whose granule nobody owns gets zero, so the
63//! limit is zero, the fast half runs no iterations, and the check inside the slow half is what reports
64//! the dangling pointer, at the access rather than at the loop.
65//!
66//! # Which loops
67//!
68//! Innermost, one latch, a preheader, a count, nothing in it that could free, and no value defined
69//! inside it that anything outside reads. The last is loop closed form, which [`crate::canon`]
70//! establishes, and it is checked rather than assumed because the copy would otherwise leave a reader
71//! outside the loop seeing whichever half happened to define the value.
72//!
73//! Canonicalization runs a long way in front of this, and `simplify-cfg` between the two undoes some
74//! of what it did, so on SQLite the closed form condition is what refuses 351 of the checks this
75//! would otherwise have taken out. Running canonicalization again in front of this gets 156 of them
76//! back and costs 17672 bytes of `.text`, which is a bad trade for eleven more checks, so the answer
77//! is for this to repair the exits of the one loop it is splitting rather than for the pipeline to
78//! repair every loop in the function. That is its own piece of work.
79//!
80//! Not every check in the loop has to be one this can size. A check whose address the analysis cannot
81//! follow simply stays in both halves, and the fast half is then a loop with fewer checks in it rather
82//! than none. That is worth having on its own and it is worth having because it is what a real loop
83//! looks like: one sweep the analysis reads and one index that came out of a table.
84//!
85//! # Which level
86//!
87//! `-O2` and `-O3`, alongside `crate::unroll` and for the same reason. The loop body is copied, so
88//! the function grows by about the size of the loop, and buying speed with code is what those levels
89//! are for and what `-Os` and `-Oz` are for declining.
90
91use std::collections::{HashMap, HashSet};
92
93use rucc_cost::heuristics;
94use rucc_ir::{
95    Block, BlockCall, Builder, Extra, Flags, Func, Inst, InstData, IntPred, Opcode, Type, Value,
96};
97
98use crate::cfg::Cfg;
99use crate::copy;
100use crate::discharge::operand_of;
101use crate::loops::{LoopId, Loops};
102use crate::scev::{Evolution, Scev};
103use crate::trip::{Around, counted, covered, inst_of};
104use crate::{Analyses, Fuel, Pass, Preserved, Stats};
105
106/// What is reported when a loop is split.
107const SPLIT: &str = "loop split, the iterations in front of the first one that could fail a check \
108                     run without them";
109
110/// What is reported when the pass ran out of fuel with a loop it was about to split.
111const NO_FUEL: &str = "loop left alone, the pass ran out of fuel";
112
113/// What is reported for a loop with nowhere to work the limit out.
114const NO_PREHEADER: &str = "loop left alone, it has no block in front of it to put a check in";
115
116/// What is reported for a loop with a loop inside it.
117const A_LOOP_INSIDE: &str = "loop left alone, it has another loop inside it";
118
119/// What is reported for a loop with more than one way round.
120const MANY_LATCHES: &str = "loop left alone, it goes back to its header from more than one place";
121
122/// What is reported for a loop with a call in it.
123const A_CALL_INSIDE: &str = "loop left alone, a call in it might free what the loop is reading";
124
125/// What is reported for a loop holding something the copier cannot copy.
126const NOT_COPYABLE: &str = "loop left alone, something in it carries a side table this cannot copy";
127
128/// What is reported for a loop whose values are read after it without going through a parameter.
129const ESCAPES: &str = "loop left alone, a value it defines is read outside it";
130
131/// What is reported for a loop whose two halves would be too much code.
132const TOO_BIG: &str = "loop left alone, the two halves would be more code than the limit allows";
133
134/// What is reported for a check whose address does not walk the loop.
135const NOT_A_SWEEP: &str = "check kept in both halves, its address does not walk the loop by a \
136                           constant";
137
138/// What is reported for a check whose address the analysis has nothing to say about.
139const NOT_FOLLOWED: &str = "check kept in both halves, what its address does round the loop is not \
140                            something the analysis follows";
141
142/// What is reported for a check whose address does not move.
143const DOES_NOT_MOVE: &str = "check kept in both halves, its address is the same every iteration";
144
145/// What is reported for a check whose address walks backwards.
146const BACKWARDS: &str = "check kept in both halves, its address walks the loop from high to low";
147
148/// What is reported for a check whose step does not keep its alignment.
149const MISALIGNED: &str =
150    "check kept in both halves, its step is not a whole number of its alignment";
151
152/// What is reported for a check that already covers a range the program worked out.
153const ALREADY_COMPUTED: &str =
154    "check kept in both halves, how many bytes it covers is a number only the program has";
155
156/// The pass.
157#[derive(Debug)]
158pub struct Split;
159
160impl Pass for Split {
161    fn name(&self) -> &'static str {
162        "split"
163    }
164
165    fn describe(&self) -> &'static str {
166        "a loop becomes a run of iterations with no checks in it and the rest of the loop with them"
167    }
168
169    fn preserves(&self) -> Preserved {
170        // Blocks appear and edges move, so nothing built on the graph stands.
171        Preserved::NONE
172    }
173
174    fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
175        let mut stats = Stats::new();
176        if func.entry().is_none() {
177            return stats;
178        }
179        let cfg = an.cfg(func).clone();
180        let loops = an.loops(func).clone();
181        if loops.count() == 0 {
182            return stats;
183        }
184
185        // Worked out first and applied afterwards, because scalar evolution reads the function and
186        // the transformation writes it. Every plan is about an innermost loop and no two innermost
187        // loops share a block, so applying one leaves every other one's blocks where they were.
188        let mut plans = Vec::new();
189        {
190            let mut scev = Scev::new(func, &cfg, &loops);
191            for id in loops.all() {
192                sweep(func, &cfg, &loops, &mut scev, id, &mut plans, &mut stats);
193            }
194        }
195
196        let mut changed = false;
197        for plan in plans {
198            if !fuel.take() {
199                stats.missed(NO_FUEL);
200                continue;
201            }
202            apply(func, &plan);
203            stats.optimized(SPLIT);
204            changed = true;
205        }
206        if changed {
207            an.clear();
208        }
209        stats
210    }
211}
212
213/// One check the fast half will not need, and the walk that says so.
214#[derive(Debug)]
215struct Sweep {
216    /// The check itself, which is removed from the fast half and kept in the copy.
217    check: Inst,
218    /// Where the first iteration's address is computed from.
219    base: Value,
220    /// How far past that value the first iteration reads.
221    offset: i128,
222    /// How far the address moves each time round, which is a positive number of bytes.
223    step: i128,
224    /// How many bytes one access covers.
225    reach: i128,
226}
227
228/// One loop to split, worked out before anything is written.
229#[derive(Debug)]
230struct Plan {
231    /// Where the limit is worked out.
232    preheader: Block,
233    /// The block the guard takes over from.
234    header: Block,
235    /// The block the back edge leaves from, which is where the iteration count goes up.
236    latch: Block,
237    /// Everything that is copied, which is the whole loop.
238    body: Vec<Block>,
239    /// How many times the loop goes round, which is what the runtime is asked to look no further
240    /// than.
241    around: Around,
242    /// The checks the fast half will not need, which is never empty in a plan.
243    sweeps: Vec<Sweep>,
244}
245
246/// Plans a loop, or counts what stopped it.
247///
248/// Nothing is reported for a loop with no check in it, because a loop that does no memory access is
249/// not a missed opportunity and a report for every one of them would bury the loops that are.
250fn sweep(
251    func: &Func,
252    cfg: &Cfg,
253    loops: &Loops,
254    scev: &mut Scev<'_>,
255    id: LoopId,
256    plans: &mut Vec<Plan>,
257    stats: &mut Stats,
258) {
259    let body = loops.blocks(id).to_vec();
260    let checks: Vec<Inst> = body
261        .iter()
262        .flat_map(|&block| func.insts(block).collect::<Vec<Inst>>())
263        .filter(|&inst| matches!(func[inst].opcode, Opcode::CheckBounds | Opcode::CheckLive))
264        .collect();
265    if checks.is_empty() {
266        return;
267    }
268
269    let (preheader, latch) = match shaped(func, cfg, loops, id, &body) {
270        Ok(shape) => shape,
271        Err(why) => {
272            stats.missed(why);
273            return;
274        }
275    };
276    let around = match counted(scev, id) {
277        Ok(around) => around,
278        Err(why) => {
279            stats.missed(why);
280            return;
281        }
282    };
283
284    let mut sweeps = Vec::new();
285    for check in checks {
286        match walked(func, scev, id, check) {
287            Ok(sweep) => sweeps.push(sweep),
288            Err(why) => stats.missed(why),
289        }
290    }
291    if sweeps.is_empty() {
292        return;
293    }
294    plans.push(Plan { preheader, header: loops.header(id), latch, body, around, sweeps });
295}
296
297/// The preheader and the latch of a loop this pass may copy, or why there is not one.
298///
299/// The conditions are the module comment's. The one worth restating is the call, because it is the
300/// only one that is about what the fast half is allowed to leave out rather than about whether the
301/// copy can be made at all: the extent is asked once before the loop and believed for the whole of
302/// the fast half, so anything that could hand the storage back in the middle would make the answer
303/// stale, and the fast half has nothing left in it to notice.
304fn shaped(
305    func: &Func,
306    cfg: &Cfg,
307    loops: &Loops,
308    id: LoopId,
309    body: &[Block],
310) -> Result<(Block, Block), &'static str> {
311    let Some(preheader) = loops.preheader(cfg, id) else {
312        return Err(NO_PREHEADER);
313    };
314    let [latch] = loops.latches(id) else {
315        return Err(MANY_LATCHES);
316    };
317    for &block in body {
318        if loops.innermost(block) != Some(id) {
319            return Err(A_LOOP_INSIDE);
320        }
321        for inst in func.insts(block) {
322            if matches!(
323                func[inst].opcode,
324                Opcode::Call
325                    | Opcode::CallIndirect
326                    | Opcode::TailCall
327                    | Opcode::InlineAsm
328                    | Opcode::MetaEnd
329                    | Opcode::MetaTransfer
330            ) {
331                return Err(A_CALL_INSIDE);
332            }
333            if !copy::copyable(func, inst) {
334                return Err(NOT_COPYABLE);
335            }
336        }
337    }
338    let inside: HashSet<Block> = body.iter().copied().collect();
339    if escapes(func, body, &inside) {
340        return Err(ESCAPES);
341    }
342    let size = body.iter().map(|&block| func.insts(block).count()).sum::<usize>();
343    if size > heuristics::SPLIT_MAX_INSNS as usize {
344        return Err(TOO_BIG);
345    }
346    Ok((preheader, *latch))
347}
348
349/// Whether anything outside the loop reads a value defined inside it.
350///
351/// After [`crate::canon`] there is no such value, because loop closed form has already routed every
352/// one of them through a parameter of the block the loop leaves to. Where there is one, the two
353/// halves would leave it reading whichever of them happened to define it, so this is refused rather
354/// than repaired.
355fn escapes(func: &Func, body: &[Block], inside: &HashSet<Block>) -> bool {
356    let mut defined: HashSet<Value> = HashSet::new();
357    for &block in body {
358        defined.extend(func[block].params.iter().copied());
359        for inst in func.insts(block) {
360            defined.extend(func[inst].results());
361        }
362    }
363    for block in func.blocks() {
364        if inside.contains(&block) {
365            continue;
366        }
367        for inst in func.insts(block) {
368            if func[func[inst].args].iter().any(|value| defined.contains(value)) {
369                return true;
370            }
371            for call in func.successors(inst) {
372                if func[call.args].iter().any(|value| defined.contains(value)) {
373                    return true;
374                }
375            }
376        }
377    }
378    false
379}
380
381/// What one check's address does round the loop, or why the pass cannot say.
382fn walked(
383    func: &Func,
384    scev: &mut Scev<'_>,
385    id: LoopId,
386    check: Inst,
387) -> Result<Sweep, &'static str> {
388    let args = &func[func[check].args];
389    // A check that already carries its own extent is one hoisting put somewhere, and how many bytes
390    // it covers is not a number this pass can divide by a step.
391    if args.len() > 2 {
392        return Err(ALREADY_COMPUTED);
393    }
394    let (Some(&capability), Some(&pointer)) = (args.first(), args.get(1)) else {
395        return Err(NOT_A_SWEEP);
396    };
397    if operand_of(func, capability, Opcode::CapOf, 0) != Some(pointer) {
398        return Err(NOT_A_SWEEP);
399    }
400    // A liveness check reads no bytes, so the window it needs is the one byte its address is in.
401    // A bounds check carries how many it reads in its payload.
402    let (reach, align) = match func[check].extra {
403        Extra::Mem(held) => (i128::from(func[held].size), i128::from(func[held].align)),
404        _ => (1, 1),
405    };
406
407    let chrec = match scev.evolution(id, pointer) {
408        Evolution::Affine(chrec) => chrec,
409        // An address that does not move at all is one check covering the same bytes every time
410        // round, which is a check to hoist rather than a loop to split, and hoisting is where that
411        // belongs. It is reported separately so the census says how often the two passes disagree
412        // about the same loop.
413        Evolution::Invariant(_) => return Err(DOES_NOT_MOVE),
414        _ => return Err(NOT_FOLLOWED),
415    };
416    let Some(step) = chrec.step.as_number() else {
417        return Err(NOT_A_SWEEP);
418    };
419    if step <= 0 {
420        return Err(BACKWARDS);
421    }
422    // Scale one because the base is an address. Anything else is a multiple of a pointer, which is
423    // not a thing the loop computed, so it is a shape this reads rather than a case to handle.
424    let (Some(base), 1) = (chrec.base.value, chrec.base.scale) else {
425        return Err(NOT_A_SWEEP);
426    };
427    if step % align != 0 {
428        return Err(MISALIGNED);
429    }
430    Ok(Sweep { check, base, offset: chrec.base.offset, step, reach })
431}
432
433/// Makes the two halves and the block that chooses between them.
434///
435/// The order matters in two places. The copy is made before anything is rewired, so the copy's back
436/// edge is remapped to the copy's own header rather than to a guard that did not exist yet. The
437/// checks come out of the fast half last, so the copy still has them.
438fn apply(func: &mut Func, plan: &Plan) {
439    // The slow half, which is the loop as it stands, under a substitution that renames everything it
440    // defines. Nothing is seeded, so its header gets parameters of its own, which is what a copy
441    // reached from a block that also reaches the original needs.
442    let mut renamed: HashMap<Value, Value> = HashMap::new();
443    let copies = copy::blocks(func, &plan.body, &mut renamed);
444    let slow = copies[&plan.header];
445
446    // The guard, which takes over the header's place: the preheader arrives here, the back edge
447    // comes back to here, and the header is reached from here and nowhere else. Its first parameter
448    // is a counter of its own, because the loop's counter is not something this pass has to find and
449    // a loop with several ways out may not have one.
450    let word = Type::int(64);
451    let types: Vec<Type> = func[plan.header].params.iter().map(|&param| func[param].ty).collect();
452    let guard = func.create_block();
453    let round = func.append_param(guard, word);
454    let carried: Vec<Value> = types.iter().map(|&ty| func.append_param(guard, ty)).collect();
455
456    let (limit, start) = limited(func, plan);
457
458    let mut build = Builder::new(func, guard);
459    let inside = build.icmp(IntPred::Slt, round, limit);
460    build.br_if(inside, plan.header, &carried, slow, &carried);
461
462    // The way in, which now hands the guard a count of no iterations so far.
463    let term = func.terminator(plan.preheader).expect("a preheader ends in a jump to the header");
464    route(func, term, plan.header, guard, start);
465
466    // The way round, which counts one more. The counter is the guard's parameter and the guard
467    // dominates every block in the fast half, so the latch may read it.
468    let term = func.terminator(plan.latch).expect("a latch ends in a branch back to the header");
469    let mut build = Builder::new(func, plan.latch);
470    let one = build.iconst(word, 1);
471    let next = build.binary(Opcode::Add, round, one, Flags::NSW);
472    for value in [one, next] {
473        let inst = inst_of(func, value);
474        func.remove_inst(inst);
475        func.insert_before(inst, term);
476    }
477    route(func, term, plan.header, guard, next);
478
479    // The checks the fast half does not need. The `cap_of` each one was reading is left where it is,
480    // for `dce` after this pass to take away, which is the arrangement `crate::hoist` and
481    // `crate::discharge` are both in.
482    for sweep in &plan.sweeps {
483        func.remove_inst(sweep.check);
484    }
485}
486
487/// Sends every edge this terminator has to `from` to `to` instead, with one more argument in front.
488fn route(func: &mut Func, term: Inst, from: Block, to: Block, first: Value) {
489    for at in func.target_list(term).iter() {
490        let call = func[at];
491        if call.block != from {
492            continue;
493        }
494        let mut args = vec![first];
495        args.extend_from_slice(&func[call.args]);
496        let args = func.push_values(&args);
497        func.set_block_call(at, BlockCall { block: to, args });
498    }
499}
500
501/// Builds the number of iterations the fast half may run, and the zero the preheader starts it at.
502///
503/// One `cap_extent` per check and the smallest of what they allow, all of it in the preheader in
504/// front of the jump into the loop. A builder appends to the end of a block, which in a block that
505/// already has its terminator is after it, so everything is built first and then moved in front of
506/// the terminator in the order it was built.
507fn limited(func: &mut Func, plan: &Plan) -> (Value, Value) {
508    let term = func.terminator(plan.preheader).expect("a preheader ends in a jump to the header");
509    let mut made = Vec::new();
510    let mut build = Builder::new(func, plan.preheader);
511    let start = build.iconst(Type::int(64), 0);
512    made.push(start);
513
514    let mut limit: Option<Value> = None;
515    for sweep in &plan.sweeps {
516        let allows = reachable(&mut build, &mut made, sweep, plan.around);
517        limit = Some(match limit {
518            None => allows,
519            Some(so_far) => {
520                let smaller = build.icmp(IntPred::Slt, allows, so_far);
521                made.push(smaller);
522                let least = build.select(smaller, allows, so_far);
523                made.push(least);
524                least
525            }
526        });
527    }
528    let limit = limit.expect("a plan holds at least one check");
529
530    for value in made {
531        let inst = inst_of(func, value);
532        func.remove_inst(inst);
533        func.insert_before(inst, term);
534    }
535    (limit, start)
536}
537
538/// How many iterations one check allows, which is `(extent - reach) / step + 1` and never negative.
539///
540/// The subtraction and the addition carry `nsw` and the division does not need it. Everything here
541/// is worked out from the extent, which the runtime answers with a count of bytes it walked and so
542/// is never negative and never larger than the object, so none of this can leave sixty four bits
543/// whatever the limit it was asked for turned out to be.
544fn reachable(
545    build: &mut Builder<'_>,
546    made: &mut Vec<Value>,
547    sweep: &Sweep,
548    around: Around,
549) -> Value {
550    let word = Type::int(64);
551    let first = if sweep.offset == 0 {
552        sweep.base
553    } else {
554        let by = build.iconst(word, sweep.offset);
555        made.push(by);
556        let args = build.func().push_values(&[sweep.base, by]);
557        let sum = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
558        made.push(sum);
559        sum
560    };
561    // How many bytes the loop was going to read, which is how far the runtime is asked to look and
562    // nothing more. An answer short of the truth costs iterations in the slow half and is never
563    // wrong, so a count that saturates rather than one that refuses is the right thing here.
564    let want = match around {
565        Around::Number(times) => {
566            let far = times.saturating_mul(sweep.step).saturating_add(sweep.reach);
567            let far = i64::try_from(far).unwrap_or(i64::MAX);
568            let bytes = build.iconst(word, i128::from(far));
569            made.push(bytes);
570            bytes
571        }
572        Around::Computed(count, reading) => {
573            covered(build, made, count, sweep.step, sweep.reach, reading, Flags::NONE)
574        }
575    };
576
577    let args = build.func().push_values(&[first]);
578    let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
579    made.push(capability);
580    let args = build.func().push_values(&[capability, first, want]);
581    let extent = build.value(InstData { args, ..InstData::new(Opcode::CapExtent) }, word);
582    made.push(extent);
583
584    let reach = build.iconst(word, sweep.reach);
585    made.push(reach);
586    let left = build.binary(Opcode::Sub, extent, reach, Flags::NSW);
587    made.push(left);
588    let zero = build.iconst(word, 0);
589    made.push(zero);
590    // Not even one access fits, which is a dangling pointer or an object smaller than the thing being
591    // read out of it. The fast half runs no iterations and the check in the slow half reports it, at
592    // the access rather than at the loop.
593    let short = build.icmp(IntPred::Slt, left, zero);
594    made.push(short);
595
596    let mut steps = left;
597    if sweep.step != 1 {
598        let by = build.iconst(word, sweep.step);
599        made.push(by);
600        steps = build.binary(Opcode::SDiv, left, by, Flags::NONE);
601        made.push(steps);
602    }
603    let one = build.iconst(word, 1);
604    made.push(one);
605    let allows = build.binary(Opcode::Add, steps, one, Flags::NSW);
606    made.push(allows);
607    let clamped = build.select(short, zero, allows);
608    made.push(clamped);
609    clamped
610}
611
612#[cfg(test)]
613mod tests {
614    use rucc_base::Interner;
615    use rucc_ir::{
616        Block, Builder, Extra, Flags, Func, Inst, InstData, IntPred, MemInfo, MemOrder, Module,
617        Opcode, Restrict, Signature, Type, Value, verify_func,
618    };
619    use rucc_target::{TargetInfo, Triple};
620
621    use super::{SPLIT, Split};
622    use crate::canon::Canon;
623    use crate::stats::Kind;
624    use crate::{Fuel, Pass, Stats};
625
626    /// How many times the loop goes round, and how wide each element of the walk is.
627    const TRIPS: i128 = 16;
628    const WIDTH: i128 = 4;
629
630    /// A counted loop that reads one element each time round and can stop on what it read.
631    ///
632    /// ```text
633    /// entry(a): jump head(0)
634    /// head(i):  p = a + i*4; check_bounds cap_of(p), p; v = load p
635    ///           br v == 0 -> done, more
636    /// more:     next = i + 1; br next < 16 -> head(next), done
637    /// done:     ret
638    /// ```
639    ///
640    /// The second way out is the point. Hoisting refuses this loop, because a loop that can stop in
641    /// the middle reads fewer bytes than its count says and one check in front of it for all of them
642    /// would refuse a program that was right. Splitting does not care, because the count it reads is
643    /// only ever an upper limit on how far to look.
644    fn leaving() -> (Interner, Func, Vec<Block>) {
645        walking(Some(TRIPS))
646    }
647
648    /// The same loop, with how many times it goes round handed in rather than written down.
649    ///
650    /// What this reaches is the other half of [`crate::trip::covered`], the one that builds the
651    /// count out of something the loop does not change. It is worth its own test because that
652    /// arithmetic promises not to wrap for hoisting and promises nothing for this pass, and the two
653    /// callers now ask for different things from the same code.
654    fn counting() -> (Interner, Func, Vec<Block>) {
655        walking(None)
656    }
657
658    /// Builds the loop, with the exit test against a number or against a second parameter.
659    fn walking(times: Option<i128>) -> (Interner, Func, Vec<Block>) {
660        let mut names = Interner::new();
661        let mut params = vec![Type::PTR];
662        params.extend(times.is_none().then_some(Type::int(64)));
663        let mut func = Func::new(names.intern("f"), Signature::new().with_params(&params));
664        let entry = func.create_block();
665        let head = func.create_block();
666        let more = func.create_block();
667        let done = func.create_block();
668        let array = func.append_param(entry, Type::PTR);
669        let handed = times.is_none().then(|| func.append_param(entry, Type::int(64)));
670        let counter = func.append_param(head, Type::int(64));
671
672        let zero = Builder::new(&mut func, entry).iconst(Type::int(64), 0);
673        Builder::new(&mut func, entry).jump(head, &[zero]);
674
675        let mut build = Builder::new(&mut func, head);
676        let by = build.iconst(Type::int(64), WIDTH);
677        let scaled = build.binary(Opcode::Mul, counter, by, Flags::NSW);
678        let args = build.func().push_values(&[array, scaled]);
679        let pointer = build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR);
680        check(&mut build, pointer);
681        let read = build.load(Type::int(32), pointer, mem(), Flags::NONE);
682        let nothing = build.iconst(Type::int(32), 0);
683        let stop = build.icmp(IntPred::Eq, read, nothing);
684        build.br_if(stop, done, &[], more, &[]);
685
686        let mut build = Builder::new(&mut func, more);
687        let one = build.iconst(Type::int(64), 1);
688        let next = build.binary(Opcode::Add, counter, one, Flags::NSW);
689        let limit = match (times, handed) {
690            (Some(times), _) => build.iconst(Type::int(64), times),
691            (None, handed) => handed.expect("a loop with no number for a limit was handed one"),
692        };
693        let again = build.icmp(IntPred::Slt, next, limit);
694        build.br_if(again, head, &[next], done, &[]);
695        Builder::new(&mut func, done).ret(&[]);
696        (names, func, vec![entry, head, more, done])
697    }
698
699    /// What one access in the loop covers.
700    fn mem() -> MemInfo {
701        MemInfo {
702            size: WIDTH as u64,
703            align: WIDTH as u32,
704            order: MemOrder::NotAtomic,
705            tbaa: None,
706            restrict: Restrict::NONE,
707        }
708    }
709
710    /// Puts `cap_of` and a `check_bounds` at `pointer` into a block.
711    ///
712    /// The shape `rucc-safety` emits, written out here rather than reached for, because `rucc-opt`
713    /// is rank 9 alongside `rucc-safety` and cannot depend on it.
714    fn check(build: &mut Builder<'_>, pointer: Value) {
715        let args = build.func().push_values(&[pointer]);
716        let capability = build.value(InstData { args, ..InstData::new(Opcode::CapOf) }, Type::CAP);
717        let args = build.func().push_values(&[capability, pointer]);
718        let extra = Extra::Mem(build.func().add_mem(mem()));
719        build.inst(InstData { args, extra, ..InstData::new(Opcode::CheckBounds) }, &[]);
720    }
721
722    /// Canonicalizes and then splits, with as much fuel as both want.
723    ///
724    /// Both, because the pass is written against the shape [`Canon`] leaves, and it is
725    /// canonicalization that gives the loop the preheader the limit is worked out in.
726    fn split_up(func: &mut Func) -> Stats {
727        let mut an = crate::machine::fixtures::analyses();
728        Canon.run(func, &mut an, &mut Fuel::unlimited());
729        Split.run(func, &mut an, &mut Fuel::unlimited())
730    }
731
732    /// Every instruction in the function with this opcode, and the block it is in.
733    fn all(func: &Func, opcode: Opcode) -> Vec<(Block, Inst)> {
734        func.blocks()
735            .flat_map(|block| func.insts(block).map(move |inst| (block, inst)).collect::<Vec<_>>())
736            .filter(|&(_, inst)| func[inst].opcode == opcode)
737            .collect()
738    }
739
740    /// Insists the function is one the rest of the compiler may believe.
741    ///
742    /// This is what the tests here rest on. The pass makes a second copy of a loop, gives a new
743    /// block parameters that stand for the old header's, and moves a preheader's worth of
744    /// arithmetic in front of a terminator that was already there, so whether every value is in
745    /// scope where it is read is not something reading the code settles.
746    fn sound(func: &Func, names: &mut Interner) {
747        let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
748        let module = Module::new(names.intern("t.c"), &target);
749        if let Err(errors) = verify_func(&module, func, names) {
750            panic!("{errors:#?}");
751        }
752    }
753
754    #[test]
755    fn a_loop_that_can_stop_early_is_split_even_though_hoisting_will_not_touch_it() {
756        // The census row this pass was written for. Of the checks SQLite still carries at -O2, the
757        // largest group by far is in loops with a second way out, which is exactly the loop here.
758        let (mut names, mut func, _) = leaving();
759        let mut an = crate::machine::fixtures::analyses();
760        Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
761        let refused = crate::hoist::Hoist.run(&mut func, &mut an, &mut Fuel::unlimited());
762        assert!(!refused.changed(), "hoisting has nothing to say about this loop");
763
764        let stats = Split.run(&mut func, &mut an, &mut Fuel::unlimited());
765        assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
766        sound(&func, &mut names);
767    }
768
769    #[test]
770    fn the_half_the_loop_runs_first_has_no_check_in_it_and_the_other_one_keeps_it() {
771        // One check went in and one check came out, and the one that came out is in the copy. That
772        // is the whole transformation: the same work, with the checking half reached only once the
773        // guard says the run of safe iterations is over.
774        let (mut names, mut func, blocks) = leaving();
775        let head = blocks[1];
776        split_up(&mut func);
777
778        let left = all(&func, Opcode::CheckBounds);
779        assert_eq!(left.len(), 1, "one check, and it is the one the slow half kept");
780        assert_ne!(left[0].0, head, "and it is not in the block the loop started in");
781        sound(&func, &mut names);
782    }
783
784    #[test]
785    fn how_far_the_runtime_is_asked_to_look_is_settled_in_front_of_the_loop() {
786        // The one thing a compiler cannot work out here is how many bytes belong to the object, so
787        // it is asked, once, before the loop starts. Once is what makes this worth doing: a query
788        // per loop in place of a check per iteration.
789        let (mut names, mut func, _) = leaving();
790        split_up(&mut func);
791
792        let asked = all(&func, Opcode::CapExtent);
793        assert_eq!(asked.len(), 1, "one question for the one check that was sized");
794        let cfg = crate::Cfg::new(&func);
795        let doms = crate::Dominators::new(&cfg);
796        let loops = crate::Loops::new(&cfg, &doms);
797        assert!(
798            loops.all().all(|id| !loops.contains(id, asked[0].0)),
799            "and it is outside the loop"
800        );
801        sound(&func, &mut names);
802    }
803
804    #[test]
805    fn a_loop_with_a_call_in_it_is_left_alone() {
806        // The extent is asked once and believed for the whole of the fast half, so anything that
807        // could hand the storage back in the middle makes the answer stale and the fast half has
808        // nothing left in it to notice.
809        let (mut names, mut func, blocks) = leaving();
810        let more = blocks[2];
811        let term = func.terminator(more).expect("the latch branches");
812        let callee = names.intern("might_free");
813        let signature = func.add_signature(Signature::new());
814        let call = Builder::new(&mut func, more).call(callee, signature, &[]);
815        func.remove_inst(call);
816        func.insert_before(call, term);
817
818        let stats = split_up(&mut func);
819        assert!(!stats.changed());
820        assert_eq!(stats.count(Kind::Missed, super::A_CALL_INSIDE), 1);
821    }
822
823    #[test]
824    fn a_check_whose_address_does_not_move_is_left_to_hoisting() {
825        // One check on the array itself, every time round. Nothing about it gets better from being
826        // in the fast half, because it is the same bytes every iteration, which is a check to lift
827        // out of the loop rather than a loop to divide in two.
828        let (_, mut func, blocks) = leaving();
829        let (entry, head) = (blocks[0], blocks[1]);
830        let array = func[entry].params[0];
831        let term = func.terminator(head).expect("the header branches");
832        let mut build = Builder::new(&mut func, head);
833        check(&mut build, array);
834        let made: Vec<Inst> = func.insts(head).skip_while(|&inst| inst != term).skip(1).collect();
835        for inst in made {
836            func.remove_inst(inst);
837            func.insert_before(inst, term);
838        }
839
840        let stats = split_up(&mut func);
841        assert_eq!(stats.count(Kind::Missed, super::DOES_NOT_MOVE), 1);
842        assert_eq!(
843            all(&func, Opcode::CheckBounds).len(),
844            3,
845            "the walking check left the fast half, and the still one stayed in both"
846        );
847    }
848
849    #[test]
850    fn a_loop_whose_count_is_an_expression_is_split_on_what_that_expression_says() {
851        // How far to look is worked out in the preheader rather than written down, out of a value
852        // the loop does not change. Nothing here promises the arithmetic stays inside sixty four
853        // bits, and it does not have to: a limit that wrapped is still answered with a true count
854        // of the bytes that belong to the object.
855        let (mut names, mut func, _) = counting();
856        let stats = split_up(&mut func);
857        assert_eq!(stats.count(Kind::Optimized, SPLIT), 1);
858        assert_eq!(all(&func, Opcode::CapExtent).len(), 1);
859        assert_eq!(all(&func, Opcode::CheckBounds).len(), 1, "the fast half lost its check");
860        sound(&func, &mut names);
861    }
862
863    #[test]
864    fn the_pass_stops_when_the_fuel_runs_out() {
865        // What `-fopt-fuel` is for, and the reason every transformation here goes through the
866        // counter rather than round it.
867        let (_, mut func, _) = leaving();
868        let mut an = crate::machine::fixtures::analyses();
869        Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
870        let stats = Split.run(&mut func, &mut an, &mut Fuel::of(0));
871        assert!(!stats.changed());
872        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
873    }
874}