From 0a01f7f39cc5c11b98c36914ef1fae24165ac550 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 16 Oct 2026 14:34:17 -0700 Subject: [PATCH] [Heap2Local] Handle newly unreachable refs in cmpxchg and cast_desc_eq `Struct2Local` rewrites expressions in a post-order walk while deferring `ReFinalize` until the end of the function walk. When an unescaping allocation flows into both a failing cast inside the `ref` operand of a `struct.atomic.rmw.cmpxchg`, `array.atomic.rmw.cmpxchg`, or nullable `ref.cast_desc_eq` and into its `expected` or `desc` operand, the inner cast is replaced with an unreachable block before the parent expression is visited. Because `ReFinalize` has not run yet, the parent's `curr->type` remains concrete while `curr->ref->type` is now `Type::unreachable`, causing `curr->ref->type.with(Nullable)` or `builder.addVar(func, curr->ref->type)` to fail assertions or produce invalid IR. Check for `curr->ref->type == Type::unreachable` in `visitStructCmpxchg`, `visitArrayCmpxchg`, and `visitRefCast`. --- src/passes/Heap2Local.cpp | 9 +- test/lit/passes/heap2local-desc.wast | 38 ++++++++ test/lit/passes/heap2local-rmw.wast | 124 +++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 3 deletions(-) diff --git a/src/passes/Heap2Local.cpp b/src/passes/Heap2Local.cpp index 7f8381e2a6c..78d643e75ff 100644 --- a/src/passes/Heap2Local.cpp +++ b/src/passes/Heap2Local.cpp @@ -898,7 +898,8 @@ struct Struct2Local : PostWalker { // side effects, but that local.set would not be reflected in the parent // map, so it would not be updated if the allocation flowing through // that desc operand were later optimized. - if (allocIsCastDescEq && !allocIsCastRef && curr->type.isNullable()) { + if (allocIsCastDescEq && !allocIsCastRef && curr->type.isNullable() && + curr->ref->type != Type::unreachable) { // There might be a null value to let through. Reuse curr as a cast to // null. Use a scratch local to move the reference value past the desc // value. @@ -1117,7 +1118,8 @@ struct Struct2Local : PostWalker { } void visitStructCmpxchg(StructCmpxchg* curr) { - if (curr->type == Type::unreachable) { + if (curr->type == Type::unreachable || + curr->ref->type == Type::unreachable) { // Leave this for DCE. return; } @@ -1199,7 +1201,8 @@ struct Struct2Local : PostWalker { } void visitArrayCmpxchg(ArrayCmpxchg* curr) { - if (curr->type == Type::unreachable) { + if (curr->type == Type::unreachable || + curr->ref->type == Type::unreachable) { // Leave this for DCE. return; } diff --git a/test/lit/passes/heap2local-desc.wast b/test/lit/passes/heap2local-desc.wast index 4691485216a..0c1288c3f14 100644 --- a/test/lit/passes/heap2local-desc.wast +++ b/test/lit/passes/heap2local-desc.wast @@ -519,6 +519,44 @@ ) ) + ;; CHECK: (func $cast-desc-eq-fail-became-unreachable-nullable (type $10) + ;; CHECK-NEXT: (local $desc (ref null (exact $super.desc))) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cast-desc-eq-fail-became-unreachable-nullable + ;; The cast admits nulls and the allocation flows into the descriptor, but + ;; the reference operand becomes unreachable during the walk due to an inner + ;; failing cast of the same allocation. + (local $desc (ref null (exact $super.desc))) + (drop + (ref.cast_desc_eq (ref null (exact $super)) + (ref.cast (ref null $super) + (local.tee $desc + (struct.new $super.desc) + ) + ) + (local.get $desc) + ) + ) + ) + ;; CHECK: (func $cast-no-desc (type $11) (param $desc (ref null (exact $super.desc))) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block diff --git a/test/lit/passes/heap2local-rmw.wast b/test/lit/passes/heap2local-rmw.wast index 07fc1eadac1..4499b1e9bf9 100644 --- a/test/lit/passes/heap2local-rmw.wast +++ b/test/lit/passes/heap2local-rmw.wast @@ -1739,3 +1739,127 @@ ) ) ) + +(module + (type $s0 (shared (struct (field (mut (ref null (shared eq))))))) + ;; CHECK: (type $0 (func)) + + ;; CHECK: (type $s1 (shared (struct))) + + ;; CHECK: (type $a0 (shared (array (mut (ref null (shared eq)))))) + (type $a0 (shared (array (mut (ref null (shared eq)))))) + (type $s1 (shared (struct))) + + ;; CHECK: (type $3 (func (param (ref $a0)))) + + ;; CHECK: (func $cmpxchg-became-unreachable-ref (type $0) + ;; CHECK-NEXT: (local $x (ref null $s1)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block ;; (replaces unreachable StructCmpxchg we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref null (shared none))) + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $cmpxchg-became-unreachable-ref + ;; The allocation flows into both a failing cast in `ref` and into + ;; `expected`. During the post-order walk, `ref` is replaced with an + ;; unreachable block before the cmpxchg is visited (and before ReFinalize + ;; runs), so `visitStructCmpxchg` must check for an unreachable `ref`. + (local $x (ref null $s1)) + (drop + (struct.atomic.rmw.cmpxchg $s0 0 + (ref.cast (ref $s0) + (local.tee $x + (struct.new_default $s1) + ) + ) + (local.get $x) + (ref.null (shared none)) + ) + ) + ) + + ;; CHECK: (func $array-cmpxchg-became-unreachable-ref (type $0) + ;; CHECK-NEXT: (local $x (ref null $s1)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block ;; (replaces unreachable ArrayCmpxchg we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref null (shared none))) + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $array-cmpxchg-became-unreachable-ref + ;; Same as above, for array.atomic.rmw.cmpxchg. + (local $x (ref null $s1)) + (drop + (array.atomic.rmw.cmpxchg $a0 + (ref.cast (ref $a0) + (local.tee $x + (struct.new_default $s1) + ) + ) + (i32.const 0) + (local.get $x) + (ref.null (shared none)) + ) + ) + ) + + ;; CHECK: (func $array-cmpxchg-unreachable (type $3) (param $a (ref $a0)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (array.atomic.rmw.cmpxchg $a0 + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (block (result (ref null (shared none))) + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $array-cmpxchg-unreachable (param $a (ref $a0)) + ;; An unreachable array.atomic.rmw.cmpxchg is left for DCE. + (drop + (array.atomic.rmw.cmpxchg $a0 + (local.get $a) + (i32.const 0) + (struct.new_default $s1) + (unreachable) + ) + ) + ) +)