rucc_opt/loop_delete.rs
1//! Takes out a loop that comes back, working out what it left behind.
2//!
3//! Design: `spec/optimizer/17-dce.md` for what makes a thing removable and section 28.9 for the
4//! closed form, the off by one in it, and why the two questions this pass asks want different
5//! things from the trip count. This is tamnd/rucc#1631.
6//!
7//! [`crate::dce`] cannot do this and the reason is worth stating, because it looks at first like a
8//! gap in that pass. An empty counted loop has a counter, an add, a compare and a branch, and
9//! every one of them is used: the add feeds the compare, the compare feeds the branch, and the
10//! branch feeds the block parameter the add reads. Nothing in it has a use count of zero, so a
11//! pass driven by use counts correctly leaves all of it alone. The question that gets the loop out
12//! is not asked of an instruction, it is asked of the loop: does anything outside read what it
13//! computes, does it do anything to memory, and does it come back. Three yeses and the loop is a
14//! way of spending time.
15//!
16//! # Which loops
17//!
18//! A preheader, one exit, and a bound rather than an estimate. The bound is what says the loop
19//! terminates, which is the third question and the one a person is most likely to forget: a loop
20//! that computes nothing and never comes back still cannot be taken out, because not coming back
21//! is what it does. Section 7.5's distinction between a bound and an estimate is exactly this: an
22//! estimate decides whether a transformation pays and a bound decides what the program does.
23//!
24//! The bound is read through [`crate::scev::Bound::comes_back`] rather than through the accessor
25//! [`crate::unroll`] uses, and the difference is worth a sentence. Unrolling multiplies by the
26//! count, so it needs the count to be the right number. Deleting only needs there to be a last
27//! iteration, and `for (i = 0; i < n; i++)` has one whatever `n` turns out to be, so a count
28//! worked out from a value the loop does not change is as good as a number here. It stops being
29//! as good the moment anything reads what the loop left behind, which is why the two questions
30//! are asked in that order and with different accessors.
31//!
32//! Every instruction inside has to be one whose not happening nothing can tell. That is the
33//! predicate [`crate::dce`] already has, so it is read from there rather than written again, and
34//! it means a plain load may be inside the loop and a `volatile` one may not. A call is allowed
35//! when the purity analysis says it reads memory at most and comes back, which is the same rule
36//! that lets a call whose result nothing reads go.
37//!
38//! # What the loop leaves behind
39//!
40//! A loop whose total somebody reads afterwards is a loop that hands something over, and most
41//! loops worth writing are that kind. Sometimes what it hands over can be worked out without
42//! running it. A value that goes up by the same amount every time round is `{base, +, step}` in
43//! [`crate::scev`]'s terms, the loop is left on the iteration the exit test first fails, and that
44//! iteration's number is the count, so what the loop leaves behind is `base + step * count`. The
45//! preheader works that out in one go and hands it over instead, and then nothing outside reads
46//! anything the loop computed and the loop goes.
47//!
48//! Handing it over is two different edits, because a value defined in the loop reaches the code
49//! after it by two different roads. It may be an argument on the edge out, landing in a parameter
50//! of the block the loop leaves to, which is the shape [`crate::canon`] puts things in. Or the
51//! block after the loop may simply name it, which is legal wherever the definition dominates the
52//! use and is what is actually there by the time this runs, since the block loop closed form put in
53//! the way is one [`crate::simplify_cfg`] has every reason to fold away again. So both are looked
54//! for, and a use of the second kind is rewritten where it stands.
55//!
56//! No overflow argument is needed for this and it is worth saying why, because the neighbouring
57//! transformation in section 28.4 does need one. A value that steps by a fixed amount evolves in
58//! its own type, which is to say modulo two to the width, and addition modulo two to the width is
59//! associative, so adding `step` to `base` `count` times and working out `base + step * count` the
60//! same way are the same number whatever either of them does to the top bit. Section 28.4's rewrite
61//! is a different claim, that one comparison holds exactly where another does, and that one does
62//! turn on whether the limit overflows. So the arithmetic written here carries neither `nsw` nor
63//! `nuw`, and the promise the loop's own increment carried is not copied onto it, because that
64//! promise is about the sequence and says nothing about this.
65//!
66//! What is written down is the whole expression rather than three instructions to be folded later,
67//! because this pass is the last one in the pipeline and there is no later. `base` and `step` are
68//! both [`crate::scev::Invariant`], which is `value * scale + offset` with the arithmetic on it
69//! already, so `base + step * count` is worked out in that form first and only what is left of it
70//! reaches the function. A loop adding one a million times leaves a constant behind and a loop
71//! adding an invariant `n` a million times leaves one multiply.
72//!
73//! A count that is an expression rather than a number is written as `base + step * max(count, 0)`,
74//! and the two things bolted onto it there are two assumptions paid for rather than believed. The
75//! clamp is [`crate::scev::Assumption::Entered`]. A count that comes out negative is a loop whose
76//! test failed the first time it ran, which is a loop that took its back edge no times and handed
77//! over what one pass through its body left, and zero is the count that says exactly that. The
78//! widening is the reading the exit test took, a sign extension for a signed test and a zero
79//! extension for an unsigned one. Section 7.7 is the warning about getting that one wrong: a limit
80//! past the middle of a thirty two bit type is a large number to an unsigned test and a negative
81//! one to a signed test, so sign extending what an unsigned test compared would clamp to zero and
82//! turn a loop over three billion elements into one that ran no times.
83//!
84//! The clamp is done in sixty four bits and the arithmetic in the value's own type, and the cut
85//! between the two is exact rather than close enough. Multiplying modulo two to the width and then
86//! cutting to a narrower width is the same number as cutting first and then multiplying, so a count
87//! worked out wide and truncated is the count. Widening it instead is a zero extension, because the
88//! clamp has already made it a number that is not negative.
89//!
90//! It is only done when it lets the loop go, which is a cost rule rather than a correctness one.
91//! Writing the final value down where the loop stays behind costs a multiply in the preheader and
92//! saves nothing, because the loop still carries the value round its own back edge and nothing in
93//! rucc yet takes out a block parameter whose only reader is the argument it passes to itself.
94//! [`crate::dce`]'s own notes call that out as a transformation worth having and a different one
95//! from what it does. When there is one, this gate is the thing to reconsider.
96//!
97//! # What it does
98//!
99//! Works out in the preheader whatever the loop was going to leave behind, puts those values where
100//! the loop's own were read, points the preheader at the block the loop left to with whatever the
101//! edge out was already carrying from outside, and lets the sweep in [`crate::simplify_cfg`] take
102//! the blocks nothing reaches. Every value named in any of it is asserted to dominate the preheader
103//! rather than assumed to: a value defined outside the loop that reaches the exit test has to
104//! dominate the preheader, and an assertion is cheaper than being wrong about why.
105
106use std::collections::{HashMap, HashSet};
107
108use rucc_ir::{Block, Builder, Extra, Func, Inst, InstData, IntPred, Opcode, Type, Value};
109
110use crate::cfg::Cfg;
111use crate::dom::Dominators;
112use crate::loops::{LoopId, Loops};
113use crate::purity::Facts;
114use crate::scev::{Count, Invariant, Plain, Reading, Scev};
115use crate::{Analyses, Fuel, Pass, Preserved, Stats};
116
117const DELETED: &str = "loop taken out, it comes back and leaves nothing behind";
118const WRITTEN: &str = "what the loop was going to leave behind worked out in front of it instead";
119const NO_COUNT: &str = "loop left as it was, nothing here says it comes back";
120const SHAPE: &str =
121 "loop left as it was, it has no preheader or it leaves from more than one place";
122const EFFECTS: &str = "loop left as it was, something in it does more than work out a value";
123const NO_FORM: &str =
124 "loop left as it was, what it leaves behind is not a thing this can work out in front of it";
125const ENTRIES: &str = "loop left as it was, it is reached somewhere other than at its header";
126const NO_FUEL: &str = "loop left as it was, the pass ran out of fuel";
127
128/// Section 17's dead code elimination, asked about a loop rather than about an instruction.
129#[derive(Debug)]
130pub struct LoopDelete;
131
132impl Pass for LoopDelete {
133 fn name(&self) -> &'static str {
134 "loop-delete"
135 }
136
137 fn describe(&self) -> &'static str {
138 "a loop that comes back and leaves nothing behind is taken out"
139 }
140
141 fn preserves(&self) -> Preserved {
142 // The loop goes, and its blocks with it.
143 Preserved::NONE
144 }
145
146 fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
147 let mut stats = Stats::new();
148 if func.entry().is_none() {
149 return stats;
150 }
151 let mut done: HashSet<Block> = HashSet::new();
152 let mut say = true;
153 while let Some(job) = plan(func, an, &done, &mut stats, say) {
154 say = false;
155 if !fuel.take() {
156 stats.missed(NO_FUEL);
157 break;
158 }
159 done.insert(job.header);
160 for _ in 0..apply(func, &job) {
161 stats.optimized(WRITTEN);
162 }
163 stats.optimized(DELETED);
164 an.clear();
165 crate::simplify_cfg::sweep(func, an, &mut stats);
166 }
167 an.clear();
168 stats
169 }
170}
171
172/// One loop to take out, worked out against the function as it stands.
173#[derive(Debug)]
174struct Job {
175 /// The block the loop is entered at, which is what says which loop this was.
176 header: Block,
177 /// The one block outside the loop with an edge to the header.
178 preheader: Block,
179 /// The block outside the loop the one exit edge arrives at.
180 exit: Block,
181 /// The blocks the loop is made of, which is what says which uses are the ones outside it.
182 inside: HashSet<Block>,
183 /// What the edge out was going to carry, which the preheader carries instead.
184 args: Vec<Value>,
185 /// Every value the loop defines that anything outside it reads, and what it ends up holding.
186 ends: Vec<(Value, Leaves)>,
187}
188
189/// What a value the loop defines holds by the time anything outside it looks.
190///
191/// Two shapes for the two shapes a count comes in, and what separates them is how much of the
192/// answer is settled before anything reaches the function. A count that is a number settles all of
193/// it, so what is left is one expression and often one constant. A count that is an expression
194/// settles none of it and the preheader does the work.
195#[derive(Clone, Copy, Debug)]
196enum Leaves {
197 /// `base + step * count`, worked out in full because the count is a number.
198 Worked {
199 /// The type it evolved in, which is the type the arithmetic is done in.
200 ty: Type,
201 /// The whole of it, as far as it goes without writing anything down.
202 end: Invariant,
203 },
204 /// `base + step * max(count, 0)`, built in the preheader because the count is an expression.
205 Built {
206 /// The type it evolved in, which is the type the arithmetic is done in.
207 ty: Type,
208 /// What the value holds the first time anything outside could have looked.
209 base: Invariant,
210 /// How much it goes up by each time round.
211 step: Invariant,
212 /// How many times the back edge is taken, before the clamp the module notes describe.
213 count: Plain,
214 /// How the exit test read what the count is built on, which is the widening owed.
215 reading: Reading,
216 },
217}
218
219/// The innermost loop that can go, and what it would take.
220///
221/// One at a time, for the reason [`crate::unroll::plan`] takes one at a time: taking a loop out
222/// invalidates the forest the next answer would be read out of. `say` is false after the first
223/// round so that a loop this declines is declined once rather than once per round.
224fn plan(
225 func: &Func,
226 an: &mut Analyses,
227 done: &HashSet<Block>,
228 stats: &mut Stats,
229 say: bool,
230) -> Option<Job> {
231 let facts = an.purity();
232 let cfg = an.cfg(func);
233 let doms = an.dominators(func);
234 let loops = an.loops(func);
235 let mut scev = Scev::new(func, cfg, loops);
236 let mut found: Option<(u32, Job)> = None;
237 for id in loops.all() {
238 if done.contains(&loops.header(id)) {
239 continue;
240 }
241 match consider(func, cfg, doms, loops, facts, &mut scev, id) {
242 Ok(job) => {
243 let depth = loops.depth(id);
244 if found.as_ref().is_none_or(|(had, _)| depth > *had) {
245 found = Some((depth, job));
246 }
247 }
248 Err(why) if say => stats.missed(why),
249 Err(_) => (),
250 }
251 }
252 found.map(|(_, job)| job)
253}
254
255/// Whether this loop can go, and why not when it cannot.
256fn consider(
257 func: &Func,
258 cfg: &Cfg,
259 doms: &Dominators,
260 loops: &Loops,
261 facts: &Facts,
262 scev: &mut Scev<'_>,
263 id: LoopId,
264) -> Result<Job, &'static str> {
265 let header = loops.header(id);
266 let preheader = loops.preheader(cfg, id).ok_or(SHAPE)?;
267 let [only] = loops.exits(id) else {
268 return Err(SHAPE);
269 };
270
271 let blocks = loops.blocks(id).to_vec();
272 let inside: HashSet<Block> = blocks.iter().copied().collect();
273 for &block in &blocks {
274 // The same reducibility check unrolling makes. A block of the loop reached from outside
275 // the loop is a region this has no right to reason about as one piece.
276 if block != header && cfg.predecessors(block).iter().any(|at| !inside.contains(at)) {
277 return Err(ENTRIES);
278 }
279 for inst in func.insts(block) {
280 if func.is_terminator(inst) {
281 // A terminator that leaves the function or goes somewhere worked out at run time
282 // is not an edge the forest accounted for, so the one exit counted above is not
283 // the only way out.
284 if !matches!(func[inst].opcode, Opcode::Jump | Opcode::BrIf) {
285 return Err(EFFECTS);
286 }
287 continue;
288 }
289 if !crate::dce::removable(func, inst, facts) {
290 return Err(EFFECTS);
291 }
292 }
293 }
294 // The bound is what says the loop comes back. A loop that computes nothing and runs forever
295 // still does something, which is run forever. A count worked out from a value the loop does
296 // not change says that as well as a number does: whatever that value is, the loop gets to it,
297 // and nothing in here needs to know how many steps that took. What is not allowed is a loop
298 // ending on `!=` whose counter may step past its limit, and that is the one assumption
299 // [`crate::scev::Bound::comes_back`] holds back.
300 let bound = scev.bound(id).ok_or(NO_COUNT)?;
301 // Taken here rather than inside [`ending`] because it belongs to the exit test rather than to
302 // any one value the loop hands over, so every one of them owes the same widening.
303 let reading = bound.reading();
304 let count = bound.comes_back().ok_or(NO_COUNT)?;
305
306 let term = func.terminator(only.from).ok_or(SHAPE)?;
307 let leaving = func.successors(term).find(|call| call.block == only.to).ok_or(SHAPE)?;
308 let args = func[leaving.args].to_vec();
309
310 let mut wanted = read_outside(func, &blocks, &inside);
311 for &arg in &args {
312 if loops.is_invariant(func, id, arg) {
313 debug_assert!(
314 doms.dominates(defined_in(func, arg), preheader),
315 "a value outside the loop that reaches the exit test dominates the preheader"
316 );
317 continue;
318 }
319 if !wanted.contains(&arg) {
320 wanted.push(arg);
321 }
322 }
323
324 let mut ends = Vec::with_capacity(wanted.len());
325 for value in wanted {
326 let end = ending(func, scev, id, value, count, reading).ok_or(NO_FORM)?;
327 debug_assert!(
328 names(end).iter().all(|&on| doms.dominates(defined_in(func, on), preheader)),
329 "a value the loop does not change is defined outside it and so dominates the preheader"
330 );
331 ends.push((value, end));
332 }
333 Ok(Job { header, preheader, exit: only.to, inside, args, ends })
334}
335
336/// Every value the loop defines that a block outside it names, in the order they turn up.
337///
338/// [`crate::unroll::escapes`] asks whether there is one of these and stops there, because a loop
339/// with one is a loop it will not copy. Here they are the work rather than the reason to stop, so
340/// the answer has to be which ones.
341fn read_outside(func: &Func, blocks: &[Block], inside: &HashSet<Block>) -> Vec<Value> {
342 let mut defined: HashSet<Value> = HashSet::new();
343 for &block in blocks {
344 defined.extend(func[block].params.iter().copied());
345 for inst in func.insts(block) {
346 defined.extend(func[inst].results());
347 }
348 }
349 let mut found = Vec::new();
350 for block in func.blocks() {
351 if inside.contains(&block) {
352 continue;
353 }
354 for inst in func.insts(block) {
355 let reads = func[func[inst].args].iter().copied();
356 let passes = func.successors(inst).flat_map(|call| func[call.args].to_vec());
357 for value in reads.chain(passes) {
358 if defined.contains(&value) && !found.contains(&value) {
359 found.push(value);
360 }
361 }
362 }
363 }
364 found
365}
366
367/// What the loop leaves in a value it hands over, or `None` when that is not a thing to write down.
368///
369/// Every refusal here is asked before anything is written, so that a refusal is a refusal rather
370/// than a preheader with half an expression in it. There is no undo and there should not need to
371/// be.
372fn ending(
373 func: &Func,
374 scev: &mut Scev<'_>,
375 id: LoopId,
376 value: Value,
377 count: Count,
378 reading: Reading,
379) -> Option<Leaves> {
380 let chrec = scev.evolution(id, value).chrec()?;
381 // The arithmetic below is integer arithmetic in one lane. A chrec over anything else is not a
382 // thing this knows how to write down, whatever the count turned out to be.
383 if !chrec.ty.is_int() || chrec.ty.is_vector() {
384 return None;
385 }
386 match count {
387 Count::Exact(trips) => {
388 let trips = i128::try_from(trips).ok()?;
389 let all = chrec.step.times(Invariant::number(trips))?;
390 let end = chrec.base.plus(all)?;
391 writable(func, end, chrec.ty)?;
392 Some(Leaves::Worked { ty: chrec.ty, end })
393 }
394 Count::Symbolic(count) => {
395 writable(func, chrec.base, chrec.ty)?;
396 writable(func, chrec.step, chrec.ty)?;
397 let count = count.plain()?;
398 // A count built on a value that is itself read through an extension carries a widening
399 // of its own, and which of that one and the exit test's should be spent is not a
400 // question with an answer here. Refused rather than guessed at, the same way
401 // [`crate::trip::counted`] refuses it.
402 if count.read.is_some() {
403 return None;
404 }
405 // Room for the clamp to happen in. A count built on something already as wide as the
406 // arithmetic that carries it has nowhere to be negative.
407 if count.value.filter(|_| count.scale != 0).is_none_or(|on| func[on].ty.bits() > 64) {
408 return None;
409 }
410 Some(Leaves::Built { ty: chrec.ty, base: chrec.base, step: chrec.step, count, reading })
411 }
412 }
413}
414
415/// Whether [`write`] can put an expression in front of the loop in the type given.
416fn writable(func: &Func, part: Invariant, ty: Type) -> Option<Plain> {
417 let plain = part.plain()?;
418 if plain.read.is_some() {
419 return None;
420 }
421 if plain.value.is_some_and(|named| func[named].ty != ty) {
422 return None;
423 }
424 Some(plain)
425}
426
427/// Every value an expression is built on, which is what the dominance assertion is asked of.
428fn names(leaves: Leaves) -> Vec<Value> {
429 let on =
430 |part: Invariant| part.plain().and_then(|plain| plain.value.filter(|_| plain.scale != 0));
431 match leaves {
432 Leaves::Worked { end, .. } => on(end).into_iter().collect(),
433 Leaves::Built { base, step, count, .. } => {
434 [on(base), on(step), count.value].into_iter().flatten().collect()
435 }
436 }
437}
438
439/// The block a value is defined in.
440fn defined_in(func: &Func, value: Value) -> Block {
441 match func[value].def {
442 rucc_ir::Def::Result { inst, .. } => {
443 func.block_of(inst).expect("a value in use is defined in a block")
444 }
445 rucc_ir::Def::Param { block, .. } => block,
446 }
447}
448
449/// Points the preheader past the loop, working out on the way what the loop was going to leave.
450///
451/// Answers how many of those there were, which is what the report counts.
452fn apply(func: &mut Func, job: &Job) -> usize {
453 let term = func.terminator(job.preheader).expect("a preheader ends in a jump to the header");
454 let mut instead: HashMap<Value, Value> = HashMap::new();
455 // One clamp for the whole loop rather than one per value, since the count belongs to the loop
456 // and the values differ only in what they do with it.
457 let mut times: Option<Value> = None;
458 for &(value, leaves) in &job.ends {
459 let worked = match leaves {
460 Leaves::Worked { ty, end } => write(func, term, ty, end),
461 Leaves::Built { ty, base, step, count, reading } => {
462 let all = match times {
463 Some(had) => had,
464 None => *times.insert(clamped(func, term, count, reading)),
465 };
466 built(func, term, ty, base, step, all)
467 }
468 };
469 instead.insert(value, worked);
470 }
471 swap_in(func, job, &instead);
472 let args: Vec<Value> =
473 job.args.iter().map(|arg| instead.get(arg).copied().unwrap_or(*arg)).collect();
474 func.remove_inst(term);
475 Builder::new(func, job.preheader).jump(job.exit, &args);
476 instead.len()
477}
478
479/// Puts the worked out values where the loop's own were read.
480///
481/// Only outside the loop, because inside it the loop's own values are still the right answer right
482/// up until the blocks go. The preheader is outside and gets walked with the rest, which is
483/// harmless and better than a special case: what was just written into it names nothing the loop
484/// defines.
485fn swap_in(func: &mut Func, job: &Job, instead: &HashMap<Value, Value>) {
486 if instead.is_empty() {
487 return;
488 }
489 let outside: Vec<Block> = func.blocks().filter(|at| !job.inside.contains(at)).collect();
490 for block in outside {
491 for inst in func.insts(block).collect::<Vec<_>>() {
492 let mut lists = vec![func[inst].args];
493 lists.extend(func.successors(inst).map(|call| call.args));
494 for list in lists {
495 func.rewrite(list, |value| instead.get(&value).copied().unwrap_or(value));
496 }
497 }
498 }
499}
500
501/// Works an expression out in front of an instruction.
502///
503/// `value * scale + offset`, with the parts that are nothing left out, so a scale of one is no
504/// multiply and an offset of zero is no add and an expression built on no value at all is one
505/// constant. That is what makes a loop adding one a million times leave a number behind rather than
506/// three instructions nothing is going to fold, this being the last pass there is.
507fn write(func: &mut Func, before: Inst, ty: Type, end: Invariant) -> Value {
508 let plain = end.plain().expect("consider refused anything this cannot write");
509 let Some(value) = plain.value.filter(|_| plain.scale != 0) else {
510 return crate::ivopts::number(func, before, ty, plain.offset);
511 };
512 let mut so_far = value;
513 if plain.scale != 1 {
514 let by = crate::ivopts::number(func, before, ty, plain.scale);
515 so_far = arith(func, before, Opcode::Mul, so_far, by, ty);
516 }
517 if plain.offset != 0 {
518 let by = crate::ivopts::number(func, before, ty, plain.offset);
519 so_far = arith(func, before, Opcode::Add, so_far, by, ty);
520 }
521 so_far
522}
523
524/// How many times the back edge is taken, worked out in front of the loop and clamped at zero.
525///
526/// `max(read(value) * scale + offset, 0)`, in sixty four bits whatever the count's own type is, and
527/// the module notes say what each of those two is paying for. The clamp is a `select` rather than a
528/// branch because the whole of this has to be straight line code in a preheader, and nothing on any
529/// of it promises anything about overflow, since the count came out of a subtraction the analysis
530/// already reasoned about rather than out of anything written here.
531///
532/// [`crate::ivopts`] writes the same clamp in front of the same kind of loop, because it is
533/// discharging the same assumption in the same place, and two of these would be two things to keep
534/// in step. It lives here because this is where it was written and where the argument for it is.
535pub(crate) fn clamped(func: &mut Func, before: Inst, count: Plain, reading: Reading) -> Value {
536 let word = Type::int(64);
537 let on = count.value.expect("a count that is an expression is built on a value");
538 let mut wide = on;
539 if func[on].ty.bits() < 64 {
540 let widen = match reading {
541 Reading::Signed => Opcode::SExt,
542 Reading::Unsigned => Opcode::ZExt,
543 };
544 wide = cast(func, before, widen, wide, word);
545 }
546 if count.scale != 1 {
547 let by = crate::ivopts::number(func, before, word, count.scale);
548 wide = arith(func, before, Opcode::Mul, wide, by, word);
549 }
550 if count.offset != 0 {
551 let by = crate::ivopts::number(func, before, word, count.offset);
552 wide = arith(func, before, Opcode::Add, wide, by, word);
553 }
554 let none = crate::ivopts::number(func, before, word, 0);
555 let args = func.push_values(&[wide, none]);
556 let test =
557 InstData { args, extra: Extra::IntPred(IntPred::Sgt), ..InstData::new(Opcode::ICmp) };
558 let entered = made(func, before, test, word.with_lane(Type::I1));
559 let args = func.push_values(&[entered, wide, none]);
560 made(func, before, InstData { args, ..InstData::new(Opcode::Select) }, word)
561}
562
563/// `base + step * times`, worked out in front of the loop in the type the value evolved in.
564///
565/// The trivial parts are left out where the numbers make them trivial, for the reason [`write`]
566/// leaves them out: nothing after this pass folds a multiply by one, so a loop counting by ones
567/// would otherwise leave one in every preheader.
568fn built(
569 func: &mut Func,
570 before: Inst,
571 ty: Type,
572 base: Invariant,
573 step: Invariant,
574 times: Value,
575) -> Value {
576 if step.as_number() == Some(0) {
577 return write(func, before, ty, base);
578 }
579 let narrow = resize(func, before, times, ty);
580 let mut so_far = narrow;
581 if step.as_number() != Some(1) {
582 let by = write(func, before, ty, step);
583 so_far = arith(func, before, Opcode::Mul, by, narrow, ty);
584 }
585 if base.as_number() != Some(0) {
586 let from = write(func, before, ty, base);
587 so_far = arith(func, before, Opcode::Add, from, so_far, ty);
588 }
589 so_far
590}
591
592/// The clamped count in the type the arithmetic is done in.
593///
594/// A truncation where that type is narrower, which loses nothing that matters: cutting a product
595/// modulo two to the width and multiplying a cut are the same number. A zero extension where it is
596/// wider, which is exact because the clamp has already made the count a number that is not
597/// negative. Neither where the widths agree.
598fn resize(func: &mut Func, before: Inst, times: Value, ty: Type) -> Value {
599 let had = func[times].ty.bits();
600 if had == ty.bits() {
601 return times;
602 }
603 let either = if ty.bits() < had { Opcode::Trunc } else { Opcode::ZExt };
604 cast(func, before, either, times, ty)
605}
606
607/// One widening or narrowing, worked out in front of another instruction.
608fn cast(func: &mut Func, before: Inst, opcode: Opcode, arg: Value, ty: Type) -> Value {
609 let args = func.push_values(&[arg]);
610 made(func, before, InstData { args, ..InstData::new(opcode) }, ty)
611}
612
613/// One arithmetic instruction, worked out in front of another one and promising nothing.
614///
615/// Neither `nsw` nor `nuw`, which is the point rather than an omission. The module notes say why:
616/// what the loop did is the same arithmetic modulo two to the width as many times as it ran, and
617/// `base + step * count` worked out the same way is the same number. A flag the loop's own
618/// increment carried is a fact about that sequence, and putting it here would be inventing one.
619pub(crate) fn arith(
620 func: &mut Func,
621 before: Inst,
622 opcode: Opcode,
623 left: Value,
624 right: Value,
625 ty: Type,
626) -> Value {
627 let args = func.push_values(&[left, right]);
628 made(func, before, InstData { args, ..InstData::new(opcode) }, ty)
629}
630
631/// One instruction with one result, put in front of another one and given its source location.
632fn made(func: &mut Func, before: Inst, data: InstData, ty: Type) -> Value {
633 let span = func.span(before);
634 let inst = func.create_inst(data, &[ty], span);
635 func.insert_before(inst, before);
636 func[inst].first_result.expect("one result was asked for")
637}
638
639#[cfg(test)]
640mod tests {
641 use rucc_base::Interner;
642 use rucc_ir::{
643 Block, Builder, Def, Flags, Func, IntPred, MemInfo, MemOrder, Module, Opcode, Restrict,
644 Signature, Type, Value, verify_func,
645 };
646 use rucc_target::{TargetInfo, Triple};
647
648 use super::{DELETED, EFFECTS, LoopDelete, NO_COUNT, NO_FORM, NO_FUEL, WRITTEN};
649 use crate::stats::Kind;
650 use crate::{Fuel, Pass, Stats};
651
652 /// Runs the pass over the function as it stands.
653 fn delete(func: &mut Func, fuel: &mut Fuel) -> Stats {
654 LoopDelete.run(func, &mut crate::machine::fixtures::analyses(), fuel)
655 }
656
657 /// Insists the function is one the rest of the compiler may believe.
658 ///
659 /// Pointing a block at a different successor is the edit that hands a block the wrong number
660 /// of arguments and strands a definition its uses still name, so this is where most of the
661 /// strength of these tests is.
662 fn sound(func: &Func, names: &mut Interner) {
663 let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
664 let module = Module::new(names.intern("t.c"), &target);
665 if let Err(errors) = verify_func(&module, func, names) {
666 panic!("{errors:#?}");
667 }
668 }
669
670 /// How many instructions of that opcode the whole function holds.
671 fn tally(func: &Func, opcode: Opcode) -> usize {
672 func.blocks()
673 .flat_map(|block| func.insts(block))
674 .filter(|&inst| func[inst].opcode == opcode)
675 .count()
676 }
677
678 /// The one value now handed to the block the loop used to leave to.
679 fn handed_value(func: &Func, done: &Block) -> Option<Value> {
680 let cfg = crate::cfg::Cfg::new(func);
681 let [only] = cfg.predecessors(*done) else {
682 return None;
683 };
684 let term = func.terminator(*only)?;
685 let call = func.successors(term).find(|call| call.block == *done)?;
686 let args = func[call.args].to_vec();
687 let [arg] = args[..] else {
688 return None;
689 };
690 Some(arg)
691 }
692
693 /// What the value handed over is a multiple of, when it is a multiple of something.
694 fn handed(func: &Func, done: &Block) -> Option<i128> {
695 let value = handed_value(func, done)?;
696 let Def::Result { inst, .. } = func[value].def else {
697 return None;
698 };
699 if func[inst].opcode != Opcode::Mul {
700 return None;
701 }
702 let args = func[func[inst].args].to_vec();
703 let (imm, ty) = crate::fold::constant(func, args[1])?;
704 Some(imm.signed(ty))
705 }
706
707 /// How many loops are left.
708 fn loops(func: &Func) -> usize {
709 let cfg = crate::cfg::Cfg::new(func);
710 let doms = crate::dom::Dominators::new(&cfg);
711 crate::loops::Loops::new(&cfg, &doms).count()
712 }
713
714 /// A four byte write with nothing said about what it aliases.
715 fn plain() -> MemInfo {
716 MemInfo {
717 size: 4,
718 align: 4,
719 order: MemOrder::NotAtomic,
720 tbaa: None,
721 owns: 0,
722 restrict: Restrict::NONE,
723 }
724 }
725
726 /// What the limit of the exit test is.
727 #[derive(Clone, Copy)]
728 enum Limit {
729 /// A number written in the program.
730 Number(i128),
731 /// A value the function was handed, which the loop does not change.
732 Given,
733 /// The same value, waited for with `!=` rather than counted up to with an ordering.
734 Landing,
735 }
736
737 /// What the loop does, which is the whole of what decides whether it can go.
738 #[derive(Clone, Copy, PartialEq)]
739 enum What {
740 /// Adds up a number nothing ever reads.
741 Nothing,
742 /// Writes each running total to the pointer it was handed.
743 Writes,
744 /// Hands the block it leaves to a total that went up by the same amount every time.
745 HandsOut,
746 /// Hands out a total that went up by one every time, so there is no multiply to write.
747 HandsOne,
748 /// Hands out a total that went up by a different amount every time round.
749 HandsSquare,
750 /// Leaves its total to be read after the loop by a road other than the edge out.
751 ReadAfter,
752 }
753
754 impl What {
755 /// Whether the total goes out on the edge the loop leaves by.
756 fn hands_out(self) -> bool {
757 matches!(self, What::HandsOut | What::HandsOne | What::HandsSquare)
758 }
759 }
760
761 struct Shape {
762 names: Interner,
763 func: Func,
764 entry: Block,
765 done: Block,
766 }
767
768 /// A counted loop in the shape `crate::canon` and `crate::header_copy` leave a `for` in.
769 ///
770 /// ```text
771 /// entry(p, n): jump head(0, 0)
772 /// head(i, sum): jump body(i, sum)
773 /// body(c, r): total = r + n; next = c + 1; test = next < limit
774 /// br test -> head(next, total), done()
775 /// done: ret
776 /// ```
777 ///
778 /// Two blocks in the loop rather than one, so that taking it out has more than one block to get
779 /// rid of, and a running total carried round, so that there is something inside worth asking
780 /// whether anybody reads. The total goes up by `n` each time round, which is the shape of
781 /// `total += seed` in the issue: a value that goes up by the same amount every time, where the
782 /// amount is not a number anything here knows.
783 fn shaped(limit: Limit, what: What) -> Shape {
784 let mut names = Interner::new();
785 let signature = Signature::new().with_params(&[Type::PTR, Type::int(32)]);
786 let mut func = Func::new(names.intern("f"), signature);
787 let entry = func.create_block();
788 let head = func.create_block();
789 let body = func.create_block();
790 let done = func.create_block();
791 let place = func.append_param(entry, Type::PTR);
792 let given = func.append_param(entry, Type::int(32));
793 let i = func.append_param(head, Type::int(32));
794 let sum = func.append_param(head, Type::int(32));
795 let carried = func.append_param(body, Type::int(32));
796 let running = func.append_param(body, Type::int(32));
797 if what.hands_out() {
798 func.append_param(done, Type::int(32));
799 }
800
801 let mut build = Builder::new(&mut func, entry);
802 let zero = build.iconst(Type::int(32), 0);
803 build.jump(head, &[zero, zero]);
804 Builder::new(&mut func, head).jump(body, &[i, sum]);
805
806 let mut build = Builder::new(&mut func, body);
807 let one = build.iconst(Type::int(32), 1);
808 let by = match what {
809 What::HandsOne => one,
810 What::HandsSquare => carried,
811 _ => given,
812 };
813 let total = build.binary(Opcode::Add, running, by, Flags::NSW);
814 if what == What::Writes {
815 build.store(total, place, plain(), Flags::NONE);
816 }
817 let next = build.binary(Opcode::Add, carried, one, Flags::NSW);
818 let stop = match limit {
819 Limit::Number(n) => build.iconst(Type::int(32), n),
820 Limit::Given | Limit::Landing => given,
821 };
822 let pred = match limit {
823 Limit::Landing => IntPred::Ne,
824 _ => IntPred::Slt,
825 };
826 let test = build.icmp(pred, next, stop);
827 let out: Vec<Value> = if what.hands_out() { vec![total] } else { Vec::new() };
828 build.br_if(test, head, &[next, total], done, &out);
829 let mut build = Builder::new(&mut func, done);
830 if what == What::ReadAfter {
831 build.store(total, place, plain(), Flags::NONE);
832 }
833 build.ret(&[]);
834 Shape { names, func, entry, done }
835 }
836
837 #[test]
838 fn a_loop_that_leaves_nothing_behind_is_taken_out() {
839 let mut it = shaped(Limit::Number(1000), What::Nothing);
840 let stats = delete(&mut it.func, &mut Fuel::unlimited());
841 assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
842 assert_eq!(loops(&it.func), 0);
843 assert_eq!(tally(&it.func, Opcode::Add), 0, "the counter and the total go with it");
844 assert_eq!(tally(&it.func, Opcode::BrIf), 0);
845 sound(&it.func, &mut it.names);
846 }
847
848 #[test]
849 fn the_blocks_it_took_out_are_swept_rather_than_left_unreachable() {
850 let mut it = shaped(Limit::Number(1000), What::Nothing);
851 delete(&mut it.func, &mut Fuel::unlimited());
852 let left: Vec<Block> = it.func.blocks().collect();
853 assert_eq!(left, vec![it.entry, it.done], "the header and the body are gone");
854 sound(&it.func, &mut it.names);
855 }
856
857 /// A loop counting up to a value nothing here knows still has a last iteration.
858 ///
859 /// The bound comes back [`crate::scev::Count::Symbolic`] with two assumptions on it rather
860 /// than one. The overflow one the front end already promised.
861 /// [`crate::scev::Assumption::Entered`] it did not, and what that one says is whether the
862 /// count is the distance to the limit or zero. Both of those are numbers of times a loop goes
863 /// round, so the question this pass asks, which is whether there is a last time, has been
864 /// answered whichever of them it turns out to be. Nothing outside reads what this loop
865 /// computes, so the count is never multiplied by and the assumption is never spent.
866 ///
867 /// This is the `for (i = 0; i < n; i++)` of tamnd/rucc#1631 and of the corpus rows the report
868 /// had rucc losing on. Reading the bound through [`crate::scev::Bound::comes_back`] is what
869 /// makes it the answer.
870 #[test]
871 fn a_loop_counting_up_to_a_value_handed_in_is_taken_out() {
872 let mut it = shaped(Limit::Given, What::Nothing);
873 let stats = delete(&mut it.func, &mut Fuel::unlimited());
874 assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
875 assert_eq!(loops(&it.func), 0);
876 assert_eq!(tally(&it.func, Opcode::Add), 0, "the counter and the total go with it");
877 sound(&it.func, &mut it.names);
878 }
879
880 /// A loop that may step over the value it is waiting for is a loop that may not come back.
881 ///
882 /// `!=` ends a loop on the one iteration where the counter is the limit, so a counter that
883 /// starts past the limit, or that steps over it, goes round until it wraps. That is
884 /// [`crate::scev::Assumption::Approaching`], the one
885 /// [`crate::scev::Bound::comes_back`] holds back, and it is held back because document 17.2
886 /// says rucc does not take out a loop that might not end. The step here is one and the loop
887 /// would in fact arrive, which is the point: the pass refuses on what it has been shown rather
888 /// than on what happens to be true.
889 #[test]
890 fn a_loop_that_may_step_over_the_value_it_waits_for_is_left_alone() {
891 let mut it = shaped(Limit::Landing, What::Nothing);
892 let stats = delete(&mut it.func, &mut Fuel::unlimited());
893 assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
894 assert_eq!(stats.count(Kind::Missed, NO_COUNT), 1);
895 assert_eq!(loops(&it.func), 1);
896 sound(&it.func, &mut it.names);
897 }
898
899 #[test]
900 fn a_loop_that_writes_to_memory_is_left_alone() {
901 let mut it = shaped(Limit::Number(1000), What::Writes);
902 let stats = delete(&mut it.func, &mut Fuel::unlimited());
903 assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
904 assert_eq!(stats.count(Kind::Missed, EFFECTS), 1);
905 assert_eq!(loops(&it.func), 1);
906 assert_eq!(tally(&it.func, Opcode::Store), 1);
907 sound(&it.func, &mut it.names);
908 }
909
910 /// A total read after the loop without going through a parameter of the block that reads it.
911 ///
912 /// This is the shape that is actually there by the time the pass runs, rather than the loop
913 /// closed form one, because the block loop closed form put in the way is one `simplify-cfg`
914 /// folds back out. The value is defined in the loop and named in a block the loop dominates,
915 /// which is legal and is what a `for` loop adding to a total and printing it afterwards comes
916 /// out as. The worked out total goes where the loop's own was read.
917 #[test]
918 fn a_total_read_after_the_loop_by_another_road_is_worked_out_too() {
919 let mut it = shaped(Limit::Number(1000), What::ReadAfter);
920 let stats = delete(&mut it.func, &mut Fuel::unlimited());
921 assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
922 assert_eq!(stats.count(Kind::Optimized, WRITTEN), 1);
923 assert_eq!(loops(&it.func), 0);
924 assert_eq!(tally(&it.func, Opcode::Mul), 1, "one multiply, by the trip count");
925 assert_eq!(tally(&it.func, Opcode::Store), 1, "and the store that read it is still there");
926 sound(&it.func, &mut it.names);
927 }
928
929 /// The total the loop was going to hand over, worked out without running the loop.
930 ///
931 /// The loop adds `n` to a running total a thousand times, so the total it hands over is
932 /// `n * 1000`, and what is left of the function is that multiply. The count is 999 rather than
933 /// 1000 and the base is `n` rather than zero, because the exit edge is taken on the iteration
934 /// the test first fails and the total has already been added to by then: `n + n * 999`. Doing
935 /// the arithmetic on [`crate::scev::Invariant`] before writing anything down is what turns that
936 /// into one instruction rather than three nothing would fold, this being the last pass run.
937 #[test]
938 fn a_total_the_loop_hands_over_is_worked_out_in_front_of_it() {
939 let mut it = shaped(Limit::Number(1000), What::HandsOut);
940 let stats = delete(&mut it.func, &mut Fuel::unlimited());
941 assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
942 assert_eq!(stats.count(Kind::Optimized, WRITTEN), 1);
943 assert_eq!(loops(&it.func), 0);
944 assert_eq!(tally(&it.func, Opcode::Mul), 1, "one multiply, by the trip count");
945 assert_eq!(tally(&it.func, Opcode::Add), 0, "and nothing to add to it");
946 assert_eq!(handed(&it.func, &it.done), Some(1000), "n times a thousand");
947 sound(&it.func, &mut it.names);
948 }
949
950 /// The same thing where the amount is one, which leaves a number rather than a multiply.
951 #[test]
952 fn a_total_that_went_up_by_one_is_left_as_a_number() {
953 let mut it = shaped(Limit::Number(1000), What::HandsOne);
954 let stats = delete(&mut it.func, &mut Fuel::unlimited());
955 assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
956 assert_eq!(stats.count(Kind::Optimized, WRITTEN), 1);
957 assert_eq!(tally(&it.func, Opcode::Mul), 0);
958 assert_eq!(tally(&it.func, Opcode::Add), 0);
959 let handed = handed_value(&it.func, &it.done).expect("the total is handed over");
960 let (imm, ty) = crate::fold::constant(&it.func, handed).expect("and it is a number");
961 assert_eq!(imm.signed(ty), 1000);
962 sound(&it.func, &mut it.names);
963 }
964
965 /// The total handed over by a loop counting up to a value nothing here knows.
966 ///
967 /// The loop adds `n` to a running total until the counter arrives at `n`, so what it hands over
968 /// is `n + n * max(n - 1, 0)` and every part of that is written down in front of where the loop
969 /// was. The `select` is the clamp. [`crate::scev::Assumption::Entered`] says the count is
970 /// either the distance to the limit or zero, and taking the larger of the two is that
971 /// assumption paid for rather than leaned on. The sign extension in front of it is the exit
972 /// test's own reading of the value, which is `<` on signed values here.
973 #[test]
974 fn a_total_from_a_loop_counting_up_to_a_value_handed_in_is_worked_out() {
975 let mut it = shaped(Limit::Given, What::HandsOut);
976 let stats = delete(&mut it.func, &mut Fuel::unlimited());
977 assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
978 assert_eq!(stats.count(Kind::Optimized, WRITTEN), 1);
979 assert_eq!(loops(&it.func), 0);
980 assert_eq!(tally(&it.func, Opcode::Select), 1, "the clamp at zero");
981 assert_eq!(tally(&it.func, Opcode::ICmp), 1, "and the test it picks on");
982 let read = "the count read the way the exit test read it";
983 assert_eq!(tally(&it.func, Opcode::SExt), 1, "{read}");
984 assert_eq!(tally(&it.func, Opcode::Trunc), 1, "and cut back to what the total is added in");
985 assert_eq!(tally(&it.func, Opcode::Mul), 1, "one multiply, by the count");
986 assert_eq!(tally(&it.func, Opcode::Add), 2, "the off by one on the count and the base");
987 sound(&it.func, &mut it.names);
988 }
989
990 /// The same total read after the loop rather than handed over, which is the corpus row.
991 ///
992 /// `loop-deletion.u32.1000000.unknown.read-back` is this shape, a bound nothing can see and a
993 /// total read once the loop is done. It is the row rucc ran a million times and gcc 16 ran no
994 /// times at all. The store stays and what it stores is worked out where the loop used to be.
995 #[test]
996 fn a_total_read_after_a_loop_counting_up_to_a_value_handed_in_is_worked_out() {
997 let mut it = shaped(Limit::Given, What::ReadAfter);
998 let stats = delete(&mut it.func, &mut Fuel::unlimited());
999 assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
1000 assert_eq!(stats.count(Kind::Optimized, WRITTEN), 1);
1001 assert_eq!(loops(&it.func), 0);
1002 assert_eq!(tally(&it.func, Opcode::Select), 1, "the clamp at zero");
1003 assert_eq!(tally(&it.func, Opcode::Store), 1, "and the store that read it is still there");
1004 sound(&it.func, &mut it.names);
1005 }
1006
1007 /// A total that went up by a different amount every time is not a thing to write down.
1008 ///
1009 /// Here the total goes up by the counter rather than by a fixed amount, so what it holds after
1010 /// `k` times round is a square number and [`crate::scev`] rightly has no affine form for it.
1011 /// The loop ends and does nothing to memory, so the only thing keeping it is the total, and the
1012 /// pass says so rather than guessing at it.
1013 #[test]
1014 fn a_total_that_went_up_by_a_different_amount_each_time_leaves_the_loop_alone() {
1015 let mut it = shaped(Limit::Number(1000), What::HandsSquare);
1016 let stats = delete(&mut it.func, &mut Fuel::unlimited());
1017 assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
1018 assert_eq!(stats.count(Kind::Missed, NO_FORM), 1);
1019 assert_eq!(loops(&it.func), 1);
1020 sound(&it.func, &mut it.names);
1021 }
1022
1023 /// A loop that comes back but not after a number of steps anything here can work out.
1024 ///
1025 /// ```text
1026 /// entry(p, n): jump head(1)
1027 /// head(c): next = c + c; test = next < 1000; br test -> head(next), done()
1028 /// done: ret
1029 /// ```
1030 ///
1031 /// The counter doubles, so it is not a value that goes up by the same amount every time and
1032 /// there is no count to be had. It does terminate, which is the point: the pass is not allowed
1033 /// to lean on a loop looking harmless, only on the count that says it ends.
1034 fn doubling() -> Shape {
1035 let mut names = Interner::new();
1036 let signature = Signature::new().with_params(&[Type::PTR, Type::int(32)]);
1037 let mut func = Func::new(names.intern("f"), signature);
1038 let entry = func.create_block();
1039 let head = func.create_block();
1040 let done = func.create_block();
1041 func.append_param(entry, Type::PTR);
1042 func.append_param(entry, Type::int(32));
1043 let carried = func.append_param(head, Type::int(32));
1044
1045 let mut build = Builder::new(&mut func, entry);
1046 let one = build.iconst(Type::int(32), 1);
1047 build.jump(head, &[one]);
1048
1049 let mut build = Builder::new(&mut func, head);
1050 let next = build.binary(Opcode::Add, carried, carried, Flags::NSW);
1051 let stop = build.iconst(Type::int(32), 1000);
1052 let test = build.icmp(IntPred::Slt, next, stop);
1053 build.br_if(test, head, &[next], done, &[]);
1054 Builder::new(&mut func, done).ret(&[]);
1055 Shape { names, func, entry, done }
1056 }
1057
1058 #[test]
1059 fn a_loop_whose_count_is_not_known_is_left_alone() {
1060 let mut it = doubling();
1061 let stats = delete(&mut it.func, &mut Fuel::unlimited());
1062 assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
1063 assert_eq!(stats.count(Kind::Missed, NO_COUNT), 1);
1064 assert_eq!(loops(&it.func), 1);
1065 sound(&it.func, &mut it.names);
1066 }
1067
1068 #[test]
1069 fn the_pass_stops_when_the_fuel_runs_out() {
1070 let mut it = shaped(Limit::Number(1000), What::Nothing);
1071 let stats = delete(&mut it.func, &mut Fuel::of(0));
1072 assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
1073 assert_eq!(stats.count(Kind::Missed, NO_FUEL), 1);
1074 assert_eq!(loops(&it.func), 1);
1075 sound(&it.func, &mut it.names);
1076 }
1077
1078 #[test]
1079 fn a_function_with_no_body_is_not_a_problem() {
1080 let mut names = Interner::new();
1081 let mut func = Func::new(names.intern("f"), Signature::new());
1082 let stats = delete(&mut func, &mut Fuel::unlimited());
1083 assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
1084 }
1085}