diff --git a/src/passes/Heap2Local.cpp b/src/passes/Heap2Local.cpp index 39c77bec489..7aec23ccf1a 100644 --- a/src/passes/Heap2Local.cpp +++ b/src/passes/Heap2Local.cpp @@ -349,6 +349,12 @@ struct EscapeAnalyzer { void visitLocalSet(LocalSet* curr) { escapes = false; } // Reference operations. TODO add more + void visitRefIsNull(RefIsNull* curr) { + // The reference is compared to null, but nothing more. + escapes = false; + fullyConsumes = true; + } + void visitRefEq(RefEq* curr) { // The reference is compared for identity, but nothing more. escapes = false; @@ -367,6 +373,11 @@ struct EscapeAnalyzer { } } + void visitRefTest(RefTest* curr) { + escapes = false; + fullyConsumes = true; + } + void visitRefCast(RefCast* curr) { // As it is our allocation that flows through here, we need to // check that the cast will not trap, so that we can continue @@ -707,6 +718,17 @@ struct Struct2Local : PostWalker { replaceCurrent(builder.makeBlock(contents)); } + void visitRefIsNull(RefIsNull* curr) { + if (!analyzer.reached.count(curr)) { + return; + } + + // The result must be 0, since the allocation is not null. Drop the RefIs + // and append that. + replaceCurrent(builder.makeSequence( + builder.makeDrop(curr), builder.makeConst(Literal(int32_t(0))))); + } + void visitRefEq(RefEq* curr) { if (!analyzer.reached.count(curr)) { return; @@ -739,6 +761,23 @@ struct Struct2Local : PostWalker { replaceCurrent(curr->value); } + void visitRefTest(RefTest* curr) { + if (!analyzer.reached.count(curr)) { + return; + } + + // This test operates on the allocation, which means we can compute whether + // it will succeed statically. We do not even need + // GCTypeUtils::evaluateCastCheck because we know the allocation's type + // precisely (it cannot be a strict subtype of the type - it is the type). + int32_t result = Type::isSubType(allocation->type, curr->castType); + // Remove the RefTest and leave only its reference child. If we kept it, + // we'd need to refinalize (as the input to the test changes, since the + // reference becomes a null, which has a different type). + replaceCurrent(builder.makeSequence(builder.makeDrop(curr->ref), + builder.makeConst(Literal(result)))); + } + void visitRefCast(RefCast* curr) { if (!analyzer.reached.count(curr)) { return; @@ -820,12 +859,15 @@ struct Array2Struct : PostWalker { EscapeAnalyzer& analyzer; Function* func; Builder builder; + // The original type of the allocation, before we turn it into a struct. + Type originalType; Array2Struct(Expression* allocation, EscapeAnalyzer& analyzer, Function* func, Module& wasm) - : allocation(allocation), analyzer(analyzer), func(func), builder(wasm) { + : allocation(allocation), analyzer(analyzer), func(func), builder(wasm), + originalType(allocation->type) { // Build the struct type we need: as many fields as the size of the array, // all of the same type as the array's element. @@ -989,6 +1031,25 @@ struct Array2Struct : PostWalker { noteCurrentIsReached(); } + // Some additional operations need special handling + void visitRefTest(RefTest* curr) { + if (!analyzer.reached.count(curr)) { + return; + } + + // When we ref.test an array allocation, we cannot simply turn the array + // into a struct, as then the test will behave different. (Note that this is + // not a problem for ref.*cast*, as the cast simply goes away when the value + // flows through, and we verify it will do so in the escape analysis.) To + // handle this, check if the test succeeds or not, and write out the outcome + // here (similar to Struct2Local::visitRefTest). Note that we test on + // |originalType| here and not |allocation->type|, as the allocation has + // been turned into a struct. + int32_t result = Type::isSubType(originalType, curr->castType); + replaceCurrent(builder.makeSequence(builder.makeDrop(curr), + builder.makeConst(Literal(result)))); + } + // Get the value in an expression we know must contain a constant index. Index getIndex(Expression* curr) { return curr->cast()->value.getUnsigned(); diff --git a/test/lit/passes/heap2local.wast b/test/lit/passes/heap2local.wast index bd613e18c4b..d642aabb3da 100644 --- a/test/lit/passes/heap2local.wast +++ b/test/lit/passes/heap2local.wast @@ -4,8 +4,10 @@ ;; RUN: foreach %s %t wasm-opt -all --remove-unused-names --heap2local -S -o - | filecheck %s (module - ;; CHECK: (type $struct.A (struct (field (mut i32)) (field (mut f64)))) - (type $struct.A (struct (field (mut i32)) (field (mut f64)))) + ;; CHECK: (type $struct.A (sub (struct (field (mut i32)) (field (mut f64))))) + (type $struct.A (sub (struct (field (mut i32)) (field (mut f64))))) + + (type $struct.B (sub $struct.A (struct (field (mut i32)) (field (mut f64))))) ;; CHECK: (type $1 (func)) @@ -40,6 +42,10 @@ ;; CHECK: (type $13 (func (param eqref eqref) (result i32))) + ;; CHECK: (type $14 (func (param anyref) (result i32))) + + ;; CHECK: (type $15 (func (param anyref))) + ;; CHECK: (func $simple (type $1) ;; CHECK-NEXT: (local $0 i32) ;; CHECK-NEXT: (local $1 f64) @@ -2382,6 +2388,251 @@ ) ) ) + + ;; CHECK: (func $ref-is (type $14) (param $x anyref) (result i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 f64) + ;; CHECK-NEXT: (local $3 i32) + ;; CHECK-NEXT: (local $4 f64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.is_null + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.is_null + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (f64.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref-is (param $x anyref) (result i32) + ;; A ref.is that we can do nothing for, and should not modify, even though + ;; we optimize later. + (drop + (ref.is_null + (local.get $x) + ) + ) + ;; The result is 0 as the allocation is not null, and we can remove the + ;; allocation. + (ref.is_null + (struct.new $struct.A + (i32.const 0) + (f64.const 0) + ) + ) + ) + + ;; CHECK: (func $ref-test (type $1) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 f64) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local $3 f64) + ;; CHECK-NEXT: (local $4 i32) + ;; CHECK-NEXT: (local $5 f64) + ;; CHECK-NEXT: (local $6 i32) + ;; CHECK-NEXT: (local $7 f64) + ;; CHECK-NEXT: (local $8 i32) + ;; CHECK-NEXT: (local $9 f64) + ;; CHECK-NEXT: (local $10 i32) + ;; CHECK-NEXT: (local $11 f64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result 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: (f64.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: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $7 + ;; CHECK-NEXT: (f64.const 2.2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (local.get $6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (local.get $7) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $10 + ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $11 + ;; CHECK-NEXT: (f64.const 4.4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $8 + ;; CHECK-NEXT: (local.get $10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $9 + ;; CHECK-NEXT: (local.get $11) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref-test + ;; This cast must succeed (it tests the exact type), and we can remove the + ;; allocation. + (drop + (ref.test (ref $struct.A) + (struct.new $struct.A + (i32.const 0) + (f64.const 0) + ) + ) + ) + ;; Testing a supertype also works. + (drop + (ref.test (ref null $struct.A) + (struct.new $struct.A + (i32.const 1) + (f64.const 2.2) + ) + ) + ) + (drop + (ref.test (ref null any) + (struct.new $struct.A + (i32.const 3) + (f64.const 4.4) + ) + ) + ) + ) + ;; CHECK: (func $ref-test-bad (type $15) (param $x anyref) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 f64) + ;; CHECK-NEXT: (local $3 i32) + ;; CHECK-NEXT: (local $4 f64) + ;; CHECK-NEXT: (local $5 i32) + ;; CHECK-NEXT: (local $6 f64) + ;; CHECK-NEXT: (local $7 i32) + ;; CHECK-NEXT: (local $8 f64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.test (ref $struct.A) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (f64.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $7 + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $8 + ;; CHECK-NEXT: (f64.const 2.2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (local.get $7) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (local.get $8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref-test-bad (param $x anyref) + ;; We know nothing about this cast. + (drop + (ref.test (ref $struct.A) + (local.get $x) + ) + ) + ;; These casts must fail, but we can remove the allocation. + (drop + (ref.test (ref $struct.B) + (struct.new $struct.A + (i32.const 0) + (f64.const 0) + ) + ) + ) + (drop + (ref.test (ref null $struct.B) + (struct.new $struct.A + (i32.const 1) + (f64.const 2.2) + ) + ) + ) + ) ) (module @@ -2662,9 +2913,9 @@ ;; CHECK: (type $1 (struct (field (mut i32)))) - ;; CHECK: (type $2 (func (result i32))) + ;; CHECK: (type $2 (func)) - ;; CHECK: (type $3 (func)) + ;; CHECK: (type $3 (func (result i32))) ;; CHECK: (type $4 (func (param i32) (result i32))) @@ -2678,7 +2929,9 @@ ;; CHECK: (type $9 (struct (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)) (field (mut i32)))) - ;; CHECK: (func $array.new_default (type $3) + ;; CHECK: (type $10 (func (param anyref))) + + ;; CHECK: (func $array.new_default (type $2) ;; CHECK-NEXT: (local $temp (ref $array)) ;; CHECK-NEXT: (local $1 i32) ;; CHECK-NEXT: (local $2 i32) @@ -2818,7 +3071,7 @@ ) ) - ;; CHECK: (func $array.new (type $2) (result i32) + ;; CHECK: (func $array.new (type $3) (result i32) ;; CHECK-NEXT: (local $temp (ref $array)) ;; CHECK-NEXT: (local $1 i32) ;; CHECK-NEXT: (local $2 i32) @@ -3000,7 +3253,7 @@ ) ) - ;; CHECK: (func $array.local.super (type $3) + ;; CHECK: (func $array.local.super (type $2) ;; CHECK-NEXT: (local $temp anyref) ;; CHECK-NEXT: (local $1 i32) ;; CHECK-NEXT: (local $2 i32) @@ -3049,7 +3302,7 @@ ) ) - ;; CHECK: (func $array.folded (type $2) (result i32) + ;; CHECK: (func $array.folded (type $3) (result i32) ;; CHECK-NEXT: (local $0 i32) ;; CHECK-NEXT: (local $1 i32) ;; CHECK-NEXT: (local $2 i32) @@ -3130,7 +3383,7 @@ ) ) - ;; CHECK: (func $array.folded.multiple (type $3) + ;; CHECK: (func $array.folded.multiple (type $2) ;; CHECK-NEXT: (local $0 i32) ;; CHECK-NEXT: (local $1 i32) ;; CHECK-NEXT: (local $2 i32) @@ -3383,7 +3636,7 @@ ) ) - ;; CHECK: (func $array.nested.refinalize.get (type $2) (result i32) + ;; CHECK: (func $array.nested.refinalize.get (type $3) (result i32) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result nullref) ;; CHECK-NEXT: (ref.null none) @@ -3403,7 +3656,7 @@ ) ) - ;; CHECK: (func $array.nested.refinalize.set (type $3) + ;; CHECK: (func $array.nested.refinalize.set (type $2) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result nullref) ;; CHECK-NEXT: (ref.null none) @@ -3425,7 +3678,7 @@ ) ) - ;; CHECK: (func $array.flowing.type (type $2) (result i32) + ;; CHECK: (func $array.flowing.type (type $3) (result i32) ;; CHECK-NEXT: (local $temp (ref $array)) ;; CHECK-NEXT: (local $1 i32) ;; CHECK-NEXT: (local $2 i32) @@ -3511,13 +3764,123 @@ ) ) - ;; CHECK: (func $get-i32 (type $2) (result i32) + ;; CHECK: (func $get-i32 (type $3) (result i32) ;; CHECK-NEXT: (i32.const 1337) ;; CHECK-NEXT: ) (func $get-i32 (result i32) ;; Helper for the above. (i32.const 1337) ) + + ;; CHECK: (func $ref-test (type $2) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; 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: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref-test + ;; This cast must succeed (it tests the exact type), and we can remove the + ;; allocation. + (drop + (ref.test (ref $array) + (array.new_default $array + (i32.const 1) + ) + ) + ) + ;; Testing a supertype also works. + (drop + (ref.test (ref null any) + (array.new_default $array + (i32.const 2) + ) + ) + ) + ) + ;; CHECK: (func $ref-test-bad (type $10) (param $x anyref) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.test (ref $array) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result nullref) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref-test-bad (param $x anyref) + ;; We know nothing about this cast. + (drop + (ref.test (ref $array) + (local.get $x) + ) + ) + ;; This cast fails, but we can remove the allocation. + (drop + (ref.test (ref struct) + (array.new_default $array + (i32.const 2) + ) + ) + ) + ) ) ;; Arrays with reference values.