Rust · 46038 bytes Raw Blame History
1 //! Bounds Check Elimination (BCE).
2 //!
3 //! Removes `RuntimeCall(CheckBounds, [index, lower, upper])` calls
4 //! when the index can be proven in-bounds. At O2+, this eliminates
5 //! the overhead of runtime bounds checking for safe array accesses.
6 //!
7 //! Safe patterns:
8 //! - Loop IV in a counted loop with bounds [lo, hi] where lo >= lower
9 //! and hi <= upper
10 //! - Constant index within [lower, upper]
11 //!
12 //! Bounds checks are inserted during lowering for scalar array
13 //! element accesses. This pass removes the checks when the index is
14 //! provably safe.
15
16 use super::loop_utils::{find_preheader, resolve_const_int};
17 use super::pass::Pass;
18 use crate::ir::inst::*;
19 use crate::ir::walk::{find_natural_loops, predecessors};
20
21 pub struct Bce;
22
23 impl Pass for Bce {
24 fn name(&self) -> &'static str {
25 "bce"
26 }
27
28 fn run(&self, module: &mut Module) -> bool {
29 let mut changed = false;
30 for func in &mut module.functions {
31 if bce_function(func) {
32 changed = true;
33 }
34 }
35 changed
36 }
37 }
38
39 fn bce_function(func: &mut Function) -> bool {
40 let loops = find_natural_loops(func);
41 let preds = predecessors(func);
42 let mut to_remove: Vec<(BlockId, usize)> = Vec::new();
43
44 for block in &func.blocks {
45 for (inst_idx, inst) in block.insts.iter().enumerate() {
46 if let InstKind::RuntimeCall(RuntimeFunc::CheckBounds, args) = &inst.kind {
47 if args.len() == 3 {
48 let index = args[0];
49 let lower = args[1];
50 let upper = args[2];
51
52 if is_provably_safe(func, &loops, &preds, index, lower, upper) {
53 to_remove.push((block.id, inst_idx));
54 }
55 }
56 }
57 }
58 }
59
60 if to_remove.is_empty() {
61 return false;
62 }
63
64 // Remove in reverse order to preserve indices.
65 to_remove.sort_by_key(|entry| std::cmp::Reverse(entry.1));
66 for (block_id, inst_idx) in to_remove {
67 func.block_mut(block_id).insts.remove(inst_idx);
68 }
69
70 true
71 }
72
73 /// Check if an array index is provably within [lower, upper].
74 fn is_provably_safe(
75 func: &Function,
76 loops: &[crate::ir::walk::NaturalLoop],
77 preds: &std::collections::HashMap<BlockId, Vec<BlockId>>,
78 index: ValueId,
79 lower: ValueId,
80 upper: ValueId,
81 ) -> bool {
82 let index = strip_int_casts(func, index);
83
84 // Case 1: constant index, constant bounds.
85 if let (Some(idx), Some(lo), Some(hi)) = (
86 resolve_int_scalar(func, index),
87 resolve_int_scalar(func, lower),
88 resolve_int_scalar(func, upper),
89 ) {
90 return idx >= lo && idx <= hi;
91 }
92
93 // Case 2: index is a canonical loop IV, and the loop's closed range
94 // stays within the checked bounds.
95 let Some(lo_const) = resolve_int_scalar(func, lower) else {
96 return false;
97 };
98 let Some(hi_const) = resolve_int_scalar(func, upper) else {
99 return false;
100 };
101 for lp in loops {
102 let Some((range_lo, range_hi)) = loop_index_range(func, lp, preds, index) else {
103 continue;
104 };
105 if range_lo >= lo_const && range_hi <= hi_const {
106 return true;
107 }
108 }
109
110 false
111 }
112
113 fn loop_index_range(
114 func: &Function,
115 lp: &crate::ir::walk::NaturalLoop,
116 preds: &std::collections::HashMap<BlockId, Vec<BlockId>>,
117 index: ValueId,
118 ) -> Option<(i64, i64)> {
119 let (index, scale, offset) = loop_index_base_and_offset(func, index)?;
120 let header = func.block(lp.header);
121 let param_idx = header.params.iter().position(|param| param.id == index)?;
122 let init = loop_init_const(func, lp, preds, param_idx)?;
123 let (bound, dir) = loop_bound_const(func, lp.header, index)?;
124 let step = loop_step_const(func, lp, param_idx, index)?;
125
126 let (range_lo, range_hi) = match dir {
127 LoopDir::Ascending if step > 0 => {
128 if init > bound {
129 return None;
130 }
131 let trip = bound.checked_sub(init)?;
132 let last = init.checked_add((trip / step).checked_mul(step)?)?;
133 (init.min(last), init.max(last))
134 }
135 LoopDir::Descending if step < 0 => {
136 if init < bound {
137 return None;
138 }
139 let step_mag = step.checked_neg()?;
140 let trip = init.checked_sub(bound)?;
141 let last = init.checked_sub((trip / step_mag).checked_mul(step_mag)?)?;
142 (init.min(last), init.max(last))
143 }
144 _ => return None,
145 };
146 // Apply scale and offset: idx == scale * base + offset, with
147 // `base` ranging over [range_lo, range_hi]. Negative scale flips
148 // the interval, so take min/max after multiplying both endpoints.
149 let lo_scaled = scale.checked_mul(range_lo)?;
150 let hi_scaled = scale.checked_mul(range_hi)?;
151 let adj_lo = lo_scaled.min(hi_scaled).checked_add(offset)?;
152 let adj_hi = lo_scaled.max(hi_scaled).checked_add(offset)?;
153 Some((adj_lo, adj_hi))
154 }
155
156 /// Decompose `value` into a linear function of an inner SSA value:
157 /// `value == scale * base + offset`. The "base" returned is whatever
158 /// definition is left after we've stripped IAdd / ISub / IMul of
159 /// constant operands; if `base` is the loop induction variable, the
160 /// caller can then map a known IV range into a known range for
161 /// `value` itself.
162 ///
163 /// Default decomposition is `(value, 1, 0)` (i.e. base==value, no
164 /// scale, no offset). Recurses through:
165 /// * `iadd lhs, const` / `isub lhs, const` → bumps offset.
166 /// * `imul lhs, const` / `imul const, rhs` → multiplies both scale
167 /// and offset, capturing `(2*i) + 3 = 2*i + 3` and `2*(i+1) = 2*i + 2`.
168 fn loop_index_base_and_offset(func: &Function, value: ValueId) -> Option<(ValueId, i64, i64)> {
169 let value = strip_int_casts(func, value);
170 let Some(kind) = find_inst_kind(func, value) else {
171 return Some((value, 1, 0));
172 };
173
174 match kind {
175 InstKind::IAdd(lhs, rhs) => {
176 if let Some((base, scale, offset)) = loop_index_base_and_offset(func, *lhs) {
177 if let Some(delta) = resolve_int_scalar(func, *rhs) {
178 return offset.checked_add(delta).map(|next| (base, scale, next));
179 }
180 }
181 if let Some((base, scale, offset)) = loop_index_base_and_offset(func, *rhs) {
182 if let Some(delta) = resolve_int_scalar(func, *lhs) {
183 return offset.checked_add(delta).map(|next| (base, scale, next));
184 }
185 }
186 Some((value, 1, 0))
187 }
188 InstKind::ISub(lhs, rhs) => {
189 if let Some((base, scale, offset)) = loop_index_base_and_offset(func, *lhs) {
190 if let Some(delta) = resolve_int_scalar(func, *rhs) {
191 return offset.checked_sub(delta).map(|next| (base, scale, next));
192 }
193 }
194 Some((value, 1, 0))
195 }
196 InstKind::IMul(lhs, rhs) => {
197 // k * (s * base + o) = (k*s) * base + k*o
198 if let Some((base, scale, offset)) = loop_index_base_and_offset(func, *lhs) {
199 if let Some(k) = resolve_int_scalar(func, *rhs) {
200 let new_scale = scale.checked_mul(k)?;
201 let new_offset = offset.checked_mul(k)?;
202 return Some((base, new_scale, new_offset));
203 }
204 }
205 if let Some((base, scale, offset)) = loop_index_base_and_offset(func, *rhs) {
206 if let Some(k) = resolve_int_scalar(func, *lhs) {
207 let new_scale = scale.checked_mul(k)?;
208 let new_offset = offset.checked_mul(k)?;
209 return Some((base, new_scale, new_offset));
210 }
211 }
212 Some((value, 1, 0))
213 }
214 _ => Some((value, 1, 0)),
215 }
216 }
217
218 fn loop_init_const(
219 func: &Function,
220 lp: &crate::ir::walk::NaturalLoop,
221 preds: &std::collections::HashMap<BlockId, Vec<BlockId>>,
222 param_idx: usize,
223 ) -> Option<i64> {
224 let preheader = find_preheader(func, lp, preds)?;
225 match &func.block(preheader).terminator {
226 Some(Terminator::Branch(dest, args)) if *dest == lp.header && param_idx < args.len() => {
227 resolve_int_scalar(func, args[param_idx])
228 }
229 _ => None,
230 }
231 }
232
233 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
234 enum LoopDir {
235 Ascending,
236 Descending,
237 }
238
239 fn loop_bound_const(func: &Function, header: BlockId, index: ValueId) -> Option<(i64, LoopDir)> {
240 for inst in &func.block(header).insts {
241 let InstKind::ICmp(op, lhs, rhs) = &inst.kind else {
242 continue;
243 };
244 match op {
245 CmpOp::Le => {
246 if *lhs == index {
247 return resolve_int_scalar(func, *rhs).map(|bound| (bound, LoopDir::Ascending));
248 }
249 if *rhs == index {
250 return resolve_int_scalar(func, *lhs)
251 .map(|bound| (bound, LoopDir::Descending));
252 }
253 }
254 CmpOp::Ge => {
255 if *lhs == index {
256 return resolve_int_scalar(func, *rhs)
257 .map(|bound| (bound, LoopDir::Descending));
258 }
259 if *rhs == index {
260 return resolve_int_scalar(func, *lhs).map(|bound| (bound, LoopDir::Ascending));
261 }
262 }
263 _ => {}
264 }
265 }
266 None
267 }
268
269 fn loop_step_const(
270 func: &Function,
271 lp: &crate::ir::walk::NaturalLoop,
272 param_idx: usize,
273 index: ValueId,
274 ) -> Option<i64> {
275 let mut step: Option<i64> = None;
276 for &latch in &lp.latches {
277 let next = edge_arg_value(func.block(latch).terminator.as_ref()?, lp.header, param_idx)?;
278 let latch_step = update_step_const(func, index, next)?;
279 if latch_step == 0 {
280 return None;
281 }
282 match step {
283 Some(prev) if prev != latch_step => return None,
284 Some(_) => {}
285 None => step = Some(latch_step),
286 }
287 }
288 step
289 }
290
291 fn edge_arg_value(term: &Terminator, target: BlockId, param_idx: usize) -> Option<ValueId> {
292 match term {
293 Terminator::Branch(dest, args) if *dest == target => args.get(param_idx).copied(),
294 Terminator::CondBranch {
295 true_dest,
296 true_args,
297 false_dest,
298 false_args,
299 ..
300 } => {
301 if *true_dest == target {
302 true_args.get(param_idx).copied()
303 } else if *false_dest == target {
304 false_args.get(param_idx).copied()
305 } else {
306 None
307 }
308 }
309 _ => None,
310 }
311 }
312
313 fn update_step_const(func: &Function, index: ValueId, next: ValueId) -> Option<i64> {
314 let next = strip_int_casts(func, next);
315 if next == index {
316 return Some(0);
317 }
318
319 let kind = find_inst_kind(func, next)?;
320 match kind {
321 InstKind::IAdd(lhs, rhs) => {
322 if *lhs == index {
323 resolve_int_scalar(func, *rhs)
324 } else if *rhs == index {
325 resolve_int_scalar(func, *lhs)
326 } else {
327 None
328 }
329 }
330 InstKind::ISub(lhs, rhs) if *lhs == index => {
331 resolve_int_scalar(func, *rhs).map(|step| -step)
332 }
333 _ => None,
334 }
335 }
336
337 fn resolve_int_scalar(func: &Function, value: ValueId) -> Option<i64> {
338 let kind = find_inst_kind(func, value)?;
339 match kind {
340 InstKind::ConstInt(v, _) => i64::try_from(*v).ok(),
341 InstKind::IntExtend(src, _, _) | InstKind::IntTrunc(src, _) => {
342 resolve_int_scalar(func, *src)
343 }
344 _ => resolve_const_int(func, value),
345 }
346 }
347
348 fn strip_int_casts(func: &Function, mut value: ValueId) -> ValueId {
349 loop {
350 let Some(kind) = find_inst_kind(func, value) else {
351 return value;
352 };
353 match kind {
354 InstKind::IntExtend(src, _, _) | InstKind::IntTrunc(src, _) => value = *src,
355 _ => return value,
356 }
357 }
358 }
359
360 fn find_inst_kind(func: &Function, value: ValueId) -> Option<&InstKind> {
361 for block in &func.blocks {
362 for inst in &block.insts {
363 if inst.id == value {
364 return Some(&inst.kind);
365 }
366 }
367 }
368 None
369 }
370
371 #[cfg(test)]
372 mod tests {
373 use super::*;
374 use crate::ir::types::{IntWidth, IrType};
375 use crate::opt::pass::Pass;
376
377 #[test]
378 fn bce_no_op_without_bounds_checks() {
379 let mut m = Module::new("test".into());
380 let mut f = Function::new("test".into(), vec![], IrType::Void);
381 f.block_mut(f.entry).terminator = Some(Terminator::Return(None));
382 m.add_function(f);
383 let pass = Bce;
384 assert!(!pass.run(&mut m), "no CheckBounds → no elimination");
385 }
386
387 #[test]
388 fn bce_removes_constant_in_bounds() {
389 let mut m = Module::new("test".into());
390 let mut f = Function::new("test".into(), vec![], IrType::Void);
391 let span = crate::lexer::Span {
392 file_id: 0,
393 start: crate::lexer::Position { line: 0, col: 0 },
394 end: crate::lexer::Position { line: 0, col: 0 },
395 };
396
397 // index = 3, lower = 1, upper = 10 → safe
398 let idx = f.next_value_id();
399 f.register_type(idx, IrType::Int(IntWidth::I32));
400 f.block_mut(f.entry).insts.push(Inst {
401 id: idx,
402 ty: IrType::Int(IntWidth::I32),
403 span,
404 kind: InstKind::ConstInt(3, IntWidth::I32),
405 });
406 let lo = f.next_value_id();
407 f.register_type(lo, IrType::Int(IntWidth::I32));
408 f.block_mut(f.entry).insts.push(Inst {
409 id: lo,
410 ty: IrType::Int(IntWidth::I32),
411 span,
412 kind: InstKind::ConstInt(1, IntWidth::I32),
413 });
414 let hi = f.next_value_id();
415 f.register_type(hi, IrType::Int(IntWidth::I32));
416 f.block_mut(f.entry).insts.push(Inst {
417 id: hi,
418 ty: IrType::Int(IntWidth::I32),
419 span,
420 kind: InstKind::ConstInt(10, IntWidth::I32),
421 });
422 let check = f.next_value_id();
423 f.register_type(check, IrType::Void);
424 f.block_mut(f.entry).insts.push(Inst {
425 id: check,
426 ty: IrType::Void,
427 span,
428 kind: InstKind::RuntimeCall(RuntimeFunc::CheckBounds, vec![idx, lo, hi]),
429 });
430 f.block_mut(f.entry).terminator = Some(Terminator::Return(None));
431 m.add_function(f);
432
433 let pass = Bce;
434 let changed = pass.run(&mut m);
435 assert!(changed, "constant 3 in [1,10] should be eliminated");
436
437 // Verify the CheckBounds call was removed.
438 let has_check = m.functions[0].blocks[0]
439 .insts
440 .iter()
441 .any(|i| matches!(i.kind, InstKind::RuntimeCall(RuntimeFunc::CheckBounds, _)));
442 assert!(!has_check, "CheckBounds should be removed");
443 }
444
445 #[test]
446 fn bce_removes_canonical_loop_iv_check() {
447 let mut m = Module::new("test".into());
448 let mut f = Function::new("test".into(), vec![], IrType::Void);
449 let span = crate::lexer::Span {
450 file_id: 0,
451 start: crate::lexer::Position { line: 0, col: 0 },
452 end: crate::lexer::Position { line: 0, col: 0 },
453 };
454
455 let header = f.create_block("header");
456 let body = f.create_block("body");
457 let exit = f.create_block("exit");
458
459 let init = f.next_value_id();
460 f.register_type(init, IrType::Int(IntWidth::I32));
461 f.block_mut(f.entry).insts.push(Inst {
462 id: init,
463 ty: IrType::Int(IntWidth::I32),
464 span,
465 kind: InstKind::ConstInt(1, IntWidth::I32),
466 });
467 let zero = f.next_value_id();
468 f.register_type(zero, IrType::Int(IntWidth::I32));
469 f.block_mut(f.entry).insts.push(Inst {
470 id: zero,
471 ty: IrType::Int(IntWidth::I32),
472 span,
473 kind: InstKind::ConstInt(0, IntWidth::I32),
474 });
475 f.block_mut(f.entry).terminator = Some(Terminator::Branch(header, vec![init, zero]));
476
477 let iv = f.next_value_id();
478 f.register_type(iv, IrType::Int(IntWidth::I32));
479 let sum = f.next_value_id();
480 f.register_type(sum, IrType::Int(IntWidth::I32));
481 f.block_mut(header).params.push(BlockParam {
482 id: iv,
483 ty: IrType::Int(IntWidth::I32),
484 });
485 f.block_mut(header).params.push(BlockParam {
486 id: sum,
487 ty: IrType::Int(IntWidth::I32),
488 });
489
490 let bound = f.next_value_id();
491 f.register_type(bound, IrType::Int(IntWidth::I32));
492 f.block_mut(header).insts.push(Inst {
493 id: bound,
494 ty: IrType::Int(IntWidth::I32),
495 span,
496 kind: InstKind::ConstInt(4, IntWidth::I32),
497 });
498 let cond = f.next_value_id();
499 f.register_type(cond, IrType::Bool);
500 f.block_mut(header).insts.push(Inst {
501 id: cond,
502 ty: IrType::Bool,
503 span,
504 kind: InstKind::ICmp(CmpOp::Le, iv, bound),
505 });
506 f.block_mut(header).terminator = Some(Terminator::CondBranch {
507 cond,
508 true_dest: body,
509 true_args: vec![],
510 false_dest: exit,
511 false_args: vec![],
512 });
513
514 let iv64 = f.next_value_id();
515 f.register_type(iv64, IrType::Int(IntWidth::I64));
516 f.block_mut(body).insts.push(Inst {
517 id: iv64,
518 ty: IrType::Int(IntWidth::I64),
519 span,
520 kind: InstKind::IntExtend(iv, IntWidth::I64, true),
521 });
522 let lo = f.next_value_id();
523 f.register_type(lo, IrType::Int(IntWidth::I64));
524 f.block_mut(body).insts.push(Inst {
525 id: lo,
526 ty: IrType::Int(IntWidth::I64),
527 span,
528 kind: InstKind::ConstInt(1, IntWidth::I64),
529 });
530 let hi = f.next_value_id();
531 f.register_type(hi, IrType::Int(IntWidth::I64));
532 f.block_mut(body).insts.push(Inst {
533 id: hi,
534 ty: IrType::Int(IntWidth::I64),
535 span,
536 kind: InstKind::ConstInt(4, IntWidth::I64),
537 });
538 let check = f.next_value_id();
539 f.register_type(check, IrType::Void);
540 f.block_mut(body).insts.push(Inst {
541 id: check,
542 ty: IrType::Void,
543 span,
544 kind: InstKind::RuntimeCall(RuntimeFunc::CheckBounds, vec![iv64, lo, hi]),
545 });
546 let step = f.next_value_id();
547 f.register_type(step, IrType::Int(IntWidth::I32));
548 f.block_mut(body).insts.push(Inst {
549 id: step,
550 ty: IrType::Int(IntWidth::I32),
551 span,
552 kind: InstKind::ConstInt(1, IntWidth::I32),
553 });
554 let next_iv = f.next_value_id();
555 f.register_type(next_iv, IrType::Int(IntWidth::I32));
556 f.block_mut(body).insts.push(Inst {
557 id: next_iv,
558 ty: IrType::Int(IntWidth::I32),
559 span,
560 kind: InstKind::IAdd(iv, step),
561 });
562 let next_sum = f.next_value_id();
563 f.register_type(next_sum, IrType::Int(IntWidth::I32));
564 f.block_mut(body).insts.push(Inst {
565 id: next_sum,
566 ty: IrType::Int(IntWidth::I32),
567 span,
568 kind: InstKind::IAdd(sum, step),
569 });
570 f.block_mut(body).terminator = Some(Terminator::Branch(header, vec![next_iv, next_sum]));
571 f.block_mut(exit).terminator = Some(Terminator::Return(None));
572 m.add_function(f);
573
574 let pass = Bce;
575 assert!(
576 pass.run(&mut m),
577 "canonical counted-loop bounds check should be removed"
578 );
579 let has_check = m.functions[0].block(body).insts.iter().any(|inst| {
580 matches!(
581 inst.kind,
582 InstKind::RuntimeCall(RuntimeFunc::CheckBounds, _)
583 )
584 });
585 assert!(!has_check, "loop body should no longer contain CheckBounds");
586 }
587
588 #[test]
589 fn bce_keeps_loop_check_when_bounds_are_tighter_than_trip_range() {
590 let mut m = Module::new("test".into());
591 let mut f = Function::new("test".into(), vec![], IrType::Void);
592 let span = crate::lexer::Span {
593 file_id: 0,
594 start: crate::lexer::Position { line: 0, col: 0 },
595 end: crate::lexer::Position { line: 0, col: 0 },
596 };
597
598 let header = f.create_block("header");
599 let body = f.create_block("body");
600 let exit = f.create_block("exit");
601
602 let init = f.next_value_id();
603 f.register_type(init, IrType::Int(IntWidth::I32));
604 f.block_mut(f.entry).insts.push(Inst {
605 id: init,
606 ty: IrType::Int(IntWidth::I32),
607 span,
608 kind: InstKind::ConstInt(1, IntWidth::I32),
609 });
610 let zero = f.next_value_id();
611 f.register_type(zero, IrType::Int(IntWidth::I32));
612 f.block_mut(f.entry).insts.push(Inst {
613 id: zero,
614 ty: IrType::Int(IntWidth::I32),
615 span,
616 kind: InstKind::ConstInt(0, IntWidth::I32),
617 });
618 f.block_mut(f.entry).terminator = Some(Terminator::Branch(header, vec![init, zero]));
619
620 let iv = f.next_value_id();
621 f.register_type(iv, IrType::Int(IntWidth::I32));
622 let sum = f.next_value_id();
623 f.register_type(sum, IrType::Int(IntWidth::I32));
624 f.block_mut(header).params.push(BlockParam {
625 id: iv,
626 ty: IrType::Int(IntWidth::I32),
627 });
628 f.block_mut(header).params.push(BlockParam {
629 id: sum,
630 ty: IrType::Int(IntWidth::I32),
631 });
632
633 let bound = f.next_value_id();
634 f.register_type(bound, IrType::Int(IntWidth::I32));
635 f.block_mut(header).insts.push(Inst {
636 id: bound,
637 ty: IrType::Int(IntWidth::I32),
638 span,
639 kind: InstKind::ConstInt(4, IntWidth::I32),
640 });
641 let cond = f.next_value_id();
642 f.register_type(cond, IrType::Bool);
643 f.block_mut(header).insts.push(Inst {
644 id: cond,
645 ty: IrType::Bool,
646 span,
647 kind: InstKind::ICmp(CmpOp::Le, iv, bound),
648 });
649 f.block_mut(header).terminator = Some(Terminator::CondBranch {
650 cond,
651 true_dest: body,
652 true_args: vec![],
653 false_dest: exit,
654 false_args: vec![],
655 });
656
657 let iv64 = f.next_value_id();
658 f.register_type(iv64, IrType::Int(IntWidth::I64));
659 f.block_mut(body).insts.push(Inst {
660 id: iv64,
661 ty: IrType::Int(IntWidth::I64),
662 span,
663 kind: InstKind::IntExtend(iv, IntWidth::I64, true),
664 });
665 let lo = f.next_value_id();
666 f.register_type(lo, IrType::Int(IntWidth::I64));
667 f.block_mut(body).insts.push(Inst {
668 id: lo,
669 ty: IrType::Int(IntWidth::I64),
670 span,
671 kind: InstKind::ConstInt(1, IntWidth::I64),
672 });
673 let hi = f.next_value_id();
674 f.register_type(hi, IrType::Int(IntWidth::I64));
675 f.block_mut(body).insts.push(Inst {
676 id: hi,
677 ty: IrType::Int(IntWidth::I64),
678 span,
679 kind: InstKind::ConstInt(3, IntWidth::I64),
680 });
681 let check = f.next_value_id();
682 f.register_type(check, IrType::Void);
683 f.block_mut(body).insts.push(Inst {
684 id: check,
685 ty: IrType::Void,
686 span,
687 kind: InstKind::RuntimeCall(RuntimeFunc::CheckBounds, vec![iv64, lo, hi]),
688 });
689 let step = f.next_value_id();
690 f.register_type(step, IrType::Int(IntWidth::I32));
691 f.block_mut(body).insts.push(Inst {
692 id: step,
693 ty: IrType::Int(IntWidth::I32),
694 span,
695 kind: InstKind::ConstInt(1, IntWidth::I32),
696 });
697 let next_iv = f.next_value_id();
698 f.register_type(next_iv, IrType::Int(IntWidth::I32));
699 f.block_mut(body).insts.push(Inst {
700 id: next_iv,
701 ty: IrType::Int(IntWidth::I32),
702 span,
703 kind: InstKind::IAdd(iv, step),
704 });
705 let next_sum = f.next_value_id();
706 f.register_type(next_sum, IrType::Int(IntWidth::I32));
707 f.block_mut(body).insts.push(Inst {
708 id: next_sum,
709 ty: IrType::Int(IntWidth::I32),
710 span,
711 kind: InstKind::IAdd(sum, step),
712 });
713 f.block_mut(body).terminator = Some(Terminator::Branch(header, vec![next_iv, next_sum]));
714 f.block_mut(exit).terminator = Some(Terminator::Return(None));
715 m.add_function(f);
716
717 let pass = Bce;
718 assert!(
719 !pass.run(&mut m),
720 "loop trip range 1..4 exceeds checked upper bound 3, so CheckBounds must remain"
721 );
722 }
723
724 #[test]
725 fn bce_removes_loop_iv_plus_constant_check() {
726 let mut m = Module::new("test".into());
727 let mut f = Function::new("test".into(), vec![], IrType::Void);
728 let span = crate::lexer::Span {
729 file_id: 0,
730 start: crate::lexer::Position { line: 0, col: 0 },
731 end: crate::lexer::Position { line: 0, col: 0 },
732 };
733
734 let header = f.create_block("header");
735 let body = f.create_block("body");
736 let exit = f.create_block("exit");
737
738 let init = f.next_value_id();
739 f.register_type(init, IrType::Int(IntWidth::I32));
740 f.block_mut(f.entry).insts.push(Inst {
741 id: init,
742 ty: IrType::Int(IntWidth::I32),
743 span,
744 kind: InstKind::ConstInt(2, IntWidth::I32),
745 });
746 f.block_mut(f.entry).terminator = Some(Terminator::Branch(header, vec![init]));
747
748 let iv = f.next_value_id();
749 f.register_type(iv, IrType::Int(IntWidth::I32));
750 f.block_mut(header).params.push(BlockParam {
751 id: iv,
752 ty: IrType::Int(IntWidth::I32),
753 });
754
755 let bound = f.next_value_id();
756 f.register_type(bound, IrType::Int(IntWidth::I32));
757 f.block_mut(header).insts.push(Inst {
758 id: bound,
759 ty: IrType::Int(IntWidth::I32),
760 span,
761 kind: InstKind::ConstInt(7, IntWidth::I32),
762 });
763 let cond = f.next_value_id();
764 f.register_type(cond, IrType::Bool);
765 f.block_mut(header).insts.push(Inst {
766 id: cond,
767 ty: IrType::Bool,
768 span,
769 kind: InstKind::ICmp(CmpOp::Le, iv, bound),
770 });
771 f.block_mut(header).terminator = Some(Terminator::CondBranch {
772 cond,
773 true_dest: body,
774 true_args: vec![],
775 false_dest: exit,
776 false_args: vec![],
777 });
778
779 let one = f.next_value_id();
780 f.register_type(one, IrType::Int(IntWidth::I32));
781 f.block_mut(body).insts.push(Inst {
782 id: one,
783 ty: IrType::Int(IntWidth::I32),
784 span,
785 kind: InstKind::ConstInt(1, IntWidth::I32),
786 });
787 let plus = f.next_value_id();
788 f.register_type(plus, IrType::Int(IntWidth::I32));
789 f.block_mut(body).insts.push(Inst {
790 id: plus,
791 ty: IrType::Int(IntWidth::I32),
792 span,
793 kind: InstKind::IAdd(iv, one),
794 });
795 let plus64 = f.next_value_id();
796 f.register_type(plus64, IrType::Int(IntWidth::I64));
797 f.block_mut(body).insts.push(Inst {
798 id: plus64,
799 ty: IrType::Int(IntWidth::I64),
800 span,
801 kind: InstKind::IntExtend(plus, IntWidth::I64, true),
802 });
803 let lo = f.next_value_id();
804 f.register_type(lo, IrType::Int(IntWidth::I64));
805 f.block_mut(body).insts.push(Inst {
806 id: lo,
807 ty: IrType::Int(IntWidth::I64),
808 span,
809 kind: InstKind::ConstInt(1, IntWidth::I64),
810 });
811 let hi = f.next_value_id();
812 f.register_type(hi, IrType::Int(IntWidth::I64));
813 f.block_mut(body).insts.push(Inst {
814 id: hi,
815 ty: IrType::Int(IntWidth::I64),
816 span,
817 kind: InstKind::ConstInt(8, IntWidth::I64),
818 });
819 let check = f.next_value_id();
820 f.register_type(check, IrType::Void);
821 f.block_mut(body).insts.push(Inst {
822 id: check,
823 ty: IrType::Void,
824 span,
825 kind: InstKind::RuntimeCall(RuntimeFunc::CheckBounds, vec![plus64, lo, hi]),
826 });
827 let next_iv = f.next_value_id();
828 f.register_type(next_iv, IrType::Int(IntWidth::I32));
829 f.block_mut(body).insts.push(Inst {
830 id: next_iv,
831 ty: IrType::Int(IntWidth::I32),
832 span,
833 kind: InstKind::IAdd(iv, one),
834 });
835 f.block_mut(body).terminator = Some(Terminator::Branch(header, vec![next_iv]));
836 f.block_mut(exit).terminator = Some(Terminator::Return(None));
837 m.add_function(f);
838
839 let pass = Bce;
840 assert!(
841 pass.run(&mut m),
842 "iv+1 check should be removed for trip range 2..7 and checked bounds 1..8"
843 );
844 let has_check = m.functions[0].block(body).insts.iter().any(|inst| {
845 matches!(
846 inst.kind,
847 InstKind::RuntimeCall(RuntimeFunc::CheckBounds, _)
848 )
849 });
850 assert!(!has_check, "loop body should no longer contain CheckBounds");
851 }
852
853 #[test]
854 fn bce_removes_loop_iv_minus_constant_check() {
855 let mut m = Module::new("test".into());
856 let mut f = Function::new("test".into(), vec![], IrType::Void);
857 let span = crate::lexer::Span {
858 file_id: 0,
859 start: crate::lexer::Position { line: 0, col: 0 },
860 end: crate::lexer::Position { line: 0, col: 0 },
861 };
862
863 let header = f.create_block("header");
864 let body = f.create_block("body");
865 let exit = f.create_block("exit");
866
867 let init = f.next_value_id();
868 f.register_type(init, IrType::Int(IntWidth::I32));
869 f.block_mut(f.entry).insts.push(Inst {
870 id: init,
871 ty: IrType::Int(IntWidth::I32),
872 span,
873 kind: InstKind::ConstInt(2, IntWidth::I32),
874 });
875 f.block_mut(f.entry).terminator = Some(Terminator::Branch(header, vec![init]));
876
877 let iv = f.next_value_id();
878 f.register_type(iv, IrType::Int(IntWidth::I32));
879 f.block_mut(header).params.push(BlockParam {
880 id: iv,
881 ty: IrType::Int(IntWidth::I32),
882 });
883
884 let bound = f.next_value_id();
885 f.register_type(bound, IrType::Int(IntWidth::I32));
886 f.block_mut(header).insts.push(Inst {
887 id: bound,
888 ty: IrType::Int(IntWidth::I32),
889 span,
890 kind: InstKind::ConstInt(7, IntWidth::I32),
891 });
892 let cond = f.next_value_id();
893 f.register_type(cond, IrType::Bool);
894 f.block_mut(header).insts.push(Inst {
895 id: cond,
896 ty: IrType::Bool,
897 span,
898 kind: InstKind::ICmp(CmpOp::Le, iv, bound),
899 });
900 f.block_mut(header).terminator = Some(Terminator::CondBranch {
901 cond,
902 true_dest: body,
903 true_args: vec![],
904 false_dest: exit,
905 false_args: vec![],
906 });
907
908 let one = f.next_value_id();
909 f.register_type(one, IrType::Int(IntWidth::I32));
910 f.block_mut(body).insts.push(Inst {
911 id: one,
912 ty: IrType::Int(IntWidth::I32),
913 span,
914 kind: InstKind::ConstInt(1, IntWidth::I32),
915 });
916 let minus = f.next_value_id();
917 f.register_type(minus, IrType::Int(IntWidth::I32));
918 f.block_mut(body).insts.push(Inst {
919 id: minus,
920 ty: IrType::Int(IntWidth::I32),
921 span,
922 kind: InstKind::ISub(iv, one),
923 });
924 let minus64 = f.next_value_id();
925 f.register_type(minus64, IrType::Int(IntWidth::I64));
926 f.block_mut(body).insts.push(Inst {
927 id: minus64,
928 ty: IrType::Int(IntWidth::I64),
929 span,
930 kind: InstKind::IntExtend(minus, IntWidth::I64, true),
931 });
932 let lo = f.next_value_id();
933 f.register_type(lo, IrType::Int(IntWidth::I64));
934 f.block_mut(body).insts.push(Inst {
935 id: lo,
936 ty: IrType::Int(IntWidth::I64),
937 span,
938 kind: InstKind::ConstInt(1, IntWidth::I64),
939 });
940 let hi = f.next_value_id();
941 f.register_type(hi, IrType::Int(IntWidth::I64));
942 f.block_mut(body).insts.push(Inst {
943 id: hi,
944 ty: IrType::Int(IntWidth::I64),
945 span,
946 kind: InstKind::ConstInt(8, IntWidth::I64),
947 });
948 let check = f.next_value_id();
949 f.register_type(check, IrType::Void);
950 f.block_mut(body).insts.push(Inst {
951 id: check,
952 ty: IrType::Void,
953 span,
954 kind: InstKind::RuntimeCall(RuntimeFunc::CheckBounds, vec![minus64, lo, hi]),
955 });
956 let next_iv = f.next_value_id();
957 f.register_type(next_iv, IrType::Int(IntWidth::I32));
958 f.block_mut(body).insts.push(Inst {
959 id: next_iv,
960 ty: IrType::Int(IntWidth::I32),
961 span,
962 kind: InstKind::IAdd(iv, one),
963 });
964 f.block_mut(body).terminator = Some(Terminator::Branch(header, vec![next_iv]));
965 f.block_mut(exit).terminator = Some(Terminator::Return(None));
966 m.add_function(f);
967
968 let pass = Bce;
969 assert!(
970 pass.run(&mut m),
971 "iv-1 check should be removed for trip range 2..7 and checked bounds 1..8"
972 );
973 let has_check = m.functions[0].block(body).insts.iter().any(|inst| {
974 matches!(
975 inst.kind,
976 InstKind::RuntimeCall(RuntimeFunc::CheckBounds, _)
977 )
978 });
979 assert!(!has_check, "loop body should no longer contain CheckBounds");
980 }
981
982 #[test]
983 fn bce_keeps_loop_iv_plus_constant_check_when_offset_exceeds_upper_bound() {
984 let mut m = Module::new("test".into());
985 let mut f = Function::new("test".into(), vec![], IrType::Void);
986 let span = crate::lexer::Span {
987 file_id: 0,
988 start: crate::lexer::Position { line: 0, col: 0 },
989 end: crate::lexer::Position { line: 0, col: 0 },
990 };
991
992 let header = f.create_block("header");
993 let body = f.create_block("body");
994 let exit = f.create_block("exit");
995
996 let init = f.next_value_id();
997 f.register_type(init, IrType::Int(IntWidth::I32));
998 f.block_mut(f.entry).insts.push(Inst {
999 id: init,
1000 ty: IrType::Int(IntWidth::I32),
1001 span,
1002 kind: InstKind::ConstInt(2, IntWidth::I32),
1003 });
1004 f.block_mut(f.entry).terminator = Some(Terminator::Branch(header, vec![init]));
1005
1006 let iv = f.next_value_id();
1007 f.register_type(iv, IrType::Int(IntWidth::I32));
1008 f.block_mut(header).params.push(BlockParam {
1009 id: iv,
1010 ty: IrType::Int(IntWidth::I32),
1011 });
1012
1013 let bound = f.next_value_id();
1014 f.register_type(bound, IrType::Int(IntWidth::I32));
1015 f.block_mut(header).insts.push(Inst {
1016 id: bound,
1017 ty: IrType::Int(IntWidth::I32),
1018 span,
1019 kind: InstKind::ConstInt(7, IntWidth::I32),
1020 });
1021 let cond = f.next_value_id();
1022 f.register_type(cond, IrType::Bool);
1023 f.block_mut(header).insts.push(Inst {
1024 id: cond,
1025 ty: IrType::Bool,
1026 span,
1027 kind: InstKind::ICmp(CmpOp::Le, iv, bound),
1028 });
1029 f.block_mut(header).terminator = Some(Terminator::CondBranch {
1030 cond,
1031 true_dest: body,
1032 true_args: vec![],
1033 false_dest: exit,
1034 false_args: vec![],
1035 });
1036
1037 let two = f.next_value_id();
1038 f.register_type(two, IrType::Int(IntWidth::I32));
1039 f.block_mut(body).insts.push(Inst {
1040 id: two,
1041 ty: IrType::Int(IntWidth::I32),
1042 span,
1043 kind: InstKind::ConstInt(2, IntWidth::I32),
1044 });
1045 let plus = f.next_value_id();
1046 f.register_type(plus, IrType::Int(IntWidth::I32));
1047 f.block_mut(body).insts.push(Inst {
1048 id: plus,
1049 ty: IrType::Int(IntWidth::I32),
1050 span,
1051 kind: InstKind::IAdd(iv, two),
1052 });
1053 let plus64 = f.next_value_id();
1054 f.register_type(plus64, IrType::Int(IntWidth::I64));
1055 f.block_mut(body).insts.push(Inst {
1056 id: plus64,
1057 ty: IrType::Int(IntWidth::I64),
1058 span,
1059 kind: InstKind::IntExtend(plus, IntWidth::I64, true),
1060 });
1061 let lo = f.next_value_id();
1062 f.register_type(lo, IrType::Int(IntWidth::I64));
1063 f.block_mut(body).insts.push(Inst {
1064 id: lo,
1065 ty: IrType::Int(IntWidth::I64),
1066 span,
1067 kind: InstKind::ConstInt(1, IntWidth::I64),
1068 });
1069 let hi = f.next_value_id();
1070 f.register_type(hi, IrType::Int(IntWidth::I64));
1071 f.block_mut(body).insts.push(Inst {
1072 id: hi,
1073 ty: IrType::Int(IntWidth::I64),
1074 span,
1075 kind: InstKind::ConstInt(8, IntWidth::I64),
1076 });
1077 let check = f.next_value_id();
1078 f.register_type(check, IrType::Void);
1079 f.block_mut(body).insts.push(Inst {
1080 id: check,
1081 ty: IrType::Void,
1082 span,
1083 kind: InstKind::RuntimeCall(RuntimeFunc::CheckBounds, vec![plus64, lo, hi]),
1084 });
1085 let one = f.next_value_id();
1086 f.register_type(one, IrType::Int(IntWidth::I32));
1087 f.block_mut(body).insts.push(Inst {
1088 id: one,
1089 ty: IrType::Int(IntWidth::I32),
1090 span,
1091 kind: InstKind::ConstInt(1, IntWidth::I32),
1092 });
1093 let next_iv = f.next_value_id();
1094 f.register_type(next_iv, IrType::Int(IntWidth::I32));
1095 f.block_mut(body).insts.push(Inst {
1096 id: next_iv,
1097 ty: IrType::Int(IntWidth::I32),
1098 span,
1099 kind: InstKind::IAdd(iv, one),
1100 });
1101 f.block_mut(body).terminator = Some(Terminator::Branch(header, vec![next_iv]));
1102 f.block_mut(exit).terminator = Some(Terminator::Return(None));
1103 m.add_function(f);
1104
1105 let pass = Bce;
1106 assert!(
1107 !pass.run(&mut m),
1108 "iv+2 check must remain when trip range 2..7 would access 9 beyond checked upper bound 8"
1109 );
1110 }
1111
1112 #[test]
1113 fn bce_removes_loop_iv_plus_constant_check_with_large_step() {
1114 let mut m = Module::new("test".into());
1115 let mut f = Function::new("test".into(), vec![], IrType::Void);
1116 let span = crate::lexer::Span {
1117 file_id: 0,
1118 start: crate::lexer::Position { line: 0, col: 0 },
1119 end: crate::lexer::Position { line: 0, col: 0 },
1120 };
1121
1122 let header = f.create_block("header");
1123 let body = f.create_block("body");
1124 let exit = f.create_block("exit");
1125
1126 let init = f.next_value_id();
1127 f.register_type(init, IrType::Int(IntWidth::I32));
1128 f.block_mut(f.entry).insts.push(Inst {
1129 id: init,
1130 ty: IrType::Int(IntWidth::I32),
1131 span,
1132 kind: InstKind::ConstInt(5, IntWidth::I32),
1133 });
1134 f.block_mut(f.entry).terminator = Some(Terminator::Branch(header, vec![init]));
1135
1136 let iv = f.next_value_id();
1137 f.register_type(iv, IrType::Int(IntWidth::I32));
1138 f.block_mut(header).params.push(BlockParam {
1139 id: iv,
1140 ty: IrType::Int(IntWidth::I32),
1141 });
1142
1143 let bound = f.next_value_id();
1144 f.register_type(bound, IrType::Int(IntWidth::I32));
1145 f.block_mut(header).insts.push(Inst {
1146 id: bound,
1147 ty: IrType::Int(IntWidth::I32),
1148 span,
1149 kind: InstKind::ConstInt(10, IntWidth::I32),
1150 });
1151 let cond = f.next_value_id();
1152 f.register_type(cond, IrType::Bool);
1153 f.block_mut(header).insts.push(Inst {
1154 id: cond,
1155 ty: IrType::Bool,
1156 span,
1157 kind: InstKind::ICmp(CmpOp::Le, iv, bound),
1158 });
1159 f.block_mut(header).terminator = Some(Terminator::CondBranch {
1160 cond,
1161 true_dest: body,
1162 true_args: vec![],
1163 false_dest: exit,
1164 false_args: vec![],
1165 });
1166
1167 let five = f.next_value_id();
1168 f.register_type(five, IrType::Int(IntWidth::I32));
1169 f.block_mut(body).insts.push(Inst {
1170 id: five,
1171 ty: IrType::Int(IntWidth::I32),
1172 span,
1173 kind: InstKind::ConstInt(5, IntWidth::I32),
1174 });
1175 let plus = f.next_value_id();
1176 f.register_type(plus, IrType::Int(IntWidth::I32));
1177 f.block_mut(body).insts.push(Inst {
1178 id: plus,
1179 ty: IrType::Int(IntWidth::I32),
1180 span,
1181 kind: InstKind::IAdd(iv, five),
1182 });
1183 let plus64 = f.next_value_id();
1184 f.register_type(plus64, IrType::Int(IntWidth::I64));
1185 f.block_mut(body).insts.push(Inst {
1186 id: plus64,
1187 ty: IrType::Int(IntWidth::I64),
1188 span,
1189 kind: InstKind::IntExtend(plus, IntWidth::I64, true),
1190 });
1191 let lo = f.next_value_id();
1192 f.register_type(lo, IrType::Int(IntWidth::I64));
1193 f.block_mut(body).insts.push(Inst {
1194 id: lo,
1195 ty: IrType::Int(IntWidth::I64),
1196 span,
1197 kind: InstKind::ConstInt(1, IntWidth::I64),
1198 });
1199 let hi = f.next_value_id();
1200 f.register_type(hi, IrType::Int(IntWidth::I64));
1201 f.block_mut(body).insts.push(Inst {
1202 id: hi,
1203 ty: IrType::Int(IntWidth::I64),
1204 span,
1205 kind: InstKind::ConstInt(10, IntWidth::I64),
1206 });
1207 let check = f.next_value_id();
1208 f.register_type(check, IrType::Void);
1209 f.block_mut(body).insts.push(Inst {
1210 id: check,
1211 ty: IrType::Void,
1212 span,
1213 kind: InstKind::RuntimeCall(RuntimeFunc::CheckBounds, vec![plus64, lo, hi]),
1214 });
1215 let step = f.next_value_id();
1216 f.register_type(step, IrType::Int(IntWidth::I32));
1217 f.block_mut(body).insts.push(Inst {
1218 id: step,
1219 ty: IrType::Int(IntWidth::I32),
1220 span,
1221 kind: InstKind::ConstInt(6, IntWidth::I32),
1222 });
1223 let next_iv = f.next_value_id();
1224 f.register_type(next_iv, IrType::Int(IntWidth::I32));
1225 f.block_mut(body).insts.push(Inst {
1226 id: next_iv,
1227 ty: IrType::Int(IntWidth::I32),
1228 span,
1229 kind: InstKind::IAdd(iv, step),
1230 });
1231 f.block_mut(body).terminator = Some(Terminator::Branch(header, vec![next_iv]));
1232 f.block_mut(exit).terminator = Some(Terminator::Return(None));
1233 m.add_function(f);
1234
1235 let pass = Bce;
1236 assert!(
1237 pass.run(&mut m),
1238 "iv+5 check should be removed when the trip sequence is only 5 and checked bounds are 1..10"
1239 );
1240 let has_check = m.functions[0].block(body).insts.iter().any(|inst| {
1241 matches!(
1242 inst.kind,
1243 InstKind::RuntimeCall(RuntimeFunc::CheckBounds, _)
1244 )
1245 });
1246 assert!(!has_check, "loop body should no longer contain CheckBounds");
1247 }
1248
1249 #[test]
1250 fn loop_index_decomposes_constant_multiplied_iv() {
1251 let mut f = Function::new("test".into(), vec![], IrType::Void);
1252 let span = crate::lexer::Span {
1253 file_id: 0,
1254 start: crate::lexer::Position { line: 0, col: 0 },
1255 end: crate::lexer::Position { line: 0, col: 0 },
1256 };
1257
1258 // Synthesise: i = next_value(); 2*i; (2*i) + 3 — verify the
1259 // decomposition (base, scale, offset) recovers (i, 2, 3).
1260 let entry = f.entry;
1261 let i = f.next_value_id();
1262 f.register_type(i, IrType::Int(IntWidth::I32));
1263 let two = f.next_value_id();
1264 f.register_type(two, IrType::Int(IntWidth::I32));
1265 f.block_mut(entry).insts.push(Inst {
1266 id: two,
1267 ty: IrType::Int(IntWidth::I32),
1268 span,
1269 kind: InstKind::ConstInt(2, IntWidth::I32),
1270 });
1271 let two_i = f.next_value_id();
1272 f.register_type(two_i, IrType::Int(IntWidth::I32));
1273 f.block_mut(entry).insts.push(Inst {
1274 id: two_i,
1275 ty: IrType::Int(IntWidth::I32),
1276 span,
1277 kind: InstKind::IMul(two, i),
1278 });
1279 let three = f.next_value_id();
1280 f.register_type(three, IrType::Int(IntWidth::I32));
1281 f.block_mut(entry).insts.push(Inst {
1282 id: three,
1283 ty: IrType::Int(IntWidth::I32),
1284 span,
1285 kind: InstKind::ConstInt(3, IntWidth::I32),
1286 });
1287 let two_i_plus_three = f.next_value_id();
1288 f.register_type(two_i_plus_three, IrType::Int(IntWidth::I32));
1289 f.block_mut(entry).insts.push(Inst {
1290 id: two_i_plus_three,
1291 ty: IrType::Int(IntWidth::I32),
1292 span,
1293 kind: InstKind::IAdd(two_i, three),
1294 });
1295
1296 let (base, scale, offset) =
1297 loop_index_base_and_offset(&f, two_i_plus_three).expect("should decompose");
1298 assert_eq!(base, i, "base should be the unanalysable inner value");
1299 assert_eq!(scale, 2, "scale captures the IMul by 2");
1300 assert_eq!(offset, 3, "offset captures the +3");
1301 }
1302 }
1303