From 43b0811a65415a98e93f144a064253431ff31f86 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 20 May 2026 14:55:38 -0700 Subject: [PATCH 1/3] LICM: Migrate from invalidates to orderedBefore Replace the coarse `invalidates` check and the coarse global state check in LICM with more precise `orderedBefore` checks. This allows LICM to move memory accesses past release stores, while still correctly blocking them from moving past acquire loads. Add a lit test to verify the asymmetrical reordering behavior with release/acquire atomics on shared memory/GC structs. --- src/passes/LoopInvariantCodeMotion.cpp | 11 +++-- test/lit/passes/licm-atomics.wast | 68 ++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 test/lit/passes/licm-atomics.wast diff --git a/src/passes/LoopInvariantCodeMotion.cpp b/src/passes/LoopInvariantCodeMotion.cpp index 6add5134a79..49a1d32a94d 100644 --- a/src/passes/LoopInvariantCodeMotion.cpp +++ b/src/passes/LoopInvariantCodeMotion.cpp @@ -122,10 +122,15 @@ struct LoopInvariantCodeMotion // The rest of the loop's effects matter too, we must also // take into account global state like interacting loads and // stores. + EffectAnalyzer loopGlobalEffects = loopEffects; + loopGlobalEffects.localsRead.clear(); + loopGlobalEffects.localsWritten.clear(); + EffectAnalyzer globalEffects = effects; + globalEffects.localsRead.clear(); + globalEffects.localsWritten.clear(); bool unsafeToMove = effects.writesGlobalState() || - effectsSoFar.invalidates(effects) || - (effects.readsMutableGlobalState() && - loopEffects.writesGlobalState()); + effectsSoFar.orderedBefore(effects) || + loopGlobalEffects.orderedBefore(globalEffects); // TODO: look into optimizing this with exceptions. for now, disallow if (effects.throws() || loopEffects.throws()) { unsafeToMove = true; diff --git a/test/lit/passes/licm-atomics.wast b/test/lit/passes/licm-atomics.wast new file mode 100644 index 00000000000..43beb342a0c --- /dev/null +++ b/test/lit/passes/licm-atomics.wast @@ -0,0 +1,68 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --licm -S -o - | filecheck %s + +(module + ;; CHECK: (type $struct (shared (struct (field (mut i32))))) + (type $struct (shared (struct (field (mut i32))))) + + ;; CHECK: (memory $mem 1 1 shared) + (memory $mem 1 1 shared) + + ;; Test 1: Allowed reordering (GC read moved before Wasm release store) + ;; CHECK: (func $allowed (type $1) (param $x (ref $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (i32.atomic.store acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $allowed (param $x (ref $struct)) + (loop $loop + ;; X: release store (Wasm memory) + (i32.atomic.store acqrel (i32.const 0) (i32.const 42)) + ;; E: memory access (shared GC read) + (drop + (struct.get $struct 0 (local.get $x)) + ) + (br $loop) + ) + ) + + ;; Test 2: Disallowed reordering (GC read moved before Wasm acquire load) + ;; CHECK: (func $disallowed (type $1) (param $x (ref $struct)) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $disallowed (param $x (ref $struct)) + (loop $loop + ;; X: acquire load (Wasm memory) + (drop + (i32.atomic.load acqrel (i32.const 0)) + ) + ;; E: memory access (shared GC read) + (drop + (struct.get $struct 0 (local.get $x)) + ) + (br $loop) + ) + ) +) From 99b67b96f249299fd6e5f50ca2f15ec64d8729e8 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 20 May 2026 15:14:43 -0700 Subject: [PATCH 2/3] LocalCSE: Migrate from invalidates to orderedBefore Replace the coarse `invalidates` check in `LocalCSE` with `orderedBefore`. This allows `LocalCSE` to reuse expression values across release stores, while still correctly blocking reuse across acquire loads. Add a lit test to verify the asymmetrical reordering behavior with release/acquire atomics on shared GC structs and Wasm memory. --- src/passes/LocalCSE.cpp | 2 +- test/lit/passes/local-cse-atomics.wast | 58 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 test/lit/passes/local-cse-atomics.wast diff --git a/src/passes/LocalCSE.cpp b/src/passes/LocalCSE.cpp index 0233d17061d..11aff64eee2 100644 --- a/src/passes/LocalCSE.cpp +++ b/src/passes/LocalCSE.cpp @@ -497,7 +497,7 @@ struct Checker continue; } auto& originalInfo = kv.second; - if (effects.invalidates(originalInfo.effects)) { + if (effects.orderedBefore(originalInfo.effects)) { invalidated.push_back(original); } } diff --git a/test/lit/passes/local-cse-atomics.wast b/test/lit/passes/local-cse-atomics.wast new file mode 100644 index 00000000000..15553ec9bd4 --- /dev/null +++ b/test/lit/passes/local-cse-atomics.wast @@ -0,0 +1,58 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt -all --local-cse -S -o - %s | filecheck %s + +(module + ;; CHECK: (type $struct (shared (struct (field (mut i32))))) + (type $struct (shared (struct (field (mut i32))))) + + ;; CHECK: (memory $mem 1 1 shared) + (memory $mem 1 1 shared) + + ;; Test 1: Allowed reordering (GC read reused across Wasm release store) + ;; CHECK: (func $allowed (type $1) (param $x (ref $struct)) (result i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (local.tee $2 + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.atomic.store acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + (func $allowed (param $x (ref $struct)) (result i32) + (local $y i32) + (local.set $y (struct.get $struct 0 (local.get $x))) + (i32.atomic.store acqrel (i32.const 0) (i32.const 42)) + (struct.get $struct 0 (local.get $x)) + ) + + ;; Test 2: Disallowed reordering (GC read NOT reused across Wasm acquire load) + ;; CHECK: (func $disallowed (type $1) (param $x (ref $struct)) (result i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $disallowed (param $x (ref $struct)) (result i32) + (local $y i32) + (local.set $y (struct.get $struct 0 (local.get $x))) + (drop (i32.atomic.load acqrel (i32.const 0))) + (struct.get $struct 0 (local.get $x)) + ) +) From 1dda6e5562450df67aec013ededeec5892300062 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 20 May 2026 15:15:18 -0700 Subject: [PATCH 3/3] CodePushing: Migrate from invalidates to orderedBefore Replace the coarse `invalidates` check in `CodePushing` with `orderedBefore`. This allows `CodePushing` to push expressions (like GC reads) past acquire loads, while still correctly blocking them from being pushed past release stores. Add a lit test to verify the asymmetrical reordering behavior with release/acquire atomics on shared GC structs and Wasm memory. --- src/passes/CodePushing.cpp | 4 +- test/lit/passes/code-pushing-atomics.wast | 145 ++++++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 test/lit/passes/code-pushing-atomics.wast diff --git a/src/passes/CodePushing.cpp b/src/passes/CodePushing.cpp index 57dd9993417..31aaf151e28 100644 --- a/src/passes/CodePushing.cpp +++ b/src/passes/CodePushing.cpp @@ -204,7 +204,7 @@ class Pusher { auto* pushable = isPushable(list[i]); if (pushable) { const auto& effects = getPushableEffects(pushable); - if (cumulativeEffects.invalidates(effects)) { + if (effects.orderedBefore(cumulativeEffects)) { // we can't push this, so further pushables must pass it cumulativeEffects.mergeIn(effects); } else { @@ -354,7 +354,7 @@ class Pusher { const auto& effects = getPushableEffects(pushable); - if (cumulativeEffects.invalidates(effects)) { + if (effects.orderedBefore(cumulativeEffects)) { // This can't be moved forward. Add it to the things that are not // moving. cumulativeEffects.walk(list[i]); diff --git a/test/lit/passes/code-pushing-atomics.wast b/test/lit/passes/code-pushing-atomics.wast new file mode 100644 index 00000000000..81046d3b02d --- /dev/null +++ b/test/lit/passes/code-pushing-atomics.wast @@ -0,0 +1,145 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt -all --code-pushing -S -o - %s | filecheck %s + +(module + ;; CHECK: (type $struct (shared (struct (field (mut i32))))) + (type $struct (shared (struct (field (mut i32))))) + + ;; CHECK: (memory $mem 1 1 shared) + (memory $mem 1 1 shared) + + ;; Test 1: Allowed reordering into If (GC read pushed past Wasm acquire load into If arm) + ;; CHECK: (func $allowed (type $1) (param $x (ref $struct)) (param $cond i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $cond) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $allowed (param $x (ref $struct)) (param $cond i32) + (local $y i32) + (local.set $y (struct.get $struct 0 (local.get $x))) + (drop (i32.atomic.load acqrel (i32.const 0))) + (if (local.get $cond) + (then + (drop (local.get $y)) + ) + ) + ) + + ;; Test 2: Disallowed reordering into If (GC read NOT pushed past Wasm release store into If arm) + ;; CHECK: (func $disallowed (type $1) (param $x (ref $struct)) (param $cond i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.atomic.store acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $cond) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $disallowed (param $x (ref $struct)) (param $cond i32) + (local $y i32) + (local.set $y (struct.get $struct 0 (local.get $x))) + (i32.atomic.store acqrel (i32.const 0) (i32.const 42)) + (if (local.get $cond) + (then + (drop (local.get $y)) + ) + ) + ) + + ;; Test 3: Allowed segment reordering (GC read pushed past Wasm acquire load AND target if block, as it is read after the if) + ;; CHECK: (func $allowed_segment (type $1) (param $x (ref $struct)) (param $cond i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $cond) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $allowed_segment (param $x (ref $struct)) (param $cond i32) + (local $y i32) + (local.set $y (struct.get $struct 0 (local.get $x))) + (drop (i32.atomic.load acqrel (i32.const 0))) + (if (local.get $cond) + (then + (nop) + ) + ) + (drop (local.get $y)) + ) + + ;; Test 4: Disallowed segment reordering (GC read NOT pushed past Wasm release store, even if it is read after the if) + ;; CHECK: (func $disallowed_segment (type $1) (param $x (ref $struct)) (param $cond i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (struct.get $struct 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.atomic.store acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $cond) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $disallowed_segment (param $x (ref $struct)) (param $cond i32) + (local $y i32) + (local.set $y (struct.get $struct 0 (local.get $x))) + (i32.atomic.store acqrel (i32.const 0) (i32.const 42)) + (if (local.get $cond) + (then + (nop) + ) + ) + (drop (local.get $y)) + ) +)