1111//! the source without going through the header
1212//! 4. For each loop, identify invariant instructions in the header (fixpoint):
1313//! - `Const` — trivially invariant
14- //! - `BinOp`, `UnOp`, `Assign`, ` Select` — invariant if all operands are
14+ //! - `BinOp`, `UnOp`, `Select` — invariant if all operands are
1515//! defined outside the loop or by other invariant instructions
16- //! - Skip: `Load`, `Store`, `Call*`, `Global*`, `Memory*`
16+ //! - Skip: `Assign`, ` Load`, `Store`, `Call*`, `Global*`, `Memory*`
1717//! 5. Create or reuse a preheader block and move invariant instructions there
1818//!
1919//! **V1 simplification:** only hoists from the loop header block (which
@@ -92,17 +92,25 @@ fn find_natural_loops(
9292 back_edges : & [ ( BlockId , BlockId ) ] ,
9393 preds : & HashMap < BlockId , HashSet < BlockId > > ,
9494) -> Vec < ( BlockId , HashSet < BlockId > ) > {
95+ // Map from loop header → set of all blocks in that loop.
9596 let mut loops: HashMap < BlockId , HashSet < BlockId > > = HashMap :: new ( ) ;
9697
9798 for & ( src, header) in back_edges {
99+ // Seed the loop with its header. Multiple back edges to the same header
100+ // share one entry, so their bodies are merged into a single loop.
98101 let loop_blocks = loops. entry ( header) . or_insert_with ( || {
99102 let mut set = HashSet :: new ( ) ;
100103 set. insert ( header) ;
101104 set
102105 } ) ;
103106
107+ // Walk backwards from `src` through predecessors, collecting every block
108+ // that can reach `src` without leaving the loop. The header acts as the
109+ // boundary: it is already in the set, so `insert` returns false and we
110+ // stop propagating past it.
104111 let mut worklist = vec ! [ src] ;
105112 while let Some ( n) = worklist. pop ( ) {
113+ // `insert` returns true only for newly-seen blocks, avoiding cycles.
106114 if loop_blocks. insert ( n) {
107115 if let Some ( n_preds) = preds. get ( & n) {
108116 for & p in n_preds {
@@ -129,7 +137,6 @@ fn is_licm_hoistable(instr: &IrInstr) -> bool {
129137 IrInstr :: Const { .. }
130138 | IrInstr :: BinOp { .. }
131139 | IrInstr :: UnOp { .. }
132- | IrInstr :: Assign { .. }
133140 | IrInstr :: Select { .. }
134141 )
135142}
@@ -1252,4 +1259,111 @@ mod tests {
12521259 HashSet :: from( [ BlockId ( 1 ) , BlockId ( 2 ) , BlockId ( 3 ) ] )
12531260 ) ;
12541261 }
1262+
1263+ // ── Assign (counter reset) must NOT be hoisted from inner loop ────────
1264+ //
1265+ // Regression test for: `IrInstr::Assign` appearing in `is_licm_hoistable`
1266+ // caused inner-loop counter resets to be hoisted to the outer preheader,
1267+ // executing only once per function call rather than once per outer iteration.
1268+ // This produced an effectively infinite loop in nested-loop programs such as
1269+ // a bubble sort (fill_sort_sum benchmark).
1270+ //
1271+ // CFG (outer ← B0→B1 back-edge; inner ← B3→B2 back-edge):
1272+ //
1273+ // B0 (outer-preheader): Jump → B1
1274+ // B1 (outer-header): BranchIf(cond_outer, B2, B5)
1275+ // B2 (inner-header): v_init = Const(0)
1276+ // j = Assign(v_init) ← counter reset
1277+ // BranchIf(cond_inner, B3, B4)
1278+ // B3 (inner-body): Jump → B2 ← inner back-edge
1279+ // B4 (inner-exit): Jump → B1 ← outer back-edge
1280+ // B5 (outer-exit): Return
1281+ //
1282+ // Before the fix: LICM on the inner loop (header = B2) would see v_init as a
1283+ // Const (trivially invariant) and then, via the fixpoint, classify
1284+ // `j = Assign(v_init)` as invariant too (because v_init is in
1285+ // invariant_dests). Both get hoisted to the outer preheader B0. On every
1286+ // outer iteration B2's instructions are already gone, so `j` is never
1287+ // reset — the inner loop runs with a stale, ever-growing `j`, hanging.
1288+ //
1289+ // After the fix: Assign is excluded from is_licm_hoistable, so only v_init
1290+ // is hoisted (to B0), while `j = Assign(v_init)` stays in B2 and resets the
1291+ // counter correctly on every outer iteration.
1292+ #[ test]
1293+ fn assign_counter_reset_not_hoisted_from_inner_loop ( ) {
1294+ // Variable map:
1295+ // VarId(0) = cond_outer (defined externally, used in B1)
1296+ // VarId(1) = cond_inner (defined externally, used in B2)
1297+ // VarId(2) = v_init (Const 0, defined in inner header B2)
1298+ // VarId(3) = j (Assign(v_init), inner loop counter, in B2)
1299+ let mut func = make_func ( vec ! [
1300+ // B0 outer preheader
1301+ IrBlock {
1302+ id: BlockId ( 0 ) ,
1303+ instructions: vec![ ] ,
1304+ terminator: IrTerminator :: Jump { target: BlockId ( 1 ) } ,
1305+ } ,
1306+ // B1 outer header
1307+ IrBlock {
1308+ id: BlockId ( 1 ) ,
1309+ instructions: vec![ ] ,
1310+ terminator: IrTerminator :: BranchIf {
1311+ condition: VarId ( 0 ) ,
1312+ if_true: BlockId ( 2 ) ,
1313+ if_false: BlockId ( 5 ) ,
1314+ } ,
1315+ } ,
1316+ // B2 inner header: v_init = Const(0); j = Assign(v_init)
1317+ IrBlock {
1318+ id: BlockId ( 2 ) ,
1319+ instructions: vec![
1320+ IrInstr :: Const {
1321+ dest: VarId ( 2 ) ,
1322+ value: IrValue :: I32 ( 0 ) ,
1323+ } ,
1324+ IrInstr :: Assign {
1325+ dest: VarId ( 3 ) ,
1326+ src: VarId ( 2 ) ,
1327+ } ,
1328+ ] ,
1329+ terminator: IrTerminator :: BranchIf {
1330+ condition: VarId ( 1 ) ,
1331+ if_true: BlockId ( 3 ) ,
1332+ if_false: BlockId ( 4 ) ,
1333+ } ,
1334+ } ,
1335+ // B3 inner body → back-edge to inner header B2
1336+ IrBlock {
1337+ id: BlockId ( 3 ) ,
1338+ instructions: vec![ ] ,
1339+ terminator: IrTerminator :: Jump { target: BlockId ( 2 ) } ,
1340+ } ,
1341+ // B4 inner exit → back-edge to outer header B1
1342+ IrBlock {
1343+ id: BlockId ( 4 ) ,
1344+ instructions: vec![ ] ,
1345+ terminator: IrTerminator :: Jump { target: BlockId ( 1 ) } ,
1346+ } ,
1347+ // B5 outer exit
1348+ IrBlock {
1349+ id: BlockId ( 5 ) ,
1350+ instructions: vec![ ] ,
1351+ terminator: IrTerminator :: Return { value: None } ,
1352+ } ,
1353+ ] ) ;
1354+
1355+ eliminate ( & mut func) ;
1356+
1357+ // The Assign (j = Assign(v_init)) MUST remain in the inner header B2.
1358+ // If it were hoisted to the outer preheader the counter reset would only
1359+ // happen once per function call, not once per outer iteration.
1360+ let inner_header = func. blocks . iter ( ) . find ( |b| b. id == BlockId ( 2 ) ) . unwrap ( ) ;
1361+ assert ! (
1362+ inner_header
1363+ . instructions
1364+ . iter( )
1365+ . any( |i| matches!( i, IrInstr :: Assign { dest: VarId ( 3 ) , .. } ) ) ,
1366+ "j = Assign(v_init) must stay in the inner header B2, not be hoisted"
1367+ ) ;
1368+ }
12551369}
0 commit comments