From 43b0811a65415a98e93f144a064253431ff31f86 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 20 May 2026 14:55:38 -0700 Subject: [PATCH 1/6] 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/6] 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/6] 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)) + ) +) From 55d014d067994acccd779d3e319b7eb123bd3a4a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 20 May 2026 15:30:58 -0700 Subject: [PATCH 4/6] HeapStoreOptimization: Migrate from invalidates to orderedBefore Replace the coarse `invalidates` check in `HeapStoreOptimization` with `orderedBefore`. This allows `HeapStoreOptimization` to optimize heap stores across release stores (e.g., moving a GC read before a release store), while still correctly blocking them from being moved before acquire loads. Added a lit test to verify the asymmetrical reordering behavior with release/acquire atomics on shared GC structs and Wasm memory. TAG=agy --- src/passes/HeapStoreOptimization.cpp | 8 +- test/lit/passes/heap-store-atomics.wast | 164 ++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 test/lit/passes/heap-store-atomics.wast diff --git a/src/passes/HeapStoreOptimization.cpp b/src/passes/HeapStoreOptimization.cpp index c720c5f0a41..6c6e729e744 100644 --- a/src/passes/HeapStoreOptimization.cpp +++ b/src/passes/HeapStoreOptimization.cpp @@ -181,7 +181,7 @@ struct HeapStoreOptimization // effects. auto firstEffects = effects(list[i]); auto secondEffects = effects(list[j]); - if (secondEffects.invalidates(firstEffects)) { + if (firstEffects.orderedBefore(secondEffects)) { return false; } @@ -241,7 +241,7 @@ struct HeapStoreOptimization if (!new_->isWithDefault()) { for (Index i = index + 1; i < operands.size(); i++) { auto operandEffects = effects(operands[i]); - if (operandEffects.invalidates(setValueEffects)) { + if (operandEffects.orderedBefore(setValueEffects)) { // TODO: we could use locals to reorder everything return false; } @@ -252,7 +252,7 @@ struct HeapStoreOptimization // if it exists. if (new_->desc) { auto descEffects = effects(new_->desc); - if (descEffects.invalidates(setValueEffects)) { + if (descEffects.orderedBefore(setValueEffects)) { // TODO: we could use locals to reorder everything return false; } @@ -264,7 +264,7 @@ struct HeapStoreOptimization // the optimization X' would happen first. ShallowEffectAnalyzer structNewEffects( getPassOptions(), *getModule(), new_); - if (structNewEffects.invalidates(setValueEffects)) { + if (structNewEffects.orderedBefore(setValueEffects)) { return false; } diff --git a/test/lit/passes/heap-store-atomics.wast b/test/lit/passes/heap-store-atomics.wast new file mode 100644 index 00000000000..76e9a0b11d2 --- /dev/null +++ b/test/lit/passes/heap-store-atomics.wast @@ -0,0 +1,164 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --heap-store-optimization -all -S -o - | filecheck %s + +(module + ;; CHECK: (type $struct (shared (struct (field (mut i32)) (field (mut i32))))) + (type $struct (shared (struct (field (mut i32)) (field (mut i32))))) + + ;; CHECK: (memory $mem 1 1 shared) + (memory $mem 1 1 shared) + + ;; Test 1: Disallowed reordering (GC read NOT moved before Wasm acquire load) + ;; CHECK: (func $disallowed (type $1) (param $x (ref $struct)) (param $other (ref $struct)) + ;; CHECK-NEXT: (local $ref (ref $struct)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new $struct + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.set $struct 0 + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: (struct.get $struct 1 + ;; CHECK-NEXT: (local.get $other) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $disallowed (param $x (ref $struct)) (param $other (ref $struct)) + (local $ref (ref $struct)) + (local.set $ref + (struct.new $struct + (i32.const 0) + ;; Acquire load (returns i32 via block) + (block (result i32) + (drop (i32.atomic.load acqrel (i32.const 0))) + (i32.const 0) + ) + ) + ) + ;; struct.set with GC read value. + (struct.set $struct 0 + (local.get $ref) + (struct.get $struct 1 (local.get $other)) + ) + ) + + ;; Test 2: Allowed reordering (GC read moved before Wasm release store) + ;; CHECK: (func $allowed (type $1) (param $x (ref $struct)) (param $other (ref $struct)) + ;; CHECK-NEXT: (local $ref (ref $struct)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new $struct + ;; CHECK-NEXT: (struct.get $struct 1 + ;; CHECK-NEXT: (local.get $other) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (i32.atomic.store acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $allowed (param $x (ref $struct)) (param $other (ref $struct)) + (local $ref (ref $struct)) + (local.set $ref + (struct.new $struct + (i32.const 0) + ;; Release store (returns i32 via block) + (block (result i32) + (i32.atomic.store acqrel (i32.const 0) (i32.const 42)) + (i32.const 0) + ) + ) + ) + ;; struct.set with GC read value + (struct.set $struct 0 + (local.get $ref) + (struct.get $struct 1 (local.get $other)) + ) + ) + ;; Test 3: Swap allowed (GC read in struct.new swapped with subsequent acquire load) + ;; CHECK: (func $swap_allowed (type $2) (param $other (ref $struct)) + ;; CHECK-NEXT: (local $ref (ref $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new $struct + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $swap_allowed (param $other (ref $struct)) + (local $ref (ref $struct)) + (local.set $ref + (struct.new $struct + (struct.get $struct 1 (local.get $other)) + (i32.const 0) + ) + ) + (drop (i32.atomic.load acqrel (i32.const 0))) + (struct.set $struct 0 + (local.get $ref) + (i32.const 42) + ) + ) + + ;; Test 4: Swap disallowed (struct.new with acquire load NOT swapped with subsequent GC read) + ;; CHECK: (func $swap_disallowed (type $2) (param $other (ref $struct)) + ;; CHECK-NEXT: (local $ref (ref $struct)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new $struct + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $struct 1 + ;; CHECK-NEXT: (local.get $other) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.set $struct 0 + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $swap_disallowed (param $other (ref $struct)) + (local $ref (ref $struct)) + (local.set $ref + (struct.new $struct + (block (result i32) + (drop (i32.atomic.load acqrel (i32.const 0))) + (i32.const 0) + ) + (i32.const 0) + ) + ) + (drop (struct.get $struct 1 (local.get $other))) + (struct.set $struct 0 + (local.get $ref) + (i32.const 42) + ) + ) +) From 33a9941c757f9ef5e0f14f8985a47a79b24d672f Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 22 May 2026 15:10:46 -0700 Subject: [PATCH 5/6] more tests --- test/lit/passes/heap-store-atomics.wast | 162 +++++++++++++++++++++++- 1 file changed, 158 insertions(+), 4 deletions(-) diff --git a/test/lit/passes/heap-store-atomics.wast b/test/lit/passes/heap-store-atomics.wast index 76e9a0b11d2..0d63b7ec05f 100644 --- a/test/lit/passes/heap-store-atomics.wast +++ b/test/lit/passes/heap-store-atomics.wast @@ -5,11 +5,19 @@ ;; CHECK: (type $struct (shared (struct (field (mut i32)) (field (mut i32))))) (type $struct (shared (struct (field (mut i32)) (field (mut i32))))) + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $described (shared (descriptor $desc) (struct (field (mut i32)) (field (mut i32))))) + (type $described (shared (descriptor $desc) (struct (field (mut i32))))) + ;; CHECK: (type $desc (shared (describes $described) (struct))) + (type $desc (shared (describes $described) (struct))) + ) + ;; CHECK: (memory $mem 1 1 shared) (memory $mem 1 1 shared) ;; Test 1: Disallowed reordering (GC read NOT moved before Wasm acquire load) - ;; CHECK: (func $disallowed (type $1) (param $x (ref $struct)) (param $other (ref $struct)) + ;; CHECK: (func $disallowed (type $3) (param $x (ref $struct)) (param $other (ref $struct)) ;; CHECK-NEXT: (local $ref (ref $struct)) ;; CHECK-NEXT: (local.set $ref ;; CHECK-NEXT: (struct.new $struct @@ -51,7 +59,7 @@ ) ;; Test 2: Allowed reordering (GC read moved before Wasm release store) - ;; CHECK: (func $allowed (type $1) (param $x (ref $struct)) (param $other (ref $struct)) + ;; CHECK: (func $allowed (type $3) (param $x (ref $struct)) (param $other (ref $struct)) ;; CHECK-NEXT: (local $ref (ref $struct)) ;; CHECK-NEXT: (local.set $ref ;; CHECK-NEXT: (struct.new $struct @@ -88,7 +96,7 @@ ) ) ;; Test 3: Swap allowed (GC read in struct.new swapped with subsequent acquire load) - ;; CHECK: (func $swap_allowed (type $2) (param $other (ref $struct)) + ;; CHECK: (func $swap_allowed (type $4) (param $other (ref $struct)) ;; CHECK-NEXT: (local $ref (ref $struct)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (i32.atomic.load acqrel @@ -119,7 +127,7 @@ ) ;; Test 4: Swap disallowed (struct.new with acquire load NOT swapped with subsequent GC read) - ;; CHECK: (func $swap_disallowed (type $2) (param $other (ref $struct)) + ;; CHECK: (func $swap_disallowed (type $4) (param $other (ref $struct)) ;; CHECK-NEXT: (local $ref (ref $struct)) ;; CHECK-NEXT: (local.set $ref ;; CHECK-NEXT: (struct.new $struct @@ -161,4 +169,150 @@ (i32.const 42) ) ) + ;; Test 5: GC read in struct.set value CAN move before release store in descriptor + ;; CHECK: (func $desc_allowed (type $5) (param $other (ref $described)) (param $d (ref (exact $desc))) + ;; CHECK-NEXT: (local $ref (ref $described)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new_desc $described + ;; CHECK-NEXT: (struct.get $described 1 + ;; CHECK-NEXT: (local.get $other) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (block (result (ref (exact $desc))) + ;; CHECK-NEXT: (i32.atomic.store acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $d) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $desc_allowed (param $other (ref $strct)) (param $d (ref (exact $desc))) + (local $ref (ref $described)) + (local.set $ref + (struct.new_desc $described + (i32.const 0) + (block (result (ref (exact $desc))) + (i32.atomic.store acqrel (i32.const 0) (i32.const 42)) + (local.get $d) + ) + ) + ) + (struct.set $described 0 + (local.get $ref) + (struct.get $struct 1 (local.get $other)) + ) + ) + + ;; Test 6: GC read in struct.set value CANNOT move before acquire load in descriptor + ;; CHECK: (func $desc_disallowed (type $5) (param $other (ref $described)) (param $d (ref (exact $desc))) + ;; CHECK-NEXT: (local $ref (ref $described)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new_desc $described + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (block (result (ref (exact $desc))) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.atomic.load acqrel + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $d) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.set $described 0 + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: (struct.get $described 1 + ;; CHECK-NEXT: (local.get $other) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $desc_disallowed (param $other (ref $struct)) (param $d (ref (exact $desc))) + (local $ref (ref $described)) + (local.set $ref + (struct.new_desc $described + (i32.const 0) + (i32.const 0) + (block (result (ref (exact $desc))) + (drop (i32.atomic.load acqrel (i32.const 0))) + (local.get $d) + ) + ) + ) + (struct.set $described 0 + (local.get $ref) + (struct.get $struct 1 (local.get $other)) + ) + ) + + ;; Test 7: Memory load in struct.set value CAN move before shallow trap (nullable desc) of struct.new + ;; CHECK: (func $shallow_allowed (type $6) (param $d (ref (exact $desc))) + ;; CHECK-NEXT: (local $ref (ref $described)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new_desc $described + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $shallow_allowed (param $d (ref (exact $desc))) + (local $ref (ref $described)) + (local.set $ref + (struct.new_desc $described + (i32.const 0) + (i32.const 0) + (ref.null $desc) + ) + ) + (struct.set $described 0 + (local.get $ref) + (i32.load (i32.const 0)) + ) + ) + + ;; Test 8: Memory store in struct.set value CANNOT move before shallow trap (nullable desc) of struct.new + ;; CHECK: (func $shallow_disallowed (type $6) (param $d (ref (exact $desc))) + ;; CHECK-NEXT: (local $ref (ref $described)) + ;; CHECK-NEXT: (local.set $ref + ;; CHECK-NEXT: (struct.new_desc $described + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.set $described 0 + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1337) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $shallow_disallowed (param $d (ref (exact $desc))) + (local $ref (ref $described)) + (local.set $ref + (struct.new_desc $described + (i32.const 0) + (i32.const 0) + (ref.null $desc) + ) + ) + (struct.set $described 0 + (local.get $ref) + (block (result i32) + (i32.store (i32.const 0) (i32.const 42)) + (i32.const 1337) + ) + ) + ) ) From 6a8a80bd07a9151dbf2e90b83d7414b7587b1ad9 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 22 May 2026 15:38:43 -0700 Subject: [PATCH 6/6] update test --- test/lit/passes/heap-store-atomics.wast | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/test/lit/passes/heap-store-atomics.wast b/test/lit/passes/heap-store-atomics.wast index 0d63b7ec05f..a5254d2408e 100644 --- a/test/lit/passes/heap-store-atomics.wast +++ b/test/lit/passes/heap-store-atomics.wast @@ -7,7 +7,7 @@ (rec ;; CHECK: (rec - ;; CHECK-NEXT: (type $described (shared (descriptor $desc) (struct (field (mut i32)) (field (mut i32))))) + ;; CHECK-NEXT: (type $described (shared (descriptor $desc) (struct (field (mut i32))))) (type $described (shared (descriptor $desc) (struct (field (mut i32))))) ;; CHECK: (type $desc (shared (describes $described) (struct))) (type $desc (shared (describes $described) (struct))) @@ -170,14 +170,13 @@ ) ) ;; Test 5: GC read in struct.set value CAN move before release store in descriptor - ;; CHECK: (func $desc_allowed (type $5) (param $other (ref $described)) (param $d (ref (exact $desc))) + ;; CHECK: (func $desc_allowed (type $5) (param $other (ref $struct)) (param $d (ref (exact $desc))) ;; CHECK-NEXT: (local $ref (ref $described)) ;; CHECK-NEXT: (local.set $ref ;; CHECK-NEXT: (struct.new_desc $described - ;; CHECK-NEXT: (struct.get $described 1 + ;; CHECK-NEXT: (struct.get $struct 1 ;; CHECK-NEXT: (local.get $other) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: (block (result (ref (exact $desc))) ;; CHECK-NEXT: (i32.atomic.store acqrel ;; CHECK-NEXT: (i32.const 0) @@ -189,7 +188,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) - (func $desc_allowed (param $other (ref $strct)) (param $d (ref (exact $desc))) + (func $desc_allowed (param $other (ref $struct)) (param $d (ref (exact $desc))) (local $ref (ref $described)) (local.set $ref (struct.new_desc $described @@ -207,12 +206,11 @@ ) ;; Test 6: GC read in struct.set value CANNOT move before acquire load in descriptor - ;; CHECK: (func $desc_disallowed (type $5) (param $other (ref $described)) (param $d (ref (exact $desc))) + ;; CHECK: (func $desc_disallowed (type $5) (param $other (ref $struct)) (param $d (ref (exact $desc))) ;; CHECK-NEXT: (local $ref (ref $described)) ;; CHECK-NEXT: (local.set $ref ;; CHECK-NEXT: (struct.new_desc $described ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: (block (result (ref (exact $desc))) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (i32.atomic.load acqrel @@ -225,7 +223,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $described 0 ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: (struct.get $described 1 + ;; CHECK-NEXT: (struct.get $struct 1 ;; CHECK-NEXT: (local.get $other) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -234,7 +232,6 @@ (local $ref (ref $described)) (local.set $ref (struct.new_desc $described - (i32.const 0) (i32.const 0) (block (result (ref (exact $desc))) (drop (i32.atomic.load acqrel (i32.const 0))) @@ -256,7 +253,6 @@ ;; CHECK-NEXT: (i32.load ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: (ref.null (shared none)) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -266,7 +262,6 @@ (local $ref (ref $described)) (local.set $ref (struct.new_desc $described - (i32.const 0) (i32.const 0) (ref.null $desc) ) @@ -283,7 +278,6 @@ ;; CHECK-NEXT: (local.set $ref ;; CHECK-NEXT: (struct.new_desc $described ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: (ref.null (shared none)) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -302,7 +296,6 @@ (local $ref (ref $described)) (local.set $ref (struct.new_desc $described - (i32.const 0) (i32.const 0) (ref.null $desc) )