From 12cde6602dead7d73b04263b50cba2c17a4464b9 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 17 Jun 2026 10:24:13 -0700 Subject: [PATCH 1/4] Heap2Local: reset data structures after optimizing This entirely eliminates the class of bugs due to stale data in the `parents` and `localGraph` after a previous allocation has been optimized. --- src/passes/Heap2Local.cpp | 63 +++--- test/lit/passes/heap2local-desc.wast | 13 +- test/lit/passes/heap2local-nested-escape.wast | 44 ++++ test/lit/passes/heap2local-rmw.wast | 188 +++++++++++++++--- 4 files changed, 236 insertions(+), 72 deletions(-) create mode 100644 test/lit/passes/heap2local-nested-escape.wast diff --git a/src/passes/Heap2Local.cpp b/src/passes/Heap2Local.cpp index 59c1e7e3fc5..ef6b4d01a40 100644 --- a/src/passes/Heap2Local.cpp +++ b/src/passes/Heap2Local.cpp @@ -192,10 +192,6 @@ enum class ParentChildInteraction : int8_t { None, }; -// When we insert scratch locals, we sometimes need to record the flow between -// their set and subsequent get. -using ScratchInfo = std::unordered_map; - // Core analysis that provides an escapes() method to check if an allocation // escapes in a way that prevents optimizing it away as described above. It also // stashes information about the relevant expressions as it goes, which helps @@ -213,7 +209,6 @@ struct EscapeAnalyzer { // scratch local, then we will never look at it again after creating it and do // not need to record it here. const LazyLocalGraph& localGraph; - ScratchInfo& scratchInfo; Parents& parents; const BranchUtils::BranchTargets& branchTargets; @@ -221,13 +216,12 @@ struct EscapeAnalyzer { Module& wasm; EscapeAnalyzer(const LazyLocalGraph& localGraph, - ScratchInfo& scratchInfo, Parents& parents, const BranchUtils::BranchTargets& branchTargets, const PassOptions& passOptions, Module& wasm) - : localGraph(localGraph), scratchInfo(scratchInfo), parents(parents), - branchTargets(branchTargets), passOptions(passOptions), wasm(wasm) {} + : localGraph(localGraph), parents(parents), branchTargets(branchTargets), + passOptions(passOptions), wasm(wasm) {} // We must track all the local.sets that write the allocation, to verify // exclusivity. @@ -287,23 +281,13 @@ struct EscapeAnalyzer { } if (auto* set = parent->dynCast()) { - - // We must also look at how the value flows from those gets. Check the - // scratchInfo first because it contains sets that localGraph doesn't - // know about. - if (auto it = scratchInfo.find(set); it != scratchInfo.end()) { - auto* get = it->second; + // This is one of the sets we are written to, and so we must check for + // exclusive use of our allocation by all the gets that read the + // value. Note the set, and we will check the gets at the end once we + // know all of our sets. + sets.insert(set); + for (auto* get : localGraph.getSetInfluences(set)) { flows.push({get, parents.getParent(get)}); - } else { - // This is one of the sets we are written to, and so we must check for - // exclusive use of our allocation by all the gets that read the - // value. Note the set, and we will check the gets at the end once we - // know all of our sets. (For scratch locals above, we know all the - // sets are already accounted for.) - sets.insert(set); - for (auto* get : localGraph.getSetInfluences(set)) { - flows.push({get, parents.getParent(get)}); - } } } @@ -1206,12 +1190,6 @@ struct Struct2Local : PostWalker { 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 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.scratchInfo.insert({setRefScratch, getRefScratch}); - analyzer.parents.setParent(getRefScratch, structGet); return; } @@ -1566,14 +1544,15 @@ struct Heap2Local { Module& wasm; const PassOptions& passOptions; - LazyLocalGraph localGraph; - ScratchInfo scratchInfo; - Parents parents; - BranchUtils::BranchTargets branchTargets; + std::unique_ptr localGraph; + std::unique_ptr parents; + std::unique_ptr branchTargets; Heap2Local(Function* func, Module& wasm, const PassOptions& passOptions) - : func(func), wasm(wasm), passOptions(passOptions), localGraph(func, &wasm), - parents(func->body), branchTargets(func->body) { + : func(func), wasm(wasm), passOptions(passOptions), + localGraph(std::make_unique(func, &wasm)), + parents(std::make_unique(func->body)), + branchTargets(std::make_unique(func->body)) { // Find all the relevant allocations in the function: StructNew, ArrayNew, // ArrayNewFixed. @@ -1639,7 +1618,7 @@ struct Heap2Local { continue; } EscapeAnalyzer analyzer( - localGraph, scratchInfo, parents, branchTargets, passOptions, wasm); + *localGraph, *parents, *branchTargets, passOptions, wasm); if (!analyzer.escapes(allocation)) { // Convert the allocation and all its uses into a struct. Then convert // the struct into locals. @@ -1647,6 +1626,10 @@ struct Heap2Local { Array2Struct(allocation, analyzer, func, wasm).structNew; Struct2Local(structNew, analyzer, func, wasm); optimized = true; + localGraph = std::make_unique(func, &wasm); + parents = std::make_unique(func->body); + branchTargets = + std::make_unique(func->body); } } @@ -1659,10 +1642,14 @@ struct Heap2Local { // Check for escaping, noting relevant information as we go. If this does // not escape, optimize it into locals. EscapeAnalyzer analyzer( - localGraph, scratchInfo, parents, branchTargets, passOptions, wasm); + *localGraph, *parents, *branchTargets, passOptions, wasm); if (!analyzer.escapes(allocation)) { Struct2Local(allocation, analyzer, func, wasm); optimized = true; + localGraph = std::make_unique(func, &wasm); + parents = std::make_unique(func->body); + branchTargets = + std::make_unique(func->body); } } diff --git a/test/lit/passes/heap2local-desc.wast b/test/lit/passes/heap2local-desc.wast index b6467beb544..f0d223ef1d4 100644 --- a/test/lit/passes/heap2local-desc.wast +++ b/test/lit/passes/heap2local-desc.wast @@ -352,11 +352,16 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (if (result nullref) - ;; CHECK-NEXT: (ref.eq - ;; CHECK-NEXT: (block (result nullref) - ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (then ;; CHECK-NEXT: (ref.null none) diff --git a/test/lit/passes/heap2local-nested-escape.wast b/test/lit/passes/heap2local-nested-escape.wast new file mode 100644 index 00000000000..ac0b07f7975 --- /dev/null +++ b/test/lit/passes/heap2local-nested-escape.wast @@ -0,0 +1,44 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt -all --heap2local -S -o - %s | filecheck %s + +(module + ;; CHECK: (type $inner (struct (field i32))) + (type $inner (struct (field i32))) + ;; CHECK: (type $outer (struct (field (mut (ref null $inner))))) + (type $outer (struct (field (mut (ref null $inner))))) + + ;; CHECK: (func $test (type $1) (result (ref null $inner)) + ;; CHECK-NEXT: (local $x (ref null $outer)) + ;; CHECK-NEXT: (local $1 (ref null $inner)) + ;; 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: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (struct.new $inner + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result (ref null $inner)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (result (ref null $inner)) + (local $x (ref null $outer)) + (local.set $x (struct.new_default $outer)) + (struct.set $outer 0 (local.get $x) (struct.new $inner (i32.const 42))) + (struct.get $outer 0 (local.get $x)) + ) +) diff --git a/test/lit/passes/heap2local-rmw.wast b/test/lit/passes/heap2local-rmw.wast index 7d329d2b53e..6a7aaca7bd6 100644 --- a/test/lit/passes/heap2local-rmw.wast +++ b/test/lit/passes/heap2local-rmw.wast @@ -961,12 +961,14 @@ ;; 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: (local $2 i32) + ;; CHECK-NEXT: (local $3 i32) + ;; CHECK-NEXT: (local $4 (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 $2 + ;; CHECK-NEXT: (local.set $4 ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (ref.null none) @@ -981,15 +983,21 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (array.new_default $array - ;; CHECK-NEXT: (i32.const 2) + ;; 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: (ref.null none) ;; 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 $2) + ;; CHECK-NEXT: (local.get $4) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -1117,7 +1125,9 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (array.new_fixed $inner 0) + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (block (result eqref) ;; CHECK-NEXT: (drop @@ -1169,25 +1179,32 @@ ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result nullref) ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $4 - ;; CHECK-NEXT: (array.new_fixed $inner 0) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.set $2 ;; CHECK-NEXT: (local.get $1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (ref.eq - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (local.set $1 - ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -1252,25 +1269,32 @@ ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (drop ;; 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: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) ;; 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: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (local.set $0 - ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -1313,25 +1337,32 @@ ;; CHECK-NEXT: (ref.null (shared none)) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (drop ;; 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: (drop + ;; CHECK-NEXT: (block (result (ref null (shared none))) + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) ;; 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: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (local.set $0 - ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null (shared none)) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -1609,3 +1640,100 @@ ) ) ) + +(module + ;; CHECK: (type $struct (shared (struct (field (mut (ref null $struct)))))) + (type $struct (shared (struct (field (mut (ref null $struct)))))) + ;; CHECK: (type $1 (func (result i32))) + + ;; CHECK: (func $must-optimize-ref-eq (type $1) (result i32) + ;; CHECK-NEXT: (local $local (ref null $struct)) + ;; CHECK-NEXT: (local $1 (ref null $struct)) + ;; CHECK-NEXT: (local $2 (ref null $struct)) + ;; CHECK-NEXT: (local $3 (ref null (shared eq))) + ;; CHECK-NEXT: (local $4 (ref null $struct)) + ;; CHECK-NEXT: (local $5 (ref null $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref null $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref null (shared none))) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref null (shared none))) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (struct.new_default $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.is_null + ;; CHECK-NEXT: (block (result (ref null $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $must-optimize-ref-eq (result i32) + (local $local (ref null $struct)) + (drop + ;; 2. Next this will be optimized. The local from (1) representing field 0 + ;; will be compared with a local holding the expected value below. This + ;; ref.eq comparison should fail if we optimize correctly, leaving the + ;; local from (1) with its default null value. + (struct.atomic.rmw.cmpxchg acqrel acqrel $struct 0 + (local.tee $local + ;; 1. This will be optimized. The local representing field 0 will be + ;; set to null. + (struct.new_default $struct) + ) + ;; 3. This is the next allocation to be optimized. When it is replaced + ;; with a ref.null, it would cause the ref.eq comparison created in (2) + ;; to start succeeding incorrectly, except that we optimize the ref.eq + ;; when we see that this allocation flows only into one side of it. + (struct.new_default $struct) ;; Expected + ;; 4. This allocation is not optimized, so we would end up incorrectly + ;; writing this non-null value into the local from (1) if we had not + ;; optimized the ref.eq. + (struct.new_default $struct) ;; Replacement + ) + ) + (ref.is_null + ;; This is replaced with a get of the local from (1), which would + ;; incorrectly contain the non-null replacement value if we had not + ;; optimized the ref.eq. + (struct.get $struct 0 (local.get $local)) + ) + ) +) From cd5c8a6e43856d1e8cb0829c7102b52b4ce979f5 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 18 Jun 2026 14:11:38 -0700 Subject: [PATCH 2/4] helper function --- src/passes/Heap2Local.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/passes/Heap2Local.cpp b/src/passes/Heap2Local.cpp index ef6b4d01a40..ba4f3409164 100644 --- a/src/passes/Heap2Local.cpp +++ b/src/passes/Heap2Local.cpp @@ -1626,10 +1626,7 @@ struct Heap2Local { Array2Struct(allocation, analyzer, func, wasm).structNew; Struct2Local(structNew, analyzer, func, wasm); optimized = true; - localGraph = std::make_unique(func, &wasm); - parents = std::make_unique(func->body); - branchTargets = - std::make_unique(func->body); + resetAnalysisData(); } } @@ -1646,10 +1643,7 @@ struct Heap2Local { if (!analyzer.escapes(allocation)) { Struct2Local(allocation, analyzer, func, wasm); optimized = true; - localGraph = std::make_unique(func, &wasm); - parents = std::make_unique(func->body); - branchTargets = - std::make_unique(func->body); + resetAnalysisData(); } } @@ -1660,6 +1654,15 @@ struct Heap2Local { EHUtils::handleBlockNestedPops(func, wasm); } } + + void resetAnalysisData() { + // Optimizations may add new locals and parent-child relationships. Rather + // than trying to keep these data structures up-to-date, just clear and + // recompute them after each optimization. + localGraph = std::make_unique(func, &wasm); + parents = std::make_unique(func->body); + branchTargets = std::make_unique(func->body); + } }; struct Heap2LocalPass : public WalkerPass> { From 5f561084cd5e7a9eebeb5b47a71633da7497e295 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 18 Jun 2026 14:12:25 -0700 Subject: [PATCH 3/4] remove redundant test --- test/lit/passes/heap2local-nested-escape.wast | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 test/lit/passes/heap2local-nested-escape.wast diff --git a/test/lit/passes/heap2local-nested-escape.wast b/test/lit/passes/heap2local-nested-escape.wast deleted file mode 100644 index ac0b07f7975..00000000000 --- a/test/lit/passes/heap2local-nested-escape.wast +++ /dev/null @@ -1,44 +0,0 @@ -;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. -;; RUN: wasm-opt -all --heap2local -S -o - %s | filecheck %s - -(module - ;; CHECK: (type $inner (struct (field i32))) - (type $inner (struct (field i32))) - ;; CHECK: (type $outer (struct (field (mut (ref null $inner))))) - (type $outer (struct (field (mut (ref null $inner))))) - - ;; CHECK: (func $test (type $1) (result (ref null $inner)) - ;; CHECK-NEXT: (local $x (ref null $outer)) - ;; CHECK-NEXT: (local $1 (ref null $inner)) - ;; 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: (block - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.null none) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $1 - ;; CHECK-NEXT: (struct.new $inner - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (block (result (ref null $inner)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.null none) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test (result (ref null $inner)) - (local $x (ref null $outer)) - (local.set $x (struct.new_default $outer)) - (struct.set $outer 0 (local.get $x) (struct.new $inner (i32.const 42))) - (struct.get $outer 0 (local.get $x)) - ) -) From d9db764e6cce8d9be82cb2d8425d391a965c8b70 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 18 Jun 2026 15:39:22 -0700 Subject: [PATCH 4/4] update comments --- test/lit/passes/heap2local-desc.wast | 5 ++++- test/lit/passes/heap2local-rmw.wast | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/test/lit/passes/heap2local-desc.wast b/test/lit/passes/heap2local-desc.wast index f0d223ef1d4..61af26efd10 100644 --- a/test/lit/passes/heap2local-desc.wast +++ b/test/lit/passes/heap2local-desc.wast @@ -374,7 +374,10 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $cast-desc-eq-fail-reverse (param $desc (ref null (exact $super.desc))) - ;; Same as above, but change where the parameter is used. + ;; Same as above, but change where the parameter is used. We can optimize + ;; more because we do not have one non-escaping allocation flowing into + ;; another non-escaping allocation (which would currently require multiple + ;; runs to fully optimize). (drop (ref.cast_desc_eq (ref (exact $super)) (struct.new_desc $super diff --git a/test/lit/passes/heap2local-rmw.wast b/test/lit/passes/heap2local-rmw.wast index 6a7aaca7bd6..07fc1eadac1 100644 --- a/test/lit/passes/heap2local-rmw.wast +++ b/test/lit/passes/heap2local-rmw.wast @@ -1148,7 +1148,9 @@ (drop ;; Then `expected` gets optimized, creating a new scratch local. Since the ;; flower is already created, the index of the scratch local would be out - ;; of bounds if we tried to look it up in the LocalGraph. + ;; of bounds if we tried to look it up in the original LocalGraph. This + ;; still works fine because we reset the LocalGraph after each + ;; optimization. (struct.atomic.rmw.cmpxchg $struct 0 (struct.new_default $struct) (array.new_fixed $inner 0)