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 273ca1fb67f289cce59b3fb93e708302559b9a5c Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 21 May 2026 10:44:48 -0700 Subject: [PATCH 3/3] explanatory comment --- src/passes/LocalCSE.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/passes/LocalCSE.cpp b/src/passes/LocalCSE.cpp index 11aff64eee2..2ad15b731fe 100644 --- a/src/passes/LocalCSE.cpp +++ b/src/passes/LocalCSE.cpp @@ -497,6 +497,8 @@ struct Checker continue; } auto& originalInfo = kv.second; + // Check whether curr must remain before COPY. We use ORIGINAL's effects + // in the check because we know they are the same as COPY's effects. if (effects.orderedBefore(originalInfo.effects)) { invalidated.push_back(original); }