rucc_opt/loop_delete.rs
1//! Takes out a loop that runs a known number of times, working out what it left behind.
2//!
3//! Design: `spec/optimizer/17-dce.md` for what makes a thing removable and
4//! `spec/optimizer/28-induction-variables.md` for where the trip count comes from. This is
5//! 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 trip count that is a number rather than an estimate. The count is
19//! what says the loop terminates, which is the third question and the one a person is most likely
20//! to forget: a loop that computes nothing and never comes back still cannot be taken out, because
21//! not coming back is what it does. [`crate::scev::Bound::under_undefined_overflow`] is the same
22//! accessor [`crate::unroll`] reads for the same reason, and section 7.5's distinction between a
23//! bound and an estimate is exactly this: an estimate decides whether a transformation pays and a
24//! bound decides what the program does.
25//!
26//! Every instruction inside has to be one whose not happening nothing can tell. That is the
27//! predicate [`crate::dce`] already has, so it is read from there rather than written again, and
28//! it means a plain load may be inside the loop and a `volatile` one may not. A call is allowed
29//! when the purity analysis says it reads memory at most and comes back, which is the same rule
30//! that lets a call whose result nothing reads go.
31//!
32//! # What the loop leaves behind
33//!
34//! A loop whose total somebody reads afterwards is a loop that hands something over, and most
35//! loops worth writing are that kind. Sometimes what it hands over can be worked out without
36//! running it. A value that goes up by the same amount every time round is `{base, +, step}` in
37//! [`crate::scev`]'s terms, the loop is left on the iteration the exit test first fails, and that
38//! iteration's number is the count, so what the loop leaves behind is `base + step * count`. The
39//! preheader works that out in one go and hands it over instead, and then nothing outside reads
40//! anything the loop computed and the loop goes.
41//!
42//! Handing it over is two different edits, because a value defined in the loop reaches the code
43//! after it by two different roads. It may be an argument on the edge out, landing in a parameter
44//! of the block the loop leaves to, which is the shape [`crate::canon`] puts things in. Or the
45//! block after the loop may simply name it, which is legal wherever the definition dominates the
46//! use and is what is actually there by the time this runs, since the block loop closed form put in
47//! the way is one [`crate::simplify_cfg`] has every reason to fold away again. So both are looked
48//! for, and a use of the second kind is rewritten where it stands.
49//!
50//! No overflow argument is needed for this and it is worth saying why, because the neighbouring
51//! transformation in section 28.4 does need one. A value that steps by a fixed amount evolves in
52//! its own type, which is to say modulo two to the width, and addition modulo two to the width is
53//! associative, so adding `step` to `base` `count` times and working out `base + step * count` the
54//! same way are the same number whatever either of them does to the top bit. Section 28.4's rewrite
55//! is a different claim, that one comparison holds exactly where another does, and that one does
56//! turn on whether the limit overflows. So the arithmetic written here carries neither `nsw` nor
57//! `nuw`, and the promise the loop's own increment carried is not copied onto it, because that
58//! promise is about the sequence and says nothing about this.
59//!
60//! What is written down is the whole expression rather than three instructions to be folded later,
61//! because this pass is the last one in the pipeline and there is no later. `base` and `step` are
62//! both [`crate::scev::Invariant`], which is `value * scale + offset` with the arithmetic on it
63//! already, so `base + step * count` is worked out in that form first and only what is left of it
64//! reaches the function. A loop adding one a million times leaves a constant behind and a loop
65//! adding an invariant `n` a million times leaves one multiply.
66//!
67//! It is only done when it lets the loop go, which is a cost rule rather than a correctness one.
68//! Writing the final value down where the loop stays behind costs a multiply in the preheader and
69//! saves nothing, because the loop still carries the value round its own back edge and nothing in
70//! rucc yet takes out a block parameter whose only reader is the argument it passes to itself.
71//! [`crate::dce`]'s own notes call that out as a transformation worth having and a different one
72//! from what it does. When there is one, this gate is the thing to reconsider.
73//!
74//! # What it does
75//!
76//! Works out in the preheader whatever the loop was going to leave behind, puts those values where
77//! the loop's own were read, points the preheader at the block the loop left to with whatever the
78//! edge out was already carrying from outside, and lets the sweep in [`crate::simplify_cfg`] take
79//! the blocks nothing reaches. Every value named in any of it is asserted to dominate the preheader
80//! rather than assumed to: a value defined outside the loop that reaches the exit test has to
81//! dominate the preheader, and an assertion is cheaper than being wrong about why.
82
83use std::collections::{HashMap, HashSet};
84
85use rucc_ir::{Block, Builder, Func, Inst, InstData, Opcode, Type, Value};
86
87use crate::cfg::Cfg;
88use crate::dom::Dominators;
89use crate::loops::{LoopId, Loops};
90use crate::purity::Facts;
91use crate::scev::{Bound, Count, Invariant, Scev};
92use crate::{Analyses, Fuel, Pass, Preserved, Stats};
93
94const DELETED: &str = "loop taken out, it runs a known number of times and leaves nothing behind";
95const WRITTEN: &str = "what the loop was going to leave behind worked out in front of it instead";
96const NO_COUNT: &str = "loop left as it was, how many times it runs is not a number known here";
97const SHAPE: &str =
98 "loop left as it was, it has no preheader or it leaves from more than one place";
99const EFFECTS: &str = "loop left as it was, something in it does more than work out a value";
100const NO_FORM: &str =
101 "loop left as it was, what it leaves behind is not a thing this can work out in front of it";
102const ENTRIES: &str = "loop left as it was, it is reached somewhere other than at its header";
103const NO_FUEL: &str = "loop left as it was, the pass ran out of fuel";
104
105/// Section 17's dead code elimination, asked about a loop rather than about an instruction.
106#[derive(Debug)]
107pub struct LoopDelete;
108
109impl Pass for LoopDelete {
110 fn name(&self) -> &'static str {
111 "loop-delete"
112 }
113
114 fn describe(&self) -> &'static str {
115 "a loop that runs a known number of times and leaves nothing behind is taken out"
116 }
117
118 fn preserves(&self) -> Preserved {
119 // The loop goes, and its blocks with it.
120 Preserved::NONE
121 }
122
123 fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
124 let mut stats = Stats::new();
125 if func.entry().is_none() {
126 return stats;
127 }
128 let mut done: HashSet<Block> = HashSet::new();
129 let mut say = true;
130 while let Some(job) = plan(func, an, &done, &mut stats, say) {
131 say = false;
132 if !fuel.take() {
133 stats.missed(NO_FUEL);
134 break;
135 }
136 done.insert(job.header);
137 for _ in 0..apply(func, &job) {
138 stats.optimized(WRITTEN);
139 }
140 stats.optimized(DELETED);
141 an.clear();
142 crate::simplify_cfg::sweep(func, an, &mut stats);
143 }
144 an.clear();
145 stats
146 }
147}
148
149/// One loop to take out, worked out against the function as it stands.
150#[derive(Debug)]
151struct Job {
152 /// The block the loop is entered at, which is what says which loop this was.
153 header: Block,
154 /// The one block outside the loop with an edge to the header.
155 preheader: Block,
156 /// The block outside the loop the one exit edge arrives at.
157 exit: Block,
158 /// The blocks the loop is made of, which is what says which uses are the ones outside it.
159 inside: HashSet<Block>,
160 /// What the edge out was going to carry, which the preheader carries instead.
161 args: Vec<Value>,
162 /// Every value the loop defines that anything outside it reads, and what it ends up holding.
163 ends: Vec<(Value, Leaves)>,
164}
165
166/// What a value the loop defines holds by the time anything outside it looks.
167#[derive(Clone, Copy, Debug)]
168struct Leaves {
169 /// The type it evolved in, which is the type the arithmetic is done in.
170 ty: Type,
171 /// `base + step * count`, as far as it goes without writing anything down.
172 end: Invariant,
173}
174
175/// The innermost loop that can go, and what it would take.
176///
177/// One at a time, for the reason [`crate::unroll::plan`] takes one at a time: taking a loop out
178/// invalidates the forest the next answer would be read out of. `say` is false after the first
179/// round so that a loop this declines is declined once rather than once per round.
180fn plan(
181 func: &Func,
182 an: &mut Analyses,
183 done: &HashSet<Block>,
184 stats: &mut Stats,
185 say: bool,
186) -> Option<Job> {
187 let facts = an.purity();
188 let cfg = an.cfg(func);
189 let doms = an.dominators(func);
190 let loops = an.loops(func);
191 let mut scev = Scev::new(func, cfg, loops);
192 let mut found: Option<(u32, Job)> = None;
193 for id in loops.all() {
194 if done.contains(&loops.header(id)) {
195 continue;
196 }
197 match consider(func, cfg, doms, loops, facts, &mut scev, id) {
198 Ok(job) => {
199 let depth = loops.depth(id);
200 if found.as_ref().is_none_or(|(had, _)| depth > *had) {
201 found = Some((depth, job));
202 }
203 }
204 Err(why) if say => stats.missed(why),
205 Err(_) => (),
206 }
207 }
208 found.map(|(_, job)| job)
209}
210
211/// Whether this loop can go, and why not when it cannot.
212fn consider(
213 func: &Func,
214 cfg: &Cfg,
215 doms: &Dominators,
216 loops: &Loops,
217 facts: &Facts,
218 scev: &mut Scev<'_>,
219 id: LoopId,
220) -> Result<Job, &'static str> {
221 let header = loops.header(id);
222 let preheader = loops.preheader(cfg, id).ok_or(SHAPE)?;
223 let [only] = loops.exits(id) else {
224 return Err(SHAPE);
225 };
226
227 let blocks = loops.blocks(id).to_vec();
228 let inside: HashSet<Block> = blocks.iter().copied().collect();
229 for &block in &blocks {
230 // The same reducibility check unrolling makes. A block of the loop reached from outside
231 // the loop is a region this has no right to reason about as one piece.
232 if block != header && cfg.predecessors(block).iter().any(|at| !inside.contains(at)) {
233 return Err(ENTRIES);
234 }
235 for inst in func.insts(block) {
236 if func.is_terminator(inst) {
237 // A terminator that leaves the function or goes somewhere worked out at run time
238 // is not an edge the forest accounted for, so the one exit counted above is not
239 // the only way out.
240 if !matches!(func[inst].opcode, Opcode::Jump | Opcode::BrIf) {
241 return Err(EFFECTS);
242 }
243 continue;
244 }
245 if !crate::dce::removable(func, inst, facts) {
246 return Err(EFFECTS);
247 }
248 }
249 }
250 // The count is what says the loop comes back. A loop that computes nothing and runs forever
251 // still does something, which is run forever. A count worked out from a value the loop does
252 // not change says that as well as a number does: whatever that value is, the loop gets to it,
253 // and nothing in here needs to know how many steps that took.
254 let count =
255 scev.bound(id).as_ref().and_then(Bound::under_undefined_overflow).ok_or(NO_COUNT)?;
256
257 let term = func.terminator(only.from).ok_or(SHAPE)?;
258 let leaving = func.successors(term).find(|call| call.block == only.to).ok_or(SHAPE)?;
259 let args = func[leaving.args].to_vec();
260
261 let mut wanted = read_outside(func, &blocks, &inside);
262 for &arg in &args {
263 if loops.is_invariant(func, id, arg) {
264 debug_assert!(
265 doms.dominates(defined_in(func, arg), preheader),
266 "a value outside the loop that reaches the exit test dominates the preheader"
267 );
268 continue;
269 }
270 if !wanted.contains(&arg) {
271 wanted.push(arg);
272 }
273 }
274
275 let mut ends = Vec::with_capacity(wanted.len());
276 for value in wanted {
277 let end = ending(func, scev, id, value, count).ok_or(NO_FORM)?;
278 debug_assert!(
279 named(end).is_none_or(|on| doms.dominates(defined_in(func, on), preheader)),
280 "a value the loop does not change is defined outside it and so dominates the preheader"
281 );
282 ends.push((value, end));
283 }
284 Ok(Job { header, preheader, exit: only.to, inside, args, ends })
285}
286
287/// Every value the loop defines that a block outside it names, in the order they turn up.
288///
289/// [`crate::unroll::escapes`] asks whether there is one of these and stops there, because a loop
290/// with one is a loop it will not copy. Here they are the work rather than the reason to stop, so
291/// the answer has to be which ones.
292fn read_outside(func: &Func, blocks: &[Block], inside: &HashSet<Block>) -> Vec<Value> {
293 let mut defined: HashSet<Value> = HashSet::new();
294 for &block in blocks {
295 defined.extend(func[block].params.iter().copied());
296 for inst in func.insts(block) {
297 defined.extend(func[inst].results());
298 }
299 }
300 let mut found = Vec::new();
301 for block in func.blocks() {
302 if inside.contains(&block) {
303 continue;
304 }
305 for inst in func.insts(block) {
306 let reads = func[func[inst].args].iter().copied();
307 let passes = func.successors(inst).flat_map(|call| func[call.args].to_vec());
308 for value in reads.chain(passes) {
309 if defined.contains(&value) && !found.contains(&value) {
310 found.push(value);
311 }
312 }
313 }
314 }
315 found
316}
317
318/// What the loop leaves in a value it hands over, or `None` when that is not a thing to write down.
319///
320/// The count has to be a number here rather than an expression, which is a narrower rule than the
321/// one the loop itself is kept under. A count worked out from something the loop does not change
322/// says the loop ends, which is all the loop needs, but multiplying by it means writing it down in
323/// the counter's own type and under the reading its exit test took, and getting that wrong turns a
324/// loop over three billion elements into a loop that runs no times. Section 7.7's warning is about
325/// exactly that, so until there is a reason to, this takes the count it can count.
326fn ending(
327 func: &Func,
328 scev: &mut Scev<'_>,
329 id: LoopId,
330 value: Value,
331 count: Count,
332) -> Option<Leaves> {
333 let Count::Exact(trips) = count else {
334 return None;
335 };
336 let trips = i128::try_from(trips).ok()?;
337 let chrec = scev.evolution(id, value).chrec()?;
338 let end = chrec.step.times(Invariant::number(trips)).and_then(|all| chrec.base.plus(all))?;
339 // Asked before anything is written, so that a refusal is a refusal rather than a preheader with
340 // half an expression in it. There is no undo here and there should not need to be.
341 let plain = end.plain()?;
342 if plain.read.is_some() {
343 return None;
344 }
345 if plain.value.is_some_and(|named| func[named].ty != chrec.ty) {
346 return None;
347 }
348 Some(Leaves { ty: chrec.ty, end })
349}
350
351/// The one value an expression is built on, when it is built on one.
352fn named(leaves: Leaves) -> Option<Value> {
353 leaves.end.plain().and_then(|plain| plain.value.filter(|_| plain.scale != 0))
354}
355
356/// The block a value is defined in.
357fn defined_in(func: &Func, value: Value) -> Block {
358 match func[value].def {
359 rucc_ir::Def::Result { inst, .. } => {
360 func.block_of(inst).expect("a value in use is defined in a block")
361 }
362 rucc_ir::Def::Param { block, .. } => block,
363 }
364}
365
366/// Points the preheader past the loop, working out on the way what the loop was going to leave.
367///
368/// Answers how many of those there were, which is what the report counts.
369fn apply(func: &mut Func, job: &Job) -> usize {
370 let term = func.terminator(job.preheader).expect("a preheader ends in a jump to the header");
371 let mut instead: HashMap<Value, Value> = HashMap::new();
372 for &(value, leaves) in &job.ends {
373 let worked = write(func, term, leaves.ty, leaves.end);
374 instead.insert(value, worked);
375 }
376 swap_in(func, job, &instead);
377 let args: Vec<Value> =
378 job.args.iter().map(|arg| instead.get(arg).copied().unwrap_or(*arg)).collect();
379 func.remove_inst(term);
380 Builder::new(func, job.preheader).jump(job.exit, &args);
381 instead.len()
382}
383
384/// Puts the worked out values where the loop's own were read.
385///
386/// Only outside the loop, because inside it the loop's own values are still the right answer right
387/// up until the blocks go. The preheader is outside and gets walked with the rest, which is
388/// harmless and better than a special case: what was just written into it names nothing the loop
389/// defines.
390fn swap_in(func: &mut Func, job: &Job, instead: &HashMap<Value, Value>) {
391 if instead.is_empty() {
392 return;
393 }
394 let outside: Vec<Block> = func.blocks().filter(|at| !job.inside.contains(at)).collect();
395 for block in outside {
396 for inst in func.insts(block).collect::<Vec<_>>() {
397 let mut lists = vec![func[inst].args];
398 lists.extend(func.successors(inst).map(|call| call.args));
399 for list in lists {
400 func.rewrite(list, |value| instead.get(&value).copied().unwrap_or(value));
401 }
402 }
403 }
404}
405
406/// Works an expression out in front of an instruction.
407///
408/// `value * scale + offset`, with the parts that are nothing left out, so a scale of one is no
409/// multiply and an offset of zero is no add and an expression built on no value at all is one
410/// constant. That is what makes a loop adding one a million times leave a number behind rather than
411/// three instructions nothing is going to fold, this being the last pass there is.
412fn write(func: &mut Func, before: Inst, ty: Type, end: Invariant) -> Value {
413 let plain = end.plain().expect("consider refused anything this cannot write");
414 let Some(value) = plain.value.filter(|_| plain.scale != 0) else {
415 return crate::ivopts::number(func, before, ty, plain.offset);
416 };
417 let mut so_far = value;
418 if plain.scale != 1 {
419 let by = crate::ivopts::number(func, before, ty, plain.scale);
420 so_far = arith(func, before, Opcode::Mul, so_far, by, ty);
421 }
422 if plain.offset != 0 {
423 let by = crate::ivopts::number(func, before, ty, plain.offset);
424 so_far = arith(func, before, Opcode::Add, so_far, by, ty);
425 }
426 so_far
427}
428
429/// One arithmetic instruction, worked out in front of another one and promising nothing.
430///
431/// Neither `nsw` nor `nuw`, which is the point rather than an omission. The module notes say why:
432/// what the loop did is the same arithmetic modulo two to the width as many times as it ran, and
433/// `base + step * count` worked out the same way is the same number. A flag the loop's own
434/// increment carried is a fact about that sequence, and putting it here would be inventing one.
435fn arith(
436 func: &mut Func,
437 before: Inst,
438 opcode: Opcode,
439 left: Value,
440 right: Value,
441 ty: Type,
442) -> Value {
443 let span = func.span(before);
444 let args = func.push_values(&[left, right]);
445 let inst = func.create_inst(InstData { args, ..InstData::new(opcode) }, &[ty], span);
446 func.insert_before(inst, before);
447 func[inst].first_result.expect("one result was asked for")
448}
449
450#[cfg(test)]
451mod tests {
452 use rucc_base::Interner;
453 use rucc_ir::{
454 Block, Builder, Def, Flags, Func, IntPred, MemInfo, MemOrder, Module, Opcode, Restrict,
455 Signature, Type, Value, verify_func,
456 };
457 use rucc_target::{TargetInfo, Triple};
458
459 use super::{DELETED, EFFECTS, LoopDelete, NO_COUNT, NO_FORM, NO_FUEL, WRITTEN};
460 use crate::stats::Kind;
461 use crate::{Fuel, Pass, Stats};
462
463 /// Runs the pass over the function as it stands.
464 fn delete(func: &mut Func, fuel: &mut Fuel) -> Stats {
465 LoopDelete.run(func, &mut crate::machine::fixtures::analyses(), fuel)
466 }
467
468 /// Insists the function is one the rest of the compiler may believe.
469 ///
470 /// Pointing a block at a different successor is the edit that hands a block the wrong number
471 /// of arguments and strands a definition its uses still name, so this is where most of the
472 /// strength of these tests is.
473 fn sound(func: &Func, names: &mut Interner) {
474 let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
475 let module = Module::new(names.intern("t.c"), &target);
476 if let Err(errors) = verify_func(&module, func, names) {
477 panic!("{errors:#?}");
478 }
479 }
480
481 /// How many instructions of that opcode the whole function holds.
482 fn tally(func: &Func, opcode: Opcode) -> usize {
483 func.blocks()
484 .flat_map(|block| func.insts(block))
485 .filter(|&inst| func[inst].opcode == opcode)
486 .count()
487 }
488
489 /// The one value now handed to the block the loop used to leave to.
490 fn handed_value(func: &Func, done: &Block) -> Option<Value> {
491 let cfg = crate::cfg::Cfg::new(func);
492 let [only] = cfg.predecessors(*done) else {
493 return None;
494 };
495 let term = func.terminator(*only)?;
496 let call = func.successors(term).find(|call| call.block == *done)?;
497 let args = func[call.args].to_vec();
498 let [arg] = args[..] else {
499 return None;
500 };
501 Some(arg)
502 }
503
504 /// What the value handed over is a multiple of, when it is a multiple of something.
505 fn handed(func: &Func, done: &Block) -> Option<i128> {
506 let value = handed_value(func, done)?;
507 let Def::Result { inst, .. } = func[value].def else {
508 return None;
509 };
510 if func[inst].opcode != Opcode::Mul {
511 return None;
512 }
513 let args = func[func[inst].args].to_vec();
514 let (imm, ty) = crate::fold::constant(func, args[1])?;
515 Some(imm.signed(ty))
516 }
517
518 /// How many loops are left.
519 fn loops(func: &Func) -> usize {
520 let cfg = crate::cfg::Cfg::new(func);
521 let doms = crate::dom::Dominators::new(&cfg);
522 crate::loops::Loops::new(&cfg, &doms).count()
523 }
524
525 /// A four byte write with nothing said about what it aliases.
526 fn plain() -> MemInfo {
527 MemInfo {
528 size: 4,
529 align: 4,
530 order: MemOrder::NotAtomic,
531 tbaa: None,
532 owns: 0,
533 restrict: Restrict::NONE,
534 }
535 }
536
537 /// What the limit of the exit test is.
538 enum Limit {
539 /// A number written in the program.
540 Number(i128),
541 /// A value the function was handed, which the loop does not change.
542 Given,
543 }
544
545 /// What the loop does, which is the whole of what decides whether it can go.
546 #[derive(Clone, Copy, PartialEq)]
547 enum What {
548 /// Adds up a number nothing ever reads.
549 Nothing,
550 /// Writes each running total to the pointer it was handed.
551 Writes,
552 /// Hands the block it leaves to a total that went up by the same amount every time.
553 HandsOut,
554 /// Hands out a total that went up by one every time, so there is no multiply to write.
555 HandsOne,
556 /// Hands out a total that went up by a different amount every time round.
557 HandsSquare,
558 /// Leaves its total to be read after the loop by a road other than the edge out.
559 ReadAfter,
560 }
561
562 impl What {
563 /// Whether the total goes out on the edge the loop leaves by.
564 fn hands_out(self) -> bool {
565 matches!(self, What::HandsOut | What::HandsOne | What::HandsSquare)
566 }
567 }
568
569 struct Shape {
570 names: Interner,
571 func: Func,
572 entry: Block,
573 done: Block,
574 }
575
576 /// A counted loop in the shape `crate::canon` and `crate::header_copy` leave a `for` in.
577 ///
578 /// ```text
579 /// entry(p, n): jump head(0, 0)
580 /// head(i, sum): jump body(i, sum)
581 /// body(c, r): total = r + n; next = c + 1; test = next < limit
582 /// br test -> head(next, total), done()
583 /// done: ret
584 /// ```
585 ///
586 /// Two blocks in the loop rather than one, so that taking it out has more than one block to get
587 /// rid of, and a running total carried round, so that there is something inside worth asking
588 /// whether anybody reads. The total goes up by `n` each time round, which is the shape of
589 /// `total += seed` in the issue: a value that goes up by the same amount every time, where the
590 /// amount is not a number anything here knows.
591 fn shaped(limit: Limit, what: What) -> Shape {
592 let mut names = Interner::new();
593 let signature = Signature::new().with_params(&[Type::PTR, Type::int(32)]);
594 let mut func = Func::new(names.intern("f"), signature);
595 let entry = func.create_block();
596 let head = func.create_block();
597 let body = func.create_block();
598 let done = func.create_block();
599 let place = func.append_param(entry, Type::PTR);
600 let given = func.append_param(entry, Type::int(32));
601 let i = func.append_param(head, Type::int(32));
602 let sum = func.append_param(head, Type::int(32));
603 let carried = func.append_param(body, Type::int(32));
604 let running = func.append_param(body, Type::int(32));
605 if what.hands_out() {
606 func.append_param(done, Type::int(32));
607 }
608
609 let mut build = Builder::new(&mut func, entry);
610 let zero = build.iconst(Type::int(32), 0);
611 build.jump(head, &[zero, zero]);
612 Builder::new(&mut func, head).jump(body, &[i, sum]);
613
614 let mut build = Builder::new(&mut func, body);
615 let one = build.iconst(Type::int(32), 1);
616 let by = match what {
617 What::HandsOne => one,
618 What::HandsSquare => carried,
619 _ => given,
620 };
621 let total = build.binary(Opcode::Add, running, by, Flags::NSW);
622 if what == What::Writes {
623 build.store(total, place, plain(), Flags::NONE);
624 }
625 let next = build.binary(Opcode::Add, carried, one, Flags::NSW);
626 let stop = match limit {
627 Limit::Number(n) => build.iconst(Type::int(32), n),
628 Limit::Given => given,
629 };
630 let test = build.icmp(IntPred::Slt, next, stop);
631 let out: Vec<Value> = if what.hands_out() { vec![total] } else { Vec::new() };
632 build.br_if(test, head, &[next, total], done, &out);
633 let mut build = Builder::new(&mut func, done);
634 if what == What::ReadAfter {
635 build.store(total, place, plain(), Flags::NONE);
636 }
637 build.ret(&[]);
638 Shape { names, func, entry, done }
639 }
640
641 #[test]
642 fn a_loop_that_leaves_nothing_behind_is_taken_out() {
643 let mut it = shaped(Limit::Number(1000), What::Nothing);
644 let stats = delete(&mut it.func, &mut Fuel::unlimited());
645 assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
646 assert_eq!(loops(&it.func), 0);
647 assert_eq!(tally(&it.func, Opcode::Add), 0, "the counter and the total go with it");
648 assert_eq!(tally(&it.func, Opcode::BrIf), 0);
649 sound(&it.func, &mut it.names);
650 }
651
652 #[test]
653 fn the_blocks_it_took_out_are_swept_rather_than_left_unreachable() {
654 let mut it = shaped(Limit::Number(1000), What::Nothing);
655 delete(&mut it.func, &mut Fuel::unlimited());
656 let left: Vec<Block> = it.func.blocks().collect();
657 assert_eq!(left, vec![it.entry, it.done], "the header and the body are gone");
658 sound(&it.func, &mut it.names);
659 }
660
661 /// A count that rests on more than the front end already promised is not a proof.
662 ///
663 /// The loop here counts up to a value handed to the function, and the bound for it comes back
664 /// [`crate::scev::Count::Symbolic`] with two assumptions on it rather than one. The overflow
665 /// one the front end already promised. [`crate::scev::Assumption::Approaching`] it did not:
666 /// nothing here has shown the counter lands on that limit rather than stepping past it. So the
667 /// pass is not entitled to say the loop ends, and a loop that might not end is a loop that does
668 /// something. Reading the count through [`crate::scev::Bound::under_undefined_overflow`] is
669 /// what makes that the answer, rather than a thing this pass would have to check for itself.
670 ///
671 /// A symbolic count with nothing but the overflow assumption on it is fine and would be taken.
672 /// This is about which assumptions are left, not about the count being a number.
673 #[test]
674 fn a_count_that_rests_on_more_than_signed_overflow_is_not_enough() {
675 let mut it = shaped(Limit::Given, What::Nothing);
676 let stats = delete(&mut it.func, &mut Fuel::unlimited());
677 assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
678 assert_eq!(stats.count(Kind::Missed, NO_COUNT), 1);
679 assert_eq!(loops(&it.func), 1);
680 sound(&it.func, &mut it.names);
681 }
682
683 #[test]
684 fn a_loop_that_writes_to_memory_is_left_alone() {
685 let mut it = shaped(Limit::Number(1000), What::Writes);
686 let stats = delete(&mut it.func, &mut Fuel::unlimited());
687 assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
688 assert_eq!(stats.count(Kind::Missed, EFFECTS), 1);
689 assert_eq!(loops(&it.func), 1);
690 assert_eq!(tally(&it.func, Opcode::Store), 1);
691 sound(&it.func, &mut it.names);
692 }
693
694 /// A total read after the loop without going through a parameter of the block that reads it.
695 ///
696 /// This is the shape that is actually there by the time the pass runs, rather than the loop
697 /// closed form one, because the block loop closed form put in the way is one `simplify-cfg`
698 /// folds back out. The value is defined in the loop and named in a block the loop dominates,
699 /// which is legal and is what a `for` loop adding to a total and printing it afterwards comes
700 /// out as. The worked out total goes where the loop's own was read.
701 #[test]
702 fn a_total_read_after_the_loop_by_another_road_is_worked_out_too() {
703 let mut it = shaped(Limit::Number(1000), What::ReadAfter);
704 let stats = delete(&mut it.func, &mut Fuel::unlimited());
705 assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
706 assert_eq!(stats.count(Kind::Optimized, WRITTEN), 1);
707 assert_eq!(loops(&it.func), 0);
708 assert_eq!(tally(&it.func, Opcode::Mul), 1, "one multiply, by the trip count");
709 assert_eq!(tally(&it.func, Opcode::Store), 1, "and the store that read it is still there");
710 sound(&it.func, &mut it.names);
711 }
712
713 /// The total the loop was going to hand over, worked out without running the loop.
714 ///
715 /// The loop adds `n` to a running total a thousand times, so the total it hands over is
716 /// `n * 1000`, and what is left of the function is that multiply. The count is 999 rather than
717 /// 1000 and the base is `n` rather than zero, because the exit edge is taken on the iteration
718 /// the test first fails and the total has already been added to by then: `n + n * 999`. Doing
719 /// the arithmetic on [`crate::scev::Invariant`] before writing anything down is what turns that
720 /// into one instruction rather than three nothing would fold, this being the last pass run.
721 #[test]
722 fn a_total_the_loop_hands_over_is_worked_out_in_front_of_it() {
723 let mut it = shaped(Limit::Number(1000), What::HandsOut);
724 let stats = delete(&mut it.func, &mut Fuel::unlimited());
725 assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
726 assert_eq!(stats.count(Kind::Optimized, WRITTEN), 1);
727 assert_eq!(loops(&it.func), 0);
728 assert_eq!(tally(&it.func, Opcode::Mul), 1, "one multiply, by the trip count");
729 assert_eq!(tally(&it.func, Opcode::Add), 0, "and nothing to add to it");
730 assert_eq!(handed(&it.func, &it.done), Some(1000), "n times a thousand");
731 sound(&it.func, &mut it.names);
732 }
733
734 /// The same thing where the amount is one, which leaves a number rather than a multiply.
735 #[test]
736 fn a_total_that_went_up_by_one_is_left_as_a_number() {
737 let mut it = shaped(Limit::Number(1000), What::HandsOne);
738 let stats = delete(&mut it.func, &mut Fuel::unlimited());
739 assert_eq!(stats.count(Kind::Optimized, DELETED), 1);
740 assert_eq!(stats.count(Kind::Optimized, WRITTEN), 1);
741 assert_eq!(tally(&it.func, Opcode::Mul), 0);
742 assert_eq!(tally(&it.func, Opcode::Add), 0);
743 let handed = handed_value(&it.func, &it.done).expect("the total is handed over");
744 let (imm, ty) = crate::fold::constant(&it.func, handed).expect("and it is a number");
745 assert_eq!(imm.signed(ty), 1000);
746 sound(&it.func, &mut it.names);
747 }
748
749 /// A total that went up by a different amount every time is not a thing to write down.
750 ///
751 /// Here the total goes up by the counter rather than by a fixed amount, so what it holds after
752 /// `k` times round is a square number and [`crate::scev`] rightly has no affine form for it.
753 /// The loop ends and does nothing to memory, so the only thing keeping it is the total, and the
754 /// pass says so rather than guessing at it.
755 #[test]
756 fn a_total_that_went_up_by_a_different_amount_each_time_leaves_the_loop_alone() {
757 let mut it = shaped(Limit::Number(1000), What::HandsSquare);
758 let stats = delete(&mut it.func, &mut Fuel::unlimited());
759 assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
760 assert_eq!(stats.count(Kind::Missed, NO_FORM), 1);
761 assert_eq!(loops(&it.func), 1);
762 sound(&it.func, &mut it.names);
763 }
764
765 /// A loop that comes back but not after a number of steps anything here can work out.
766 ///
767 /// ```text
768 /// entry(p, n): jump head(1)
769 /// head(c): next = c + c; test = next < 1000; br test -> head(next), done()
770 /// done: ret
771 /// ```
772 ///
773 /// The counter doubles, so it is not a value that goes up by the same amount every time and
774 /// there is no count to be had. It does terminate, which is the point: the pass is not allowed
775 /// to lean on a loop looking harmless, only on the count that says it ends.
776 fn doubling() -> Shape {
777 let mut names = Interner::new();
778 let signature = Signature::new().with_params(&[Type::PTR, Type::int(32)]);
779 let mut func = Func::new(names.intern("f"), signature);
780 let entry = func.create_block();
781 let head = func.create_block();
782 let done = func.create_block();
783 func.append_param(entry, Type::PTR);
784 func.append_param(entry, Type::int(32));
785 let carried = func.append_param(head, Type::int(32));
786
787 let mut build = Builder::new(&mut func, entry);
788 let one = build.iconst(Type::int(32), 1);
789 build.jump(head, &[one]);
790
791 let mut build = Builder::new(&mut func, head);
792 let next = build.binary(Opcode::Add, carried, carried, Flags::NSW);
793 let stop = build.iconst(Type::int(32), 1000);
794 let test = build.icmp(IntPred::Slt, next, stop);
795 build.br_if(test, head, &[next], done, &[]);
796 Builder::new(&mut func, done).ret(&[]);
797 Shape { names, func, entry, done }
798 }
799
800 #[test]
801 fn a_loop_whose_count_is_not_known_is_left_alone() {
802 let mut it = doubling();
803 let stats = delete(&mut it.func, &mut Fuel::unlimited());
804 assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
805 assert_eq!(stats.count(Kind::Missed, NO_COUNT), 1);
806 assert_eq!(loops(&it.func), 1);
807 sound(&it.func, &mut it.names);
808 }
809
810 #[test]
811 fn the_pass_stops_when_the_fuel_runs_out() {
812 let mut it = shaped(Limit::Number(1000), What::Nothing);
813 let stats = delete(&mut it.func, &mut Fuel::of(0));
814 assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
815 assert_eq!(stats.count(Kind::Missed, NO_FUEL), 1);
816 assert_eq!(loops(&it.func), 1);
817 sound(&it.func, &mut it.names);
818 }
819
820 #[test]
821 fn a_function_with_no_body_is_not_a_problem() {
822 let mut names = Interner::new();
823 let mut func = Func::new(names.intern("f"), Signature::new());
824 let stats = delete(&mut func, &mut Fuel::unlimited());
825 assert_eq!(stats.count(Kind::Optimized, DELETED), 0);
826 }
827}