Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions src/passes/Heap2Local.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -379,11 +379,13 @@ struct EscapeAnalyzer {
}

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
// to (hopefully) optimize this allocation.
if (Type::isSubType(allocation->type, curr->type)) {
escapes = false;
// Whether the cast succeeds or fails, it does not escape.
escapes = false;

// If the cast fails then the allocation is fully consumed and does not
// flow any further (instead, we trap).
if (!Type::isSubType(allocation->type, curr->type)) {
fullyConsumes = true;
}
}

Expand Down Expand Up @@ -783,24 +785,22 @@ struct Struct2Local : PostWalker<Struct2Local> {
return;
}

// It is safe to optimize out this RefCast, since we proved it
// contains our allocation and we have checked that the type of
// the allocation is a subtype of the type of the cast, and so
// cannot trap.
replaceCurrent(curr->ref);
// We know this RefCast receives our allocation, so we can see whether it
// succeeds or fails.
if (Type::isSubType(allocation->type, curr->type)) {
// The cast succeeds, so it is a no-op, and we can skip it, since after we
// remove the allocation it will not even be needed for validation.
replaceCurrent(curr->ref);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if the cast is known to succeed, we might still need it to make the static types work out, right? This replacement is only valid if curr->ref->type is a subtype of curr->type, not necessarily if allocation->type is a subtype.

This isn't a new issue, so I must be missing something that makes this safe.

Aha, it's because we're going to remove the allocation entirely. This is worth a comment!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I added a comment.

} else {
// The cast fails, so this must trap.
replaceCurrent(builder.makeSequence(builder.makeDrop(curr->ref),
builder.makeUnreachable()));
}

// We need to refinalize after this, as while we know the cast is not
// logically needed - the value flowing through will not be used - we do
// need validation to succeed even before other optimizations remove the
// code. For example:
//
// (block (result $B)
// (ref.cast $B
// (block (result $A)
//
// Without the cast this does not validate, so we need to refinalize
// (which will fix this, as we replace the unused value with a null, so
// that type will propagate out).
// Either way, we need to refinalize here (we either added an unreachable,
// or we replaced a cast with the value being cast, which may have a less-
// refined type - it will not be used after we remove the allocation, but we
// must still fix that up for validation).
refinalize = true;
}

Expand Down
104 changes: 89 additions & 15 deletions test/lit/passes/heap2local.wast
Original file line number Diff line number Diff line change
Expand Up @@ -2640,24 +2640,34 @@
(type $A (sub (struct (field (ref null $A)))))
;; CHECK: (type $1 (func (result anyref)))

;; CHECK: (type $B (sub $A (struct (field (ref $A)))))
(type $B (sub $A (struct (field (ref $A)))))
;; CHECK: (type $B (sub $A (struct (field (ref $A)) (field i32))))
(type $B (sub $A (struct (field (ref $A)) (field i32))))

;; CHECK: (type $3 (func (result i32)))

;; CHECK: (func $func (type $1) (result anyref)
;; CHECK-NEXT: (local $a (ref $A))
;; CHECK-NEXT: (local $1 (ref $A))
;; CHECK-NEXT: (local $2 (ref $A))
;; CHECK-NEXT: (local $2 i32)
;; CHECK-NEXT: (local $3 (ref $A))
;; CHECK-NEXT: (local $4 i32)
;; CHECK-NEXT: (ref.cast (ref $B)
;; CHECK-NEXT: (block (result (ref $A))
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (block (result nullref)
;; CHECK-NEXT: (local.set $2
;; CHECK-NEXT: (local.set $3
;; CHECK-NEXT: (struct.new $A
;; CHECK-NEXT: (ref.null none)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $4
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $1
;; CHECK-NEXT: (local.get $2)
;; CHECK-NEXT: (local.get $3)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $2
;; CHECK-NEXT: (local.get $4)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes are what I meant in the second paragraph - $B has an extra field now, which is also handled here (with value 1). This does not affect what the test is testing.

;; CHECK-NEXT: )
;; CHECK-NEXT: (ref.null none)
;; CHECK-NEXT: )
Expand All @@ -2680,6 +2690,7 @@
(struct.new $A
(ref.null none)
)
(i32.const 1)
)
)
)
Expand All @@ -2688,16 +2699,24 @@

;; CHECK: (func $cast-success (type $1) (result anyref)
;; CHECK-NEXT: (local $0 (ref $A))
;; CHECK-NEXT: (local $1 (ref $A))
;; CHECK-NEXT: (local $1 i32)
;; CHECK-NEXT: (local $2 (ref $A))
;; CHECK-NEXT: (local $3 i32)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (block (result nullref)
;; CHECK-NEXT: (local.set $1
;; CHECK-NEXT: (local.set $2
;; CHECK-NEXT: (struct.new $A
;; CHECK-NEXT: (ref.null none)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $3
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (local.set $0
;; CHECK-NEXT: (local.get $1)
;; 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: )
Expand All @@ -2711,25 +2730,80 @@
(struct.new $A
(ref.null none)
)
(i32.const 1)
)
)
)
)
;; CHECK: (func $cast-failure (type $1) (result anyref)
;; CHECK-NEXT: (struct.get $B 0
;; CHECK-NEXT: (ref.cast (ref $B)
;; CHECK-NEXT: (struct.new $A
;; CHECK-NEXT: (struct.new $A
;; CHECK-NEXT: (ref.null none)
;; CHECK-NEXT: (local $0 (ref null $A))
;; CHECK-NEXT: (local $1 (ref null $A))
;; CHECK-NEXT: (block ;; (replaces unreachable StructGet we can't emit)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (block
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (block (result nullref)
;; CHECK-NEXT: (local.set $1
;; CHECK-NEXT: (struct.new $A
;; CHECK-NEXT: (ref.null none)
;; CHECK-NEXT: )
;; 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: (unreachable)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (unreachable)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $cast-failure (result anyref)
(struct.get $B 0
;; The allocated $A arrives here, but the cast will fail, so we do not
;; optimize.
;; The allocated $A arrives here, but the cast will fail. We can remove
;; the allocation and put an unreachable here. (Note that the inner
;; struct.new survives, which would take another cycle to remove.)
(ref.cast (ref $B)
(struct.new $A
(struct.new $A
(ref.null none)
)
)
)
)
)

;; CHECK: (func $cast-failure-nofield (type $3) (result i32)
;; CHECK-NEXT: (local $0 (ref null $A))
;; CHECK-NEXT: (local $1 (ref null $A))
;; CHECK-NEXT: (block ;; (replaces unreachable StructGet we can't emit)
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (block
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (block (result nullref)
;; CHECK-NEXT: (local.set $1
;; CHECK-NEXT: (struct.new $A
;; CHECK-NEXT: (ref.null none)
;; CHECK-NEXT: )
;; 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: (unreachable)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (unreachable)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $cast-failure-nofield (result i32)
;; As above, but we read from a field that only exists in $B, despite the
;; allocation that flows here being an $A. We should not error on that.
(struct.get $B 1 ;; this changes from 0 to 1
(ref.cast (ref $B)
(struct.new $A
(struct.new $A
Expand Down