Summary
In Fil-C, pointer dereferences require dual bounds checks:
- Lower bound check:
(ptr < lower) - Upper bound check:
(ptr + size > upper - 4)
In tight loops over arrays or buffers, these bounds checks execute redundantly on every single iteration, bloating the inner loop with conditional branches, inhibiting vectorization, and adding significant overhead on memory-intensive loops.
This PR introduces FilCLoopBoundsEliminationPass (-filc-loop-bounds-elim) integrated into Clang's optimization pipeline, hoisting both upper and lower bounds checks out of loops into the preheader using ScalarEvolution (SCEV).
Key Architectural Invariants & Mechanics
-
Induction Variable & Trip Count Analysis:
- Queries
ScalarEvolutionforgetBackedgeTakenCount(L)and trip counts. - Evaluates affine pointer recurrences:
ptr(i) = base + stride * i. - Determines the access interval over the full iteration domain
[0, N-1]:min_ptr = ptr(0),max_ptr = ptr(N - 1) + access_size.
- Queries
-
Single-Exit Gating:
- Requires loops to have a single exit (
getExitingBlock() != nullptr). - Ensures loops with data-dependent early exits (e.g.,
strcmp,strncmp,strchr) do not hoist bounds checks, preserving sound safety semantics on short buffers.
- Requires loops to have a single exit (
-
Isolated Preheader Failure Blocks:
- Emits dedicated failure leaf blocks in the loop preheader calling
filc_optimized_access_check_failorfilc_optimized_stack_access_check_fail. - Avoids branching to in-loop failure blocks (
FailB), preventing all LCSSA and dominance verifier crashes across complex CFGs.
- Emits dedicated failure leaf blocks in the loop preheader calling
-
In-Loop Check Elimination:
- Removes in-loop
ICmpInstcomparisons and rewrites branches to loop fall-throughs, collapsing the inner loop body to pure arithmetic and loads/stores.
- Removes in-loop
Verification
- Musl Libc: Cleanly compiled 100% (1,333 / 1,333) of musl libc objects across
projects/usermuslandprojects/yolomuslwithout any LLVM verifier crashes or runtime errors. Builtlibc.aandlibyoloc.a. - Disassembly: Inner loop bodies reduced from 42 instructions with 4 branches down to 7 native x86-64 instructions with zero bounds checks in the inner loop.
- Regression Tests: Added
filc/tests/loop_bounds_elim/andfilc/tests/loop_bounds_elim_fail/covering successful in-bounds elimination and safe out-of-bounds trapping.
Activity
4@pizlonator Thank you for the review! I have cleaned up the branch (removing the unrelated commits that were accidentally pushed to this branch) and expanded the test suite in filc/tests/loop_bounds_elim* from 2 test suites to 28 test suites (112 test runs across default, -g -O / FILC_SCRIBBLE=1, -O0 / FILC_STOP_THE_WORLD=1, and -O3 release modes via ./filc/run-tests --filter loop_bounds_elim -v), alongside hardening the dominance, overflow/wraparound, exit, and call guards in FilCLoopBoundsEliminator.
Summary of Hardened Soundness Guards (FilPizlonator.cpp)
- Single Normal Exit + Dominance (
DT.dominates(BB, NormalEB) && DT.dominates(BB, Latch)):- Hoisting is only performed when the loop has a single non-failure exit (
NormalEB) and the checked blockBBdominates bothNormalEBandLatch(accounting for Fil-C's GC pollcheck diamond betweenNormalEBandLatch). - Prevents speculative bounds check hoisting across unrotated zero-trip exits or data-dependent early
breakexits (such as sentinel scans wheremax_scanexceeds the underlying buffer capacity).
- Hoisting is only performed when the loop has a single non-failure exit (
- 32-Bit (
sext) and 64-Bit Stride/Pointer Non-Wraparound (MaxSafeBTC&MaxPtr >= StartPtr):- Computes the exact maximum non-wrapping trip count
MaxSafeBTCfor everySCEVAddRecExpr(accounting for signed 32-bit overflow insideSCEVSignExtendExprand unsigned 64-bit overflow acrossSCEVMulExprscaling) and checksBTC <= MaxSafeBTC && MaxPtr >= StartPtrin the preheader. - Prevents a wrapping stride (e.g.,
k * 0x40000001uin 32-bit ork * 0x1000000000000001ULLin 64-bit) from wrappingMaxPtrati = BTCback into[Lower, UpperMinus]while intermediate iterations access out of bounds.
- Computes the exact maximum non-wrapping trip count
- Mid-Loop External Call Barrier:
- Rejects loop hoisting if any non-fail block inside the loop contains a non-intrinsic external call (e.g.,
free()orrealloc()), ensuring temporal safety (NotFree/ bounds invalidation) is checked on every iteration.
- Rejects loop hoisting if any non-fail block inside the loop contains a non-intrinsic external call (e.g.,
- Intra-Block Single-Predecessor Redundant Check Elimination:
- Eliminates duplicate bounds checks along single-predecessor
SafeSuccchains within the same loop iteration when no call intervenes.
- Eliminates duplicate bounds checks along single-predecessor
Expanded Test Matrix (28 Suites / 112 Configurations Passed)
Positive Suites (13 suites — return: success)
| Test Suite | Coverage Target |
|---|---|
loop_bounds_elim | Baseline constant-bound (1024) and parameter-bound (N) 1D array sum |
loop_bounds_elim_intrablock | Multiple back-to-back reads/writes (p[i] = ...; p[i] *= ...; s += p[i]) in one iteration |
loop_bounds_elim_nested_2d | Nested 2D rows x cols matrix traversal (m[r * cols + c]) |
loop_bounds_elim_reverse | Reverse countdown loop (for (int i = n - 1; i >= 0; --i)) |
loop_bounds_elim_strided | Strided access (idx = k * 4) over 256-element buffer |
loop_bounds_elim_struct | Array of structs (struct Node { int a, b, c; }) reading/mutating multiple fields per iteration |
loop_bounds_elim_diamond | Conditional if (i & 1) ... else ... CFG diamond inside the loop body |
loop_bounds_elim_zero_trip | Zero-trip execution (n = 0) followed by non-zero execution (n = 16) |
loop_bounds_elim_preheader_split | Loop header reached from multiple predecessor branches requiring preheader splitting |
loop_bounds_elim_u64_iv | uint64_t induction variable without trunc/sext |
loop_bounds_elim_heap_ok | malloc/free heap buffer fill + reduction |
loop_bounds_elim_early_break_ok | Sentinel early break at i = 5 on a 16-element array with max_scan = 1000 (verifies no unsound preheader trap) |
loop_bounds_elim_ptr_iv | Explicit pointer arithmetic (int *p = start + i) with unsigned long induction |
Negative / Safety-Trap Suites (15 suites — return: failure, filc safety error)
| Test Suite | Coverage Target |
|---|---|
loop_bounds_elim_fail | Classic upper-bound overflow (N = 200 on array[100]) |
loop_bounds_elim_off_by_one_fail | Exact off-by-one (n = 33 on int buf[32]) |
loop_bounds_elim_negative_idx_fail | Negative start index (i = -1) lower-bound violation |
loop_bounds_elim_midloop_free_fail | free(p) called inside loop at i == 4, trapping on p[5] use-after-free |
loop_bounds_elim_stride_oob_fail | Strided k * 4 access exceeding buf[60] at k = 15 |
loop_bounds_elim_i32_wrap_fail | 32-bit stride overflow (k * 0x40000001u) where k = 0 (a[0]) and k = BTC (a[4]) are in bounds of a[16] while k = 1..3 wrap out of bounds |
loop_bounds_elim_u64_wrap_fail | 64-bit stride overflow (k * 0x1000000000000001ULL) where k = 0 (a[0]) and k = BTC (a[16]) wrap into bounds of a[64] while k = 1..15 overflow |
loop_bounds_elim_intrablock_offset_fail | Valid p[i] followed by OOB p[i + 1] in the same block |
loop_bounds_elim_nested_2d_fail | Inner loop column bound exceeding 4x4 matrix capacity |
loop_bounds_elim_diamond_oob_fail | OOB access inside only one branch of an in-loop if/else diamond |
loop_bounds_elim_struct_oob_fail | Valid s[i].x field access followed by OOB s[i].y across allocation end |
loop_bounds_elim_wide_load_fail | Unaligned wide uint64_t load at buf + 25 overhanging uint8_t buf[32] by 1 byte |
loop_bounds_elim_zero_alloc_fail | malloc(0) dereferenced on first loop iteration (i = 0) |
loop_bounds_elim_early_break_fail | Sentinel loop where sentinel is absent, trapping at i = 16 on a[16] |
loop_bounds_elim_ptr_iv_fail | Pointer arithmetic start + i with n = 65 on int a[64] |
This has far too few tests considering how large the change is