From 59af5a5f871b8be5f2d2f767084296a66ebf06f0 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Sat, 17 Oct 2026 07:12:33 -0700 Subject: [PATCH] [subtype-exprs] Fix shared constraints on unreachable RefEqs SubtypingDiscoverer::visitRefEq checked curr->type != Type::unreachable and read only curr->left->type to determine whether its operands must be subtypes of shared or unshared eqref. When one operand of a RefEq is unreachable and the other operand is a shared reference, curr->type is Type::unreachable, causing visitRefEq to constrain the shared operand to unshared eqref and trip the Type::isSubType(curr->type, type) assertion in TranslateToFuzzReader::mutate. Check both operands of RefEq for shared reference types when determining the eqref shareability constraint. Add unit tests as well as new assertions that allow an Unsbutyping regression test to catch the problem. --- src/ir/subtype-exprs.h | 11 +++++--- src/passes/Unsubtyping.cpp | 2 ++ test/gtest/subtype-exprs.cpp | 46 ++++++++++++++++++++++++++++++++ test/lit/passes/unsubtyping.wast | 34 +++++++++++++++++++++++ 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/ir/subtype-exprs.h b/src/ir/subtype-exprs.h index a9168061476..60e4628a7c1 100644 --- a/src/ir/subtype-exprs.h +++ b/src/ir/subtype-exprs.h @@ -235,11 +235,14 @@ struct SubtypingDiscoverer : public OverriddenVisitor { void visitRefEq(RefEq* curr) { // Match the shareability of the current content (if it exists). // TODO: This could also allow both sides to flip. - HeapType eq = HeapType::eq; - if (curr->type != Type::unreachable) { - eq = eq.getBasic(curr->left->type.getHeapType().getShared()); + auto share = Unshared; + if ((curr->left->type.isRef() && + curr->left->type.getHeapType().isShared()) || + (curr->right->type.isRef() && + curr->right->type.getHeapType().isShared())) { + share = Shared; } - auto type = Type(eq, Nullable); + auto type = Type(HeapTypes::eq.getBasic(share), Nullable); self()->noteNonFlowSubtype(curr->left, type); self()->noteNonFlowSubtype(curr->right, type); } diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index c4bd9885e48..ea481bcd6c1 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -456,6 +456,7 @@ template struct Noter { Self& self() { return *static_cast(this); } void noteSubtype(HeapType sub, HeapType super) { + assert(HeapType::isSubType(sub, super)); // Bottom types are uninteresting, but other basic heap types can be // interesting because of their interactions with casts. if (sub == super || sub.isBottom()) { @@ -701,6 +702,7 @@ struct Unsubtyping : Pass, Noter { } void noteNonFlowSubtype(Expression* sub, Type super) { + assert(Type::isSubType(sub->type, super)); // This expression's type must be a subtype of |super|, but the value // does not flow anywhere - this is a static constraint. As the value // does not flow, it cannot reach anywhere else, which means we need diff --git a/test/gtest/subtype-exprs.cpp b/test/gtest/subtype-exprs.cpp index 0f2db3a4942..e3e87ec2d6a 100644 --- a/test/gtest/subtype-exprs.cpp +++ b/test/gtest/subtype-exprs.cpp @@ -77,3 +77,49 @@ TEST(SubtypeExprsTest, BrIf) { ASSERT_EQ(finder.seen.size(), 1); EXPECT_EQ(finder.seen.count(block), 1); } + +// A RefEq with an unreachable operand and a shared reference operand must still +// constrain its operands to shared eqref rather than unshared eqref. +TEST(SubtypeExprsTest, RefEqSharedUnreachable) { + Module wasm; + Builder builder(wasm); + Type sharedEqref = Type(HeapTypes::eq.getBasic(Shared), Nullable); + auto* sharedNull = builder.makeRefNull(HeapTypes::none.getBasic(Shared)); + auto* unreachable = builder.makeUnreachable(); + auto* refEq = builder.makeRefEq(unreachable, sharedNull); + ASSERT_EQ(refEq->type, Type::unreachable); + auto* drop = builder.makeDrop(refEq); + auto func = builder.makeFunction( + "func", {}, Signature(Type::none, Type::none), {}, drop); + + struct Finder + : public ControlFlowWalker> { + std::map noted; + + void noteSubtype(Type sub, Type super) {} + void noteSubtype(HeapType sub, HeapType super) {} + void noteSubtype(Type sub, Expression* super) {} + void noteSubtype(Expression* sub, Type super) {} + void noteSubtype(Expression* sub, Expression* super) {} + void noteNonFlowSubtype(Expression* sub, Type super) { noted[sub] = super; } + void noteCast(HeapType src, Type dst) {} + void noteCast(Expression* src, Type dst) {} + void noteCast(Expression* src, Expression* dst) {} + } finder; + + finder.walkFunctionInModule(func.get(), &wasm); + EXPECT_EQ(finder.noted[sharedNull], sharedEqref); + EXPECT_TRUE(Type::isSubType(sharedNull->type, finder.noted[sharedNull])); + + // Swap the operands so the shared reference is on the left and unreachable is + // on the right. + refEq->left = sharedNull; + refEq->right = unreachable; + refEq->finalize(); + ASSERT_EQ(refEq->type, Type::unreachable); + + finder.noted.clear(); + finder.walkFunctionInModule(func.get(), &wasm); + EXPECT_EQ(finder.noted[sharedNull], sharedEqref); + EXPECT_TRUE(Type::isSubType(sharedNull->type, finder.noted[sharedNull])); +} diff --git a/test/lit/passes/unsubtyping.wast b/test/lit/passes/unsubtyping.wast index bbbeb8e7252..311e0791c9b 100644 --- a/test/lit/passes/unsubtyping.wast +++ b/test/lit/passes/unsubtyping.wast @@ -2059,3 +2059,37 @@ ) ) ) + +;; Regression test for unreachable ref.eq with shared reference operands. +(module + ;; CHECK: (type $0 (func (param (ref null (shared eq))))) + + ;; CHECK: (func $ref-eq-shared-unreachable (type $0) (param $eq (ref null (shared eq))) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (local.get $eq) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.eq + ;; CHECK-NEXT: (local.get $eq) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref-eq-shared-unreachable (param $eq (ref null (shared eq))) + (drop + (ref.eq + (unreachable) + (local.get $eq) + ) + ) + (drop + (ref.eq + (local.get $eq) + (unreachable) + ) + ) + ) +)