// Wasm Ion loop unrolling (js/src/jit/UnrollLoops.cpp): stale memory // dependency on loads placed after an unrolled+peeled loop. // // Run: js --fuzzing-safe --wasm-compiler=optimized poc.js -> FAIL lines // js --fuzzing-safe --wasm-compiler=baseline poc.js -> all PASS // Also reproduces with default flags after warmup (lazy tiering to Ion), // and with -P wasm_unroll_loops=false the optimized run becomes correct. // // The loop alternates a/b between two structs each iteration and stores an // incrementing counter into the current struct's field, then immediately // reloads it: // S: a.f = c (store) // L: v = a.f (load, dependency() == S) // After the loop, the field of the *first* struct is reloaded: // A: r = s0.f (load, dependency() == S as well: S is the last // aliasing store in RPO order) // // UnrollAndOrPeelLoop clones the loop body (peel + unroll factor 3) and // remaps dependency() fields only for the *cloned* instructions // (UnrollLoops.cpp, "For cloned instructions that have a (load) dependency // field ..."). The dependency of A, which lies outside the loop, is left // pointing at the original store S, which after peeling is the store of the // once-executed peeled iteration. The cloned aliasing stores S1..S3 that // now execute between S and A are not reflected in A's dependency. // // Peeling collapses the copy-0 header phi for `a` to its initial value s0, // so in the GVN rerun after unrolling (which runs even with --ion-gvn=off) // the peeled-iteration load L0 (= load s0.f, dep S) becomes congruent with // A (= load s0.f, dep S) and A is replaced by L0. The function then returns // the value of the *first* iteration's store instead of the last aliasing // store: stores from all cloned loop iterations are lost. let inst = wasmEvalText(`(module (type $S (struct (field (mut i32)))) (func (export "f") (param $n i32) (result i32) (local $a (ref null $S)) (local $b (ref null $S)) (local $t (ref null $S)) (local $s0 (ref null $S)) (local $i i32) (local $c i32) (local $sum i32) (local $v i32) (local.set $s0 (struct.new_default $S)) (local.set $a (local.get $s0)) (local.set $b (struct.new_default $S)) (local.set $c (i32.const 100)) (loop $l ;; S: a.f = c (struct.set $S 0 (local.get $a) (local.get $c)) ;; L: v = a.f (same-iteration reload; dependency = S) (local.set $v (struct.get $S 0 (local.get $a))) (local.set $sum (i32.add (local.get $sum) (local.get $v))) ;; swap a <-> b (local.set $t (local.get $a)) (local.set $a (local.get $b)) (local.set $b (local.get $t)) (local.set $c (i32.add (local.get $c) (i32.const 1))) (local.set $i (i32.add (local.get $i) (i32.const 1))) (br_if $l (i32.lt_u (local.get $i) (local.get $n))) ) ;; A: reload s0.f after the loop (stale dependency after unrolling) (i32.add (struct.get $S 0 (local.get $s0)) (i32.mul (local.get $sum) (i32.const 65536)))))`); // Reference implementation of the wasm function above. function ref(n) { let s0 = 0, s1 = 0, cur = 0, c = 100, sum = 0; for (let i = 0; i < n; i++) { if (cur === 0) s0 = c; else s1 = c; sum += (cur === 0 ? s0 : s1); cur ^= 1; c++; } return ((sum << 16) | s0) >>> 0; } let failures = 0; for (let n = 1; n <= 10; n++) { let got = inst.exports.f(n) >>> 0; let want = ref(n); let ok = got === want; if (!ok) failures++; } assertEq(failures, 0);