From a31e628dd11b9c89c350b3cd882fae926823a686 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 18 Mar 2026 19:03:29 -0700 Subject: [PATCH 1/4] [WIP] Handle StructCmpxchg::expected in Heap2Local We previously assumed that if we were optimizing a StructCmpxchg in Heap2Local, then the flow must be from the `ref` operand. This was based on an assumption that allocations are processed in order of appearance, and because the `ref` operand appears before the `expected operand`, it must be the one getting optimized. But this neglected the fact the array allocations are processed before struct allocations, so if `expected` is an array, it could be optimized first. This faulty assumption led to assertion failures and invalid code. Fix the problem by handling flows from `expected` explicitly. When a non-escaping allocation flows into `expected`, we know it cannot possibly match the value already in the accessed struct, so we can optimize the `cmpxchg` to an atomic `struct.get`. WIP because this is not quite correct due to stale LocalGraph information. When `ref` is subsequently optimized, the LocalGraph does not know about the scratch local it is written to, so the `struct.atomic.get` it now flows into is not properly optimized out. --- src/passes/Heap2Local.cpp | 37 +++++++++++---- test/lit/passes/heap2local-rmw.wast | 71 ++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 11 deletions(-) diff --git a/src/passes/Heap2Local.cpp b/src/passes/Heap2Local.cpp index 00bd1ad2cf2..aec33083ac3 100644 --- a/src/passes/Heap2Local.cpp +++ b/src/passes/Heap2Local.cpp @@ -157,11 +157,11 @@ #include "ir/local-graph.h" #include "ir/parents.h" #include "ir/properties.h" -#include "ir/type-updating.h" #include "ir/utils.h" #include "pass.h" #include "support/unique_deferring_queue.h" #include "wasm-builder.h" +#include "wasm-type.h" #include "wasm.h" namespace wasm { @@ -1110,17 +1110,36 @@ struct Struct2Local : PostWalker { } void visitStructCmpxchg(StructCmpxchg* curr) { - if (analyzer.getInteraction(curr->ref) != ParentChildInteraction::Flows) { - // The allocation can't flow into `replacement` if we've made it this far, - // but it might flow into `expected`, in which case we don't need to do - // anything because we would still be performing the cmpxchg on a real - // struct. We only need to replace the cmpxchg if the ref is being - // replaced with locals. + if (curr->type == Type::unreachable) { + // Leave this for DCE. return; } - if (curr->type == Type::unreachable) { - // As with RefGetDesc and StructGet, above. + // The allocation might flow into `ref` or `expected`, but not + // `replacement`, because then it would be considered to have escaped. + if (analyzer.getInteraction(curr->expected) == + ParentChildInteraction::Flows) { + // Since the allocation does not escape, it cannot possibly match the + // value already in the struct. The cmpxchg will just do a read. Drop the + // other arguments and do the atomic read at the end, when the cmpxchg + // would have happened. Use a nullable scratch local in case we also + // optimize `ref` later and need to replace it with a null. + auto refType = curr->ref->type.with(Nullable); + auto refScratch = builder.addVar(func, refType); + auto* block = builder.makeBlock( + {builder.makeLocalSet(refScratch, curr->ref), + builder.makeDrop(curr->expected), + builder.makeDrop(curr->replacement), + builder.makeStructGet(curr->index, + builder.makeLocalGet(refScratch, refType), + curr->order, + curr->type)}); + replaceCurrent(block); + return; + } + if (analyzer.getInteraction(curr->ref) != ParentChildInteraction::Flows) { + // Since the allocation does not flow from `ref`, it must not flow through + // this cmpxchg at all. return; } diff --git a/test/lit/passes/heap2local-rmw.wast b/test/lit/passes/heap2local-rmw.wast index a8245b3b9f9..37c72054693 100644 --- a/test/lit/passes/heap2local-rmw.wast +++ b/test/lit/passes/heap2local-rmw.wast @@ -1,6 +1,6 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. -;; RUN: wasm-opt %s -all --heap2local -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt -all --heap2local -S -o - | filecheck %s (module (type $i32 (struct (field (mut i32)))) @@ -52,16 +52,24 @@ ;; CHECK: (func $no-escape-cmpxchg-expected (type $3) (param $0 (ref null $struct)) (result (ref null $struct)) ;; CHECK-NEXT: (local $1 (ref null $struct)) - ;; CHECK-NEXT: (struct.atomic.rmw.cmpxchg $struct 0 + ;; CHECK-NEXT: (local $2 (ref null $struct)) + ;; CHECK-NEXT: (local.set $2 ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result nullref) ;; CHECK-NEXT: (local.set $1 ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $0) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.atomic.get $struct 0 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $no-escape-cmpxchg-expected (param (ref null $struct)) (result (ref null $struct)) ;; Allocations that flow into the cmpxchg `expected` operand do not escape @@ -939,3 +947,62 @@ ) ) ) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (struct (field (mut (ref null $array))))) + (type $struct (struct (field (mut (ref null $array))))) + ;; CHECK: (type $array (array (mut i32))) + (type $array (array (field (mut i32)))) + ) + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $test (type $2) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 (ref null (exact $struct))) + ;; CHECK-NEXT: (local $2 (ref null $array)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref null $array)) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (array.new_default $array + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.atomic.get $struct 0 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + (drop + (struct.atomic.rmw.cmpxchg $struct 0 + (struct.new_default $struct) + ;; This will be optimized out before the previous `ref` operand because + ;; array allocations are processed before struct allocations. If we did + ;; not handle flows from `expected` specially, we would incorrectly + ;; refinalize the cmpxchg to have the array replacement type, then get + ;; confused and crash when later processing the `ref` operand. + (array.new_default $array (i32.const 1)) + (array.new_default $array (i32.const 2)) + ) + ) + ) +) From 698251d1e756c2f6150a9b266b09139f1b062f3a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 18 Mar 2026 20:34:21 -0700 Subject: [PATCH 2/4] Fix staleness by updating parents --- src/ir/parents.h | 7 ++- src/passes/Heap2Local.cpp | 26 +++++---- test/lit/passes/heap2local-rmw.wast | 83 +++++++++++++++++++++++++++-- 3 files changed, 100 insertions(+), 16 deletions(-) diff --git a/src/ir/parents.h b/src/ir/parents.h index 10652a8a1f1..6eea568fd2d 100644 --- a/src/ir/parents.h +++ b/src/ir/parents.h @@ -17,7 +17,8 @@ #ifndef wasm_ir_parents_h #define wasm_ir_parents_h -#include "parsing.h" +#include "wasm-traversal.h" +#include "wasm.h" namespace wasm { @@ -32,6 +33,10 @@ struct Parents { return nullptr; } + void setParent(Expression* child, Expression* parent) { + inner.parentMap[child] = parent; + } + private: struct Inner : public ExpressionStackWalker> { diff --git a/src/passes/Heap2Local.cpp b/src/passes/Heap2Local.cpp index aec33083ac3..bfd07237505 100644 --- a/src/passes/Heap2Local.cpp +++ b/src/passes/Heap2Local.cpp @@ -203,14 +203,14 @@ struct EscapeAnalyzer { // We use a lazy graph here because we only need this for reference locals, // and even among them, only ones we see an allocation is stored to. const LazyLocalGraph& localGraph; - const Parents& parents; + Parents& parents; const BranchUtils::BranchTargets& branchTargets; const PassOptions& passOptions; Module& wasm; EscapeAnalyzer(const LazyLocalGraph& localGraph, - const Parents& parents, + Parents& parents, const BranchUtils::BranchTargets& branchTargets, const PassOptions& passOptions, Module& wasm) @@ -1126,15 +1126,21 @@ struct Struct2Local : PostWalker { // optimize `ref` later and need to replace it with a null. auto refType = curr->ref->type.with(Nullable); auto refScratch = builder.addVar(func, refType); - auto* block = builder.makeBlock( - {builder.makeLocalSet(refScratch, curr->ref), - builder.makeDrop(curr->expected), - builder.makeDrop(curr->replacement), - builder.makeStructGet(curr->index, - builder.makeLocalGet(refScratch, refType), - curr->order, - curr->type)}); + auto* setRefScratch = builder.makeLocalSet(refScratch, curr->ref); + auto* getRefScratch = builder.makeLocalGet(refScratch, refType); + auto* structGet = builder.makeStructGet( + curr->index, getRefScratch, curr->order, curr->type); + auto* block = builder.makeBlock({setRefScratch, + builder.makeDrop(curr->expected), + builder.makeDrop(curr->replacement), + structGet}); replaceCurrent(block); + // Record the new data flow into and out of the new scratch local. This is + // necessary in case `ref` gets optimized later so we can detect that it + // flows to the new struct.atomic.get. + analyzer.parents.setParent(curr->ref, setRefScratch); + analyzer.parents.setParent(getRefScratch, structGet); + analyzer.parents.setParent(structGet, block); return; } if (analyzer.getInteraction(curr->ref) != ParentChildInteraction::Flows) { diff --git a/test/lit/passes/heap2local-rmw.wast b/test/lit/passes/heap2local-rmw.wast index 37c72054693..192b658d9d3 100644 --- a/test/lit/passes/heap2local-rmw.wast +++ b/test/lit/passes/heap2local-rmw.wast @@ -958,13 +958,13 @@ ) ;; CHECK: (type $2 (func)) - ;; CHECK: (func $test (type $2) + ;; CHECK: (func $cmpxchg-expected-first (type $2) ;; CHECK-NEXT: (local $0 i32) ;; CHECK-NEXT: (local $1 (ref null (exact $struct))) ;; CHECK-NEXT: (local $2 (ref null $array)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result (ref null $array)) - ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result nullref) ;; CHECK-NEXT: (local.set $2 ;; CHECK-NEXT: (ref.null none) @@ -985,13 +985,16 @@ ;; CHECK-NEXT: (i32.const 2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (struct.atomic.get $struct 0 - ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (block (result (ref null $array)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $2) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $test + (func $cmpxchg-expected-first (drop (struct.atomic.rmw.cmpxchg $struct 0 (struct.new_default $struct) @@ -1001,8 +1004,78 @@ ;; refinalize the cmpxchg to have the array replacement type, then get ;; confused and crash when later processing the `ref` operand. (array.new_default $array (i32.const 1)) + ;; Normally replacements escape and cannot be optimized, but the cmpxchg + ;; is replaced when `expected` is processed, so we end up seeing that + ;; this doesn't escape. (array.new_default $array (i32.const 2)) ) ) ) ) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (struct (field (mut (ref null $array))))) + (type $struct (struct (field (mut (ref null $array))))) + ;; CHECK: (type $array (array (mut i32))) + (type $array (array (field (mut i32)))) + ) + ;; CHECK: (type $2 (func (result (ref $array)))) + + ;; CHECK: (func $cmpxchg-expected-first (type $2) (result (ref $array)) + ;; CHECK-NEXT: (local $array (ref $array)) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 (ref null (exact $struct))) + ;; CHECK-NEXT: (local $3 (ref null $array)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref null $array)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $array + ;; CHECK-NEXT: (array.new_default $array + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result (ref null $array)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $array) + ;; CHECK-NEXT: ) + (func $cmpxchg-expected-first (result (ref $array)) + ;; Same as before, but now the replacement still escapes. Nothing should go + ;; wrong. + (local $array (ref $array)) + (drop + (struct.atomic.rmw.cmpxchg $struct 0 + (struct.new_default $struct) + (array.new_default $array (i32.const 1)) + (local.tee $array + (array.new_default $array (i32.const 2)) + ) + ) + ) + (local.get $array) + ) +) From f1cdca4fd6340aa9c20928888b6e296d019a8abf Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 18 Mar 2026 20:41:04 -0700 Subject: [PATCH 3/4] do less --- src/passes/Heap2Local.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/passes/Heap2Local.cpp b/src/passes/Heap2Local.cpp index bfd07237505..4fcdeea41ff 100644 --- a/src/passes/Heap2Local.cpp +++ b/src/passes/Heap2Local.cpp @@ -1136,11 +1136,10 @@ struct Struct2Local : PostWalker { structGet}); replaceCurrent(block); // Record the new data flow into and out of the new scratch local. This is - // necessary in case `ref` gets optimized later so we can detect that it - // flows to the new struct.atomic.get. + // necessary in case `ref` gets processed later so we can detect that it + // flows to the new struct.atomic.get, which may need to be replaced. analyzer.parents.setParent(curr->ref, setRefScratch); analyzer.parents.setParent(getRefScratch, structGet); - analyzer.parents.setParent(structGet, block); return; } if (analyzer.getInteraction(curr->ref) != ParentChildInteraction::Flows) { From bf9d3b0c6618de2a7db5b73ad4357da066a25f64 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 18 Mar 2026 23:51:46 -0700 Subject: [PATCH 4/4] Fix scratch local when optimizing cmpxchg in Heap2Local When optimizing the `ref` operand to a StructCmpxchg in Heap2Local, we previously used a scratch local with the type of the accessed struct field to hold `expected`. But `expected` is allowed to have any subtype of `eqref` (or shared `eqref`). Fix the type of the scratch local to avoid validation failures. --- src/passes/Heap2Local.cpp | 12 ++- test/lit/passes/heap2local-rmw.wast | 138 +++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 4 deletions(-) diff --git a/src/passes/Heap2Local.cpp b/src/passes/Heap2Local.cpp index 4fcdeea41ff..d514c2d0edf 100644 --- a/src/passes/Heap2Local.cpp +++ b/src/passes/Heap2Local.cpp @@ -1154,9 +1154,15 @@ struct Struct2Local : PostWalker { assert(!field.isPacked()); // Hold everything in scratch locals, just like for other RMW ops and - // struct.new. + // struct.new. Use a nullable (shared) eqref local for `expected` to + // accommodate any allowed optimized or unoptimized value there. + auto expectedType = type; + if (type.isRef()) { + expectedType = + Type(HeapTypes::eq.getBasic(type.getHeapType().getShared()), Nullable); + } auto oldScratch = builder.addVar(func, type); - auto expectedScratch = builder.addVar(func, type); + auto expectedScratch = builder.addVar(func, expectedType); auto replacementScratch = builder.addVar(func, type); auto local = localIndexes[curr->index]; @@ -1168,7 +1174,7 @@ struct Struct2Local : PostWalker { // Create the check for whether we should do the exchange. auto* lhs = builder.makeLocalGet(local, type); - auto* rhs = builder.makeLocalGet(expectedScratch, type); + auto* rhs = builder.makeLocalGet(expectedScratch, expectedType); Expression* pred; if (type.isRef()) { pred = builder.makeRefEq(lhs, rhs); diff --git a/test/lit/passes/heap2local-rmw.wast b/test/lit/passes/heap2local-rmw.wast index 192b658d9d3..0105230a9db 100644 --- a/test/lit/passes/heap2local-rmw.wast +++ b/test/lit/passes/heap2local-rmw.wast @@ -590,7 +590,7 @@ ;; CHECK: (func $rmw-cmpxchg-ref (type $4) (param $0 (ref null $struct)) (param $1 (ref null $struct)) (result (ref null $struct)) ;; CHECK-NEXT: (local $2 (ref null $struct)) ;; CHECK-NEXT: (local $3 (ref null $struct)) - ;; CHECK-NEXT: (local $4 (ref null $struct)) + ;; CHECK-NEXT: (local $4 eqref) ;; CHECK-NEXT: (local $5 (ref null $struct)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result nullref) @@ -1079,3 +1079,139 @@ (local.get $array) ) ) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $outer (struct (field (mut (ref $inner))))) + (type $outer (struct (field (mut (ref $inner))))) + ;; CHECK: (type $inner (struct)) + (type $inner (struct)) + + ;; CHECK: (type $shared-outer (shared (struct (field (mut (ref $shared-inner)))))) + (type $shared-outer (shared (struct (field (mut (ref $shared-inner)))))) + ;; CHECK: (type $shared-inner (shared (struct))) + (type $shared-inner (shared (struct))) + ) + ;; CHECK: (type $4 (func)) + + ;; CHECK: (func $cmpxchg-non-nullable-field (type $4) + ;; CHECK-NEXT: (local $0 (ref $inner)) + ;; CHECK-NEXT: (local $1 (ref $inner)) + ;; CHECK-NEXT: (local $2 (ref $inner)) + ;; CHECK-NEXT: (local $3 eqref) + ;; CHECK-NEXT: (local $4 (ref $inner)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref $inner)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (struct.new_default $inner) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (struct.new_default $inner) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cmpxchg-non-nullable-field + (drop + (struct.atomic.rmw.cmpxchg $outer 0 + ;; When `ref` gets optimized, we need to make sure the scratch local for + ;; `expected` is nullable, even though the operand and field are both + ;; non-nullable. This avoids type errors when we later optimize the + ;; `expected` field and make it a nullref. + (struct.new $outer + (struct.new_default $inner) + ) + (struct.new_default $inner) + (struct.new_default $inner) + ) + ) + ) + + ;; CHECK: (func $cmpxchg-non-nullable-field-shared (type $4) + ;; CHECK-NEXT: (local $0 (ref $shared-inner)) + ;; CHECK-NEXT: (local $1 (ref $shared-inner)) + ;; CHECK-NEXT: (local $2 (ref $shared-inner)) + ;; CHECK-NEXT: (local $3 (ref null (shared eq))) + ;; CHECK-NEXT: (local $4 (ref $shared-inner)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref $shared-inner)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref null (shared none))) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (struct.new_default $shared-inner) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (block (result (ref null (shared none))) + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (struct.new_default $shared-inner) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cmpxchg-non-nullable-field-shared + (drop + (struct.atomic.rmw.cmpxchg $shared-outer 0 + ;; Same, but now with shared types. The scratch local must be a shared + ;; eqref. + (struct.new $shared-outer + (struct.new_default $shared-inner) + ) + (struct.new_default $shared-inner) + (struct.new_default $shared-inner) + ) + ) + ) +)