From e4d2f1265fc46960c59e7995a23763b6be94865a Mon Sep 17 00:00:00 2001 From: Yi LIU Date: Thu, 12 Feb 2026 09:22:32 +0800 Subject: [PATCH] Fix Array2Struct missing handlers for ArrayRMW and ArrayCmpxchg The escape analysis in Heap2Local accepted ArrayRMW and ArrayCmpxchg as FullyConsumes, allowing non-escaping arrays with these operations through to the Array2Struct transformation. However, Array2Struct had no visitArrayRMW or visitArrayCmpxchg handlers, so these operations were left unrewritten after the array-to-struct conversion, producing broken IR where array operations referenced a now-nonexistent array allocation. This caused the operations to be silently replaced with unreachable blocks, turning working code into unconditional traps. Fix by: 1. Adding const-index checks to the escape analysis for ArrayRMW and ArrayCmpxchg (matching the existing checks for ArrayGet and ArraySet), since Array2Struct requires compile-time-known indices. 2. Adding visitArrayRMW and visitArrayCmpxchg to Array2Struct that convert array atomic operations to their struct equivalents (following the pattern of visitArrayGet and visitArraySet), with proper OOB handling. --- src/passes/Heap2Local.cpp | 45 +++++++++ test/lit/passes/heap2local-rmw.wast | 141 +++++++++++++++++++++++++++- 2 files changed, 185 insertions(+), 1 deletion(-) diff --git a/src/passes/Heap2Local.cpp b/src/passes/Heap2Local.cpp index 467812df00b..59cde812603 100644 --- a/src/passes/Heap2Local.cpp +++ b/src/passes/Heap2Local.cpp @@ -462,12 +462,18 @@ struct EscapeAnalyzer { fullyConsumes = true; } void visitArrayRMW(ArrayRMW* curr) { + if (!curr->index->is()) { + return; + } if (curr->ref == child) { escapes = false; fullyConsumes = true; } } void visitArrayCmpxchg(ArrayCmpxchg* curr) { + if (!curr->index->is()) { + return; + } if (curr->ref == child || curr->expected == child) { escapes = false; fullyConsumes = true; @@ -1343,6 +1349,45 @@ struct Array2Struct : PostWalker { index, curr->ref, MemoryOrder::Unordered, curr->type, curr->signed_)); } + void visitArrayRMW(ArrayRMW* curr) { + if (analyzer.getInteraction(curr) == ParentChildInteraction::None) { + return; + } + + auto index = getIndex(curr->index); + if (index >= numFields) { + replaceCurrent(builder.makeBlock({builder.makeDrop(curr->ref), + builder.makeDrop(curr->value), + builder.makeUnreachable()})); + refinalize = true; + return; + } + + // Convert the ArrayRMW into a StructRMW. + replaceCurrent(builder.makeStructRMW( + curr->op, index, curr->ref, curr->value, curr->order)); + } + + void visitArrayCmpxchg(ArrayCmpxchg* curr) { + if (analyzer.getInteraction(curr) == ParentChildInteraction::None) { + return; + } + + auto index = getIndex(curr->index); + if (index >= numFields) { + replaceCurrent(builder.makeBlock({builder.makeDrop(curr->ref), + builder.makeDrop(curr->expected), + builder.makeDrop(curr->replacement), + builder.makeUnreachable()})); + refinalize = true; + return; + } + + // Convert the ArrayCmpxchg into a StructCmpxchg. + replaceCurrent(builder.makeStructCmpxchg( + index, curr->ref, curr->expected, curr->replacement, curr->order)); + } + // Some additional operations need special handling void visitRefTest(RefTest* curr) { diff --git a/test/lit/passes/heap2local-rmw.wast b/test/lit/passes/heap2local-rmw.wast index 33fef1cb506..bb7ecc8cd77 100644 --- a/test/lit/passes/heap2local-rmw.wast +++ b/test/lit/passes/heap2local-rmw.wast @@ -7,7 +7,6 @@ (type $i64 (struct (field (mut i64)))) ;; CHECK: (type $struct (struct (field (mut (ref null $struct))))) (type $struct (struct (field (mut (ref null $struct))))) - ;; CHECK: (type $1 (func (result i32))) ;; CHECK: (type $2 (func (result i64))) @@ -16,6 +15,11 @@ ;; CHECK: (type $4 (func (param (ref null $struct) (ref null $struct)) (result (ref null $struct)))) + ;; CHECK: (type $5 (func (param i32) (result i32))) + + ;; CHECK: (type $arr (array (mut i32))) + (type $arr (array (mut i32))) + ;; CHECK: (func $escape-rmw (type $3) (param $0 (ref null $struct)) (result (ref null $struct)) ;; CHECK-NEXT: (struct.atomic.rmw.xchg $struct 0 ;; CHECK-NEXT: (local.get $0) @@ -693,4 +697,139 @@ (i32.const 2) ) ) + + ;; CHECK: (func $array-rmw-add (type $1) (result i32) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local $3 i32) + ;; CHECK-NEXT: (local $4 i32) + ;; CHECK-NEXT: (local $5 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + (func $array-rmw-add (result i32) + ;; Array atomic RMW on a non-escaping fixed-size array should be + ;; optimized: the array is converted to a struct, then to locals. + (array.atomic.rmw.add $arr + (array.new_fixed $arr 2 + (i32.const 0) + (i32.const 0) + ) + (i32.const 0) + (i32.const 1) + ) + ) + + ;; CHECK: (func $array-cmpxchg (type $1) (result i32) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local $3 i32) + ;; CHECK-NEXT: (local $4 i32) + ;; CHECK-NEXT: (local $5 i32) + ;; CHECK-NEXT: (local $6 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (local.get $6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + (func $array-cmpxchg (result i32) + ;; Array atomic cmpxchg on a non-escaping fixed-size array should be + ;; optimized similarly. + (array.atomic.rmw.cmpxchg $arr + (array.new_fixed $arr 2 + (i32.const 0) + (i32.const 0) + ) + (i32.const 0) + (i32.const 10) + (i32.const 20) + ) + ) + + ;; CHECK: (func $array-rmw-nonconstant-index (type $5) (param $idx i32) (result i32) + ;; CHECK-NEXT: (array.atomic.rmw.add $arr + ;; CHECK-NEXT: (array.new_fixed $arr 2 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $idx) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $array-rmw-nonconstant-index (param $idx i32) (result i32) + ;; A non-constant index prevents the optimization, since Array2Struct + ;; needs to know which struct field to access at compile time. + (array.atomic.rmw.add $arr + (array.new_fixed $arr 2 + (i32.const 0) + (i32.const 0) + ) + (local.get $idx) + (i32.const 1) + ) + ) )