Background. With iter_args gone from fir.do_loop from the recent commit, lowering now materializes a DO variable’s final value with an explicit fir.store %final → iv_slot after the loop, and the body round-trips the IV through its memory slot (store then load). For some loop optimizations a perfectly-nested form is desirable: drop the in-body IV round-trip store, and re-emit the final-value store after the outermost loop (recomputed from the trip count, with substitution for triangular nests).
Concrete shape. For a nest:
subroutine s(n, m, a)
integer :: n, m, i, j, a(m,n)
do i = 1, n
do j = 1, m
a(j,i) = i + j
end do
end do
end subroutine
As lowered today — each loop round-trips its IV through memory, and each loop’s final-value store sits right after that loop, so the inner loop’s final store lands inside the outer body (breaking perfect nesting):
fir.do_loop %i = %c1 to %n step %c1 : i32 {
fir.store %i to %i_slot
fir.do_loop %j = %c1 to %m step %c1 : i32 {
fir.store %j to %j_slot // inner round-trip
// ... uses of loads from %i_slot / %j_slot ...
}
// compute the final value of iv %m then store
fir.store %m to %j_slot // inner final store, INSIDE outer body
}
fir.store %n to %i_slot // outer final store, after the nest
Desired canonical form — round-trips dropped, bodies use the IVs directly, and both final-value stores sunk after the outermost loop (now perfectly nested):
fir.do_loop %i = %c1 to %n step %c1 : i32 {
fir.do_loop %j = %c1 to %m step %c1 : i32 {
// ... uses of %i / %j directly ...
}
}
fir.store %m to %j_slot // sunk out of the outer loop
fir.store %n to %i_slot
Question. Where should this canonicalization live — (A) emitted directly at lowering (HLFIR → FIR), or (B) a FIR pass run after lowering?
Constraint (checked on IR). Recomputing the final value needs the loop bounds available outside the outermost loop. Outermost bounds are fine (a DO evaluates them once before the loop), but inner invariant bounds are memory loads re-emitted inside the outer body i.e. they’re only usable if a loop-invariant-code-motion pass hoists them out first. Without that hoist, the inner bound looks like a non-invariant load and the nest has to be rejected. So doing this at lowering would require running LICM before lowering.
Trade-offs (both directions):
- A pass gets whole-nest visibility and can rely on LICM/CSE/alias info that only exists post-lowering; it also keeps lowering focused on semantic lowering rather than canonicalization.
- Doing it at lowering phase avoids the drop-then-recompute round trip and has PFT knowledge, but there’s no whole-nest handle at the point each loop is emitted, the triangular forward-dependency is awkward, and LICM hasn’t run yet (so invariant inner-bound loads aren’t hoisted).
One more question. Dropping the in-body round-trip is safe when nothing else observes the IV’s memory during the loop. Since a Fortran DO variable can’t be modified in the body and the induction value is already an SSA value, we could forward body uses to the IV directly (removing the scalar store/load in the nest) provided the IV’s memory isn’t otherwise read i.e. its address doesn’t escape and it isn’t read through a call. If those hold, can this forwarding legitimately be done during lowering, or is it better left to a separate pass?
Would appreciate input on:
- Which is the better home for this simplification - the lowering phase, or a separate downstream pass?
- Would a discardable attribute stamped by lowering on the final-value store (so a later pass can identify it precisely instead of pattern-matching “first store after the loop”) be considered idiomatic?
- Longer term: any appetite for carrying IV-slot info on fir.do_loop itself so downstream passes don’t have to rediscover it?