[wasm-gc] Implement function subtyping

Changes:
- Implement function subtyping in wasm-subtyping.cc.
- Add Signature::Build(), which takes initializer lists for the return
  and parameter types.
- Only throw kTrapFuncSigMismatch in call_indirect, change that trap's
  message.
- Add a missing "return 0" in function-body-decoder-impl.h
- Fix a faulty check in wasm-objects.cc.
- Improve some comments.
- Write tests. Improve readability of subtyping-unittest.

Bug: v8:7748
Change-Id: I1caba09d5bd01cfd4d6125f300cd9c16af7aba99
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2822633
Reviewed-by: Jakob Kummerow <jkummerow@chromium.org>
Commit-Queue: Manos Koukoutos <manoskouk@chromium.org>
Cr-Commit-Position: refs/heads/master@{#73972}
This commit is contained in:
Manos Koukoutos 2021-04-13 05:40:00 +00:00 committed by Commit Bot
parent 0add53a789
commit e1cae86eba
12 changed files with 339 additions and 126 deletions

View File

@ -104,6 +104,14 @@ class Signature : public ZoneObject {
T* buffer_;
};
static Signature<T>* Build(Zone* zone, std::initializer_list<T> returns,
std::initializer_list<T> params) {
Builder builder(zone, returns.size(), params.size());
for (T ret : returns) builder.AddReturn(ret);
for (T param : params) builder.AddParam(param);
return builder.Build();
}
static constexpr size_t kReturnCountOffset = 0;
static constexpr size_t kParameterCountOffset =
kReturnCountOffset + kSizetSize;

View File

@ -564,7 +564,7 @@ namespace internal {
T(WasmTrapRemByZero, "remainder by zero") \
T(WasmTrapFloatUnrepresentable, "float unrepresentable in integer range") \
T(WasmTrapTableOutOfBounds, "table index is out of bounds") \
T(WasmTrapFuncSigMismatch, "function signature mismatch") \
T(WasmTrapFuncSigMismatch, "null function or function signature mismatch") \
T(WasmTrapMultiReturnLengthMismatch, "multi-return length mismatch") \
T(WasmTrapJSTypeError, "type incompatibility when transforming from/to JS") \
T(WasmTrapDataSegmentDropped, "data segment has been dropped") \

View File

@ -3074,12 +3074,13 @@ Node* WasmGraphBuilder::BuildIndirectCall(uint32_t table_index,
const wasm::ValueType table_type = env_->module->tables[table_index].type;
// Check that the table entry is not null and that the type of the function is
// a subtype of the function type declared at the call site. In the absence of
// function subtyping, the latter can only happen if the table type is (ref
// null? func). Also, subtyping reduces to normalized signature equality
// checking.
// TODO(7748): Expand this with function subtyping once we have that.
// **identical with** the function type declared at the call site (no
// subtyping of functions is allowed).
// Note: Since null entries are identified by having ift_sig_id (-1), we only
// need one comparison.
// TODO(9495): Change this if we should do full function subtyping instead.
const bool needs_signature_check =
FLAG_experimental_wasm_gc ||
table_type.is_reference_to(wasm::HeapType::kFunc) ||
table_type.is_nullable();
if (needs_signature_check) {
@ -3088,19 +3089,10 @@ Node* WasmGraphBuilder::BuildIndirectCall(uint32_t table_index,
Node* loaded_sig = gasm_->LoadFromObject(MachineType::Int32(), ift_sig_ids,
int32_scaled_key);
if (table_type.is_reference_to(wasm::HeapType::kFunc)) {
int32_t expected_sig_id = env_->module->canonicalized_type_ids[sig_index];
Node* sig_match =
gasm_->Word32Equal(loaded_sig, Int32Constant(expected_sig_id));
TrapIfFalse(wasm::kTrapFuncSigMismatch, sig_match, position);
} else {
// If the table entries are nullable, we still have to check that the
// entry is initialized.
Node* function_is_null =
gasm_->Word32Equal(loaded_sig, Int32Constant(-1));
TrapIfTrue(wasm::kTrapNullDereference, function_is_null, position);
}
int32_t expected_sig_id = env_->module->canonicalized_type_ids[sig_index];
Node* sig_match =
gasm_->Word32Equal(loaded_sig, Int32Constant(expected_sig_id));
TrapIfFalse(wasm::kTrapFuncSigMismatch, sig_match, position);
}
Node* key_intptr = BuildChangeUint32ToUintPtr(key);

View File

@ -5549,7 +5549,6 @@ class LiftoffCompiler {
__ Load(LiftoffRegister(scratch), table, index, 0, LoadType::kI32Load,
pinned);
// TODO(9495): Do not always compare signatures, same as wasm-compiler.cc.
// Compare against expected signature.
__ LoadConstant(LiftoffRegister(tmp_const), WasmValue(canonical_sig_num));

View File

@ -1442,6 +1442,7 @@ class WasmDecoder : public Decoder {
}
inline bool Validate(const byte* pc, CallIndirectImmediate<validate>& imm) {
// Validate immediate table index.
if (!VALIDATE(imm.table_index < module_->tables.size())) {
DecodeError(pc, "call_indirect: table index immediate out of bounds");
return false;
@ -1453,10 +1454,13 @@ class WasmDecoder : public Decoder {
imm.table_index);
return false;
}
// Validate immediate signature index.
if (!Complete(imm)) {
DecodeError(pc, "invalid signature index: #%u", imm.sig_index);
return false;
}
// Check that the dynamic signature for this call is a subtype of the static
// type of the table the function is defined in.
ValueType immediate_type = ValueType::Ref(imm.sig_index, kNonNullable);
@ -1465,6 +1469,7 @@ class WasmDecoder : public Decoder {
"call_indirect: Immediate signature #%u is not a subtype of "
"immediate table #%u",
imm.sig_index, imm.table_index);
return false;
}
return true;
}
@ -4359,7 +4364,6 @@ class WasmFullDecoder : public WasmDecoder<validate> {
CALL_INTERFACE_IF_OK_AND_REACHABLE(Drop);
CALL_INTERFACE_IF_OK_AND_REACHABLE(AssertNull, obj, &value);
} else {
// TODO(manoskouk): Change the trap label.
CALL_INTERFACE_IF_OK_AND_REACHABLE(Trap,
TrapReason::kTrapIllegalCast);
EndControl();

View File

@ -454,21 +454,19 @@ void WasmTableObject::Set(Isolate* isolate, Handle<WasmTableObject> table,
case wasm::HeapType::kEq:
case wasm::HeapType::kData:
case wasm::HeapType::kI31:
// TODO(7748): Implement once we have a story for struct/arrays/i31ref in
// JS.
UNIMPLEMENTED();
// TODO(7748): Implement once we have struct/arrays/i31ref tables.
UNREACHABLE();
case wasm::HeapType::kBottom:
UNREACHABLE();
default:
DCHECK(!table->instance().IsUndefined());
if (WasmInstanceObject::cast(table->instance())
.module()
->has_signature(entry_index)) {
SetFunctionTableEntry(isolate, table, entries, entry_index, entry);
return;
}
// TODO(7748): Implement once we have a story for struct/arrays in JS.
UNIMPLEMENTED();
// TODO(7748): Relax this once we have struct/array/i31ref tables.
DCHECK_EQ(WasmInstanceObject::cast(table->instance())
.module()
->type_kinds[table->type().ref_index()],
wasm::kWasmFunctionTypeCode);
SetFunctionTableEntry(isolate, table, entries, entry_index, entry);
return;
}
}
@ -2256,7 +2254,8 @@ bool TypecheckJSObject(Isolate* isolate, const WasmModule* module,
if (WasmJSFunction::IsWasmJSFunction(*value)) {
// Since a WasmJSFunction cannot refer to indexed types (definable
// only in a module), we do not need to use EquivalentTypes().
// only in a module), we do not need full function subtyping.
// TODO(manoskouk): Change this if wasm types can be exported.
if (!WasmJSFunction::cast(*value).MatchesSignature(
module->signature(expected.ref_index()))) {
*error_message =
@ -2268,11 +2267,12 @@ bool TypecheckJSObject(Isolate* isolate, const WasmModule* module,
}
if (WasmCapiFunction::IsWasmCapiFunction(*value)) {
// Since a WasmCapiFunction cannot refer to indexed types
// (definable only in a module), we do not need full function
// subtyping.
// TODO(manoskouk): Change this if wasm types can be exported.
if (!WasmCapiFunction::cast(*value).MatchesSignature(
module->signature(expected.ref_index()))) {
// Since a WasmCapiFunction cannot refer to indexed types
// (definable in a module), we don't need to invoke
// IsEquivalentType();
*error_message =
"assigned WasmCapiFunction has to be a subtype of the "
"expected type";

View File

@ -258,14 +258,46 @@ bool ArrayIsSubtypeOf(uint32_t subtype_index, uint32_t supertype_index,
}
}
// TODO(7748): Expand this with function subtyping when it is introduced.
bool FunctionIsSubtypeOf(uint32_t subtype_index, uint32_t supertype_index,
const WasmModule* sub_module,
const WasmModule* super_module) {
return FunctionEquivalentIndices(subtype_index, supertype_index, sub_module,
super_module);
}
if (!FLAG_experimental_wasm_gc) {
return FunctionEquivalentIndices(subtype_index, supertype_index, sub_module,
super_module);
}
const FunctionSig* sub_func = sub_module->types[subtype_index].function_sig;
const FunctionSig* super_func =
super_module->types[supertype_index].function_sig;
if (sub_func->parameter_count() != super_func->parameter_count() ||
sub_func->return_count() != super_func->return_count()) {
return false;
}
TypeJudgementCache::instance()->cache_subtype(subtype_index, supertype_index,
sub_module, super_module);
for (uint32_t i = 0; i < sub_func->parameter_count(); i++) {
// Contravariance for params.
if (!IsSubtypeOf(super_func->parameters()[i], sub_func->parameters()[i],
super_module, sub_module)) {
TypeJudgementCache::instance()->uncache_subtype(
subtype_index, supertype_index, sub_module, super_module);
return false;
}
}
for (uint32_t i = 0; i < sub_func->return_count(); i++) {
// Covariance for returns.
if (!IsSubtypeOf(sub_func->returns()[i], super_func->returns()[i],
sub_module, super_module)) {
TypeJudgementCache::instance()->uncache_subtype(
subtype_index, supertype_index, sub_module, super_module);
return false;
}
}
return true;
}
} // namespace
V8_NOINLINE V8_EXPORT_PRIVATE bool IsSubtypeOfImpl(

View File

@ -60,8 +60,10 @@ V8_NOINLINE bool EquivalentTypes(ValueType type1, ValueType type2,
// - Struct subtyping: Subtype must have at least as many fields as supertype,
// covariance for immutable fields, equivalence for mutable fields.
// - Array subtyping (mutable only) is the equivalence relation.
// - Function subtyping is the equivalence relation (note: this rule might
// change in the future to include type variance).
// - Function subtyping depends on the enabled wasm features: if
// --experimental-wasm-gc is enabled, then subtyping is computed
// contravariantly for parameter types and covariantly for return types.
// Otherwise, the subtyping relation is the equivalence relation.
V8_INLINE bool IsSubtypeOf(ValueType subtype, ValueType supertype,
const WasmModule* sub_module,
const WasmModule* super_module) {

View File

@ -43,8 +43,8 @@ class WasmGCTester {
&v8::internal::FLAG_liftoff_only,
execution_tier == TestExecutionTier::kLiftoff ? true : false),
flag_tierup(&v8::internal::FLAG_wasm_tier_up, false),
zone(&allocator, ZONE_NAME),
builder_(&zone),
zone_(&allocator, ZONE_NAME),
builder_(&zone_),
isolate_(CcTest::InitIsolateOnce()),
scope(isolate_),
thrower(isolate_, "Test wasm GC") {
@ -82,7 +82,7 @@ class WasmGCTester {
}
byte DefineStruct(std::initializer_list<F> fields) {
StructType::Builder type_builder(&zone,
StructType::Builder type_builder(&zone_,
static_cast<uint32_t>(fields.size()));
for (F field : fields) {
type_builder.AddField(field.first, field.second);
@ -91,7 +91,8 @@ class WasmGCTester {
}
byte DefineArray(ValueType element_type, bool mutability) {
return builder_.AddArrayType(zone.New<ArrayType>(element_type, mutability));
return builder_.AddArrayType(
zone_.New<ArrayType>(element_type, mutability));
}
byte DefineSignature(FunctionSig* sig) { return builder_.AddSignature(sig); }
@ -101,7 +102,7 @@ class WasmGCTester {
}
void CompileModule() {
ZoneBuffer buffer(&zone);
ZoneBuffer buffer(&zone_);
builder_.WriteTo(&buffer);
MaybeHandle<WasmInstanceObject> maybe_instance =
testing::CompileAndInstantiateForTesting(
@ -174,6 +175,7 @@ class WasmGCTester {
Handle<WasmInstanceObject> instance() { return instance_; }
Isolate* isolate() { return isolate_; }
WasmModuleBuilder* builder() { return &builder_; }
Zone* zone() { return &zone_; }
TestSignatures sigs;
@ -186,7 +188,7 @@ class WasmGCTester {
const FlagScope<bool> flag_tierup;
v8::internal::AccountingAllocator allocator;
Zone zone;
Zone zone_;
WasmModuleBuilder builder_;
Isolate* const isolate_;
@ -1484,19 +1486,101 @@ WASM_COMPILED_EXEC_TEST(GlobalInitReferencingGlobal) {
tester.CheckResult(func, 42);
}
WASM_COMPILED_EXEC_TEST(IndirectNullSetManually) {
WASM_COMPILED_EXEC_TEST(GCTables) {
WasmGCTester tester(execution_tier);
byte sig_index = tester.DefineSignature(tester.sigs.i_i());
tester.DefineTable(ValueType::Ref(sig_index, kNullable), 1, 1);
byte func_index = tester.DefineFunction(
tester.sigs.i_i(), {},
{WASM_TABLE_SET(0, WASM_I32V(0), WASM_REF_NULL(sig_index)),
WASM_CALL_INDIRECT(sig_index, WASM_I32V(0), WASM_LOCAL_GET(0)),
kExprEnd});
byte super_struct = tester.DefineStruct({F(kWasmI32, false)});
byte sub_struct =
tester.DefineStruct({F(kWasmI32, false), F(kWasmI32, true)});
FunctionSig* super_sig =
FunctionSig::Build(tester.zone(), {kWasmI32}, {optref(sub_struct)});
byte super_sig_index = tester.DefineSignature(super_sig);
FunctionSig* sub_sig =
FunctionSig::Build(tester.zone(), {kWasmI32}, {optref(super_struct)});
byte sub_sig_index = tester.DefineSignature(sub_sig);
tester.DefineTable(optref(super_sig_index), 10, 10);
byte super_func = tester.DefineFunction(
super_sig, {},
{WASM_I32_ADD(WASM_STRUCT_GET(sub_struct, 0, WASM_LOCAL_GET(0)),
WASM_STRUCT_GET(sub_struct, 1, WASM_LOCAL_GET(0))),
WASM_END});
byte sub_func = tester.DefineFunction(
sub_sig, {},
{WASM_STRUCT_GET(super_struct, 0, WASM_LOCAL_GET(0)), WASM_END});
byte setup_func = tester.DefineFunction(
tester.sigs.i_v(), {},
{WASM_TABLE_SET(0, WASM_I32V(0), WASM_REF_NULL(super_sig_index)),
WASM_TABLE_SET(0, WASM_I32V(1), WASM_REF_FUNC(super_func)),
WASM_TABLE_SET(0, WASM_I32V(2), WASM_REF_FUNC(sub_func)), WASM_I32V(0),
WASM_END});
byte super_struct_producer = tester.DefineFunction(
FunctionSig::Build(tester.zone(), {ref(super_struct)}, {}), {},
{WASM_STRUCT_NEW_WITH_RTT(super_struct, WASM_I32V(-5),
WASM_RTT_CANON(super_struct)),
WASM_END});
byte sub_struct_producer = tester.DefineFunction(
FunctionSig::Build(tester.zone(), {ref(sub_struct)}, {}), {},
{WASM_STRUCT_NEW_WITH_RTT(sub_struct, WASM_I32V(7), WASM_I32V(11),
WASM_RTT_CANON(sub_struct)),
WASM_END});
// Calling a null entry should trap.
byte call_null = tester.DefineFunction(
tester.sigs.i_v(), {},
{WASM_CALL_INDIRECT(super_sig_index,
WASM_CALL_FUNCTION0(sub_struct_producer),
WASM_I32V(0)),
WASM_END});
// Calling with a signature identical to the type of the table should work,
// provided the entry has the same signature.
byte call_same_type = tester.DefineFunction(
tester.sigs.i_v(), {},
{WASM_CALL_INDIRECT(super_sig_index,
WASM_CALL_FUNCTION0(sub_struct_producer),
WASM_I32V(1)),
WASM_END});
// Calling with a signature that is a subtype of the type of the table should
// work, provided the entry has the same signature.
byte call_subtype = tester.DefineFunction(
tester.sigs.i_v(), {},
{WASM_CALL_INDIRECT(sub_sig_index,
WASM_CALL_FUNCTION0(super_struct_producer),
WASM_I32V(2)),
WASM_END});
// Calling with a signature that is mismatched to that of the entry should
// trap.
byte call_type_mismatch = tester.DefineFunction(
tester.sigs.i_v(), {},
{WASM_CALL_INDIRECT(super_sig_index,
WASM_CALL_FUNCTION0(sub_struct_producer),
WASM_I32V(2)),
WASM_END});
// Getting a table element and then calling it with call_ref should work.
byte table_get_and_call_ref = tester.DefineFunction(
tester.sigs.i_v(), {},
{WASM_CALL_REF(WASM_TABLE_GET(0, WASM_I32V(2)),
WASM_CALL_FUNCTION0(sub_struct_producer)),
WASM_END});
// Only here so these functions count as "declared".
tester.AddGlobal(optref(super_sig_index), false,
WasmInitExpr::RefFuncConst(super_func));
tester.AddGlobal(optref(sub_sig_index), false,
WasmInitExpr::RefFuncConst(sub_func));
tester.CompileModule();
tester.CheckHasThrown(func_index, 42);
tester.CheckResult(setup_func, 0);
tester.CheckHasThrown(call_null);
tester.CheckResult(call_same_type, 18);
tester.CheckResult(call_subtype, -5);
tester.CheckHasThrown(call_type_mismatch);
tester.CheckResult(table_get_and_call_ref, 7);
}
WASM_COMPILED_EXEC_TEST(JsAccess) {

View File

@ -831,7 +831,7 @@ let kTrapMsgs = [
'remainder by zero',
'float unrepresentable in integer range',
'table index is out of bounds',
'function signature mismatch',
'null function or function signature mismatch',
'operation does not support unaligned accesses',
'data segment has been dropped',
'element segment has been dropped',

View File

@ -1914,6 +1914,50 @@ TEST_F(FunctionBodyDecoderTest, IndirectCallsWithMismatchedSigs2) {
"call_indirect: immediate table #1 is not of a function type");
}
TEST_F(FunctionBodyDecoderTest, TablesWithFunctionSubtyping) {
WASM_FEATURE_SCOPE(reftypes);
WASM_FEATURE_SCOPE(typed_funcref);
WASM_FEATURE_SCOPE(gc);
EXPERIMENTAL_FLAG_SCOPE(gc);
byte empty_struct = builder.AddStruct({});
byte super_struct = builder.AddStruct({F(kWasmI32, false)});
byte sub_struct = builder.AddStruct({F(kWasmI32, false), F(kWasmF64, false)});
byte table_type = builder.AddSignature(
FunctionSig::Build(zone(), {ValueType::Ref(super_struct, kNullable)},
{ValueType::Ref(sub_struct, kNullable)}));
byte table_supertype = builder.AddSignature(
FunctionSig::Build(zone(), {ValueType::Ref(empty_struct, kNullable)},
{ValueType::Ref(sub_struct, kNullable)}));
auto function_sig =
FunctionSig::Build(zone(), {ValueType::Ref(sub_struct, kNullable)},
{ValueType::Ref(super_struct, kNullable)});
byte function_type = builder.AddSignature(function_sig);
byte function = builder.AddFunction(function_sig);
byte table = builder.InitializeTable(ValueType::Ref(table_type, kNullable));
// We can call-indirect from a typed function table with an immediate type
// that is a subtype of the table type.
ExpectValidates(
FunctionSig::Build(zone(), {ValueType::Ref(sub_struct, kNullable)}, {}),
{WASM_CALL_INDIRECT_TABLE(
table, function_type,
WASM_STRUCT_NEW_DEFAULT(super_struct, WASM_RTT_CANON(super_struct)),
WASM_ZERO)});
// table.set's subtyping works as expected.
ExpectValidates(sigs.v_i(), {WASM_TABLE_SET(0, WASM_LOCAL_GET(0),
WASM_REF_FUNC(function))});
// table.get's subtyping works as expected.
ExpectValidates(
FunctionSig::Build(zone(), {ValueType::Ref(table_supertype, kNullable)},
{kWasmI32}),
{WASM_TABLE_GET(0, WASM_LOCAL_GET(0))});
}
TEST_F(FunctionBodyDecoderTest, IndirectCallsWithoutTableCrash) {
const FunctionSig* sig = sigs.i_i();

View File

@ -3,6 +3,7 @@
// found in the LICENSE file.
#include "src/wasm/wasm-subtyping.h"
#include "test/common/wasm/flag-utils.h"
#include "test/unittests/test-utils.h"
namespace v8 {
@ -33,6 +34,13 @@ void DefineArray(WasmModule* module, FieldInit element_type) {
element_type.first, element_type.second));
}
void DefineSignature(WasmModule* module,
std::initializer_list<ValueType> params,
std::initializer_list<ValueType> returns) {
module->add_signature(
FunctionSig::Build(module->signature_zone.get(), returns, params));
}
TEST_F(WasmSubtypingTest, Subtyping) {
v8::internal::AccountingAllocator allocator;
WasmModule module1_(std::make_unique<Zone>(&allocator, ZONE_NAME));
@ -43,128 +51,168 @@ TEST_F(WasmSubtypingTest, Subtyping) {
// Set up two identical modules.
for (WasmModule* module : {module1, module2}) {
/* 0 */ DefineStruct(module, {mut(ref(2)), immut(optRef(2))});
/* 1 */ DefineStruct(module, {mut(ref(2)), immut(ref(2))});
/* 2 */ DefineArray(module, immut(ref(0)));
/* 3 */ DefineArray(module, immut(ref(1)));
/* 4 */ DefineStruct(module, {mut(ref(2)), immut(ref(3)), immut(kWasmF64)});
/* 5 */ DefineStruct(module, {mut(optRef(2)), immut(ref(2))});
/* 6 */ DefineArray(module, mut(kWasmI32));
/* 7 */ DefineArray(module, immut(kWasmI32));
/* 8 */ DefineStruct(module, {mut(kWasmI32), immut(optRef(8))});
/* 9 */ DefineStruct(module, {mut(kWasmI32), immut(optRef(8))});
/* 0 */ DefineStruct(module, {mut(ref(2)), immut(optRef(2))});
/* 1 */ DefineStruct(module, {mut(ref(2)), immut(ref(2))});
/* 2 */ DefineArray(module, immut(ref(0)));
/* 3 */ DefineArray(module, immut(ref(1)));
/* 4 */ DefineStruct(module,
{mut(ref(2)), immut(ref(3)), immut(kWasmF64)});
/* 5 */ DefineStruct(module, {mut(optRef(2)), immut(ref(2))});
/* 6 */ DefineArray(module, mut(kWasmI32));
/* 7 */ DefineArray(module, immut(kWasmI32));
/* 8 */ DefineStruct(module, {mut(kWasmI32), immut(optRef(8))});
/* 9 */ DefineStruct(module, {mut(kWasmI32), immut(optRef(8))});
/* 10 */ DefineSignature(module, {}, {});
/* 11 */ DefineSignature(module, {kWasmI32}, {kWasmI32});
/* 12 */ DefineSignature(module, {kWasmI32, kWasmI32}, {kWasmI32});
/* 13 */ DefineSignature(module, {ref(1)}, {kWasmI32});
/* 14 */ DefineSignature(module, {ref(0)}, {kWasmI32});
/* 15 */ DefineSignature(module, {ref(0)}, {ref(4)});
/* 16 */ DefineSignature(module, {ref(0)}, {ref(0)});
}
ValueType numeric_types[] = {kWasmI32, kWasmI64, kWasmF32, kWasmF64,
kWasmS128};
ValueType ref_types[] = {
kWasmExternRef, kWasmFuncRef, kWasmEqRef, kWasmI31Ref, kWasmDataRef,
kWasmAnyRef, optRef(0), ref(0), optRef(2), ref(2)};
ValueType ref_types[] = {kWasmExternRef, kWasmFuncRef, kWasmEqRef,
kWasmI31Ref, kWasmDataRef, kWasmAnyRef,
optRef(0), ref(0), optRef(2),
ref(2), optRef(11), ref(11)};
#define SUBTYPE(type1, type2) \
EXPECT_TRUE(IsSubtypeOf(type1, type2, module1, module))
#define NOT_SUBTYPE(type1, type2) \
EXPECT_FALSE(IsSubtypeOf(type1, type2, module1, module))
#define SUBTYPE_IFF(type1, type2, condition) \
EXPECT_EQ(IsSubtypeOf(type1, type2, module1, module), condition)
// Type judgements across modules should work the same as within one module.
for (WasmModule* module : {module1, module2}) {
// Value types are unrelated, except if they are equal.
for (ValueType subtype : numeric_types) {
for (ValueType supertype : numeric_types) {
EXPECT_EQ(IsSubtypeOf(subtype, supertype, module1, module),
subtype == supertype);
SUBTYPE_IFF(subtype, supertype, subtype == supertype);
}
}
// Value types are unrelated with reference types.
for (ValueType value_type : numeric_types) {
for (ValueType ref_type : ref_types) {
EXPECT_TRUE(!IsSubtypeOf(value_type, ref_type, module1, module));
EXPECT_TRUE(!IsSubtypeOf(ref_type, value_type, module1, module));
NOT_SUBTYPE(value_type, ref_type);
NOT_SUBTYPE(ref_type, value_type);
}
}
for (ValueType ref_type : ref_types) {
// Concrete reference types, i31ref and dataref are subtypes of eqref,
// externref/funcref/anyref are not.
EXPECT_EQ(IsSubtypeOf(ref_type, kWasmEqRef, module1, module),
ref_type != kWasmFuncRef && ref_type != kWasmExternRef &&
ref_type != kWasmAnyRef);
// externref/funcref/anyref/functions are not.
SUBTYPE_IFF(ref_type, kWasmEqRef,
ref_type != kWasmFuncRef && ref_type != kWasmExternRef &&
ref_type != kWasmAnyRef && ref_type != optRef(11) &&
ref_type != ref(11));
// Non-nullable struct/array types are subtypes of dataref.
EXPECT_EQ(IsSubtypeOf(ref_type, kWasmDataRef, module1, module),
ref_type == kWasmDataRef ||
(ref_type.kind() == kRef && ref_type.has_index()));
SUBTYPE_IFF(
ref_type, kWasmDataRef,
ref_type == kWasmDataRef || ref_type == ref(0) || ref_type == ref(2));
// Functions are subtypes of funcref.
SUBTYPE_IFF(ref_type, kWasmFuncRef,
ref_type == kWasmFuncRef || ref_type == optRef(11) ||
ref_type == ref(11));
// Each reference type is a subtype of itself.
EXPECT_TRUE(IsSubtypeOf(ref_type, ref_type, module1, module));
SUBTYPE(ref_type, ref_type);
// Each reference type is a subtype of anyref.
EXPECT_TRUE(IsSubtypeOf(ref_type, kWasmAnyRef, module1, module));
SUBTYPE(ref_type, kWasmAnyRef);
// Only anyref is a subtype of anyref.
EXPECT_EQ(IsSubtypeOf(kWasmAnyRef, ref_type, module1, module),
ref_type == kWasmAnyRef);
SUBTYPE_IFF(kWasmAnyRef, ref_type, ref_type == kWasmAnyRef);
}
// The rest of ref. types are unrelated.
for (ValueType type_1 : {kWasmExternRef, kWasmFuncRef, kWasmI31Ref}) {
for (ValueType type_2 : {kWasmExternRef, kWasmFuncRef, kWasmI31Ref}) {
EXPECT_EQ(IsSubtypeOf(type_1, type_2, module1, module),
type_1 == type_2);
SUBTYPE_IFF(type_1, type_2, type_1 == type_2);
}
}
// Unrelated refs are unrelated.
EXPECT_TRUE(!IsSubtypeOf(ref(0), ref(2), module1, module));
EXPECT_TRUE(!IsSubtypeOf(optRef(3), optRef(1), module1, module));
NOT_SUBTYPE(ref(0), ref(2));
NOT_SUBTYPE(optRef(3), optRef(1));
// ref is a subtype of optref for the same struct/array.
EXPECT_TRUE(IsSubtypeOf(ref(0), optRef(0), module1, module));
EXPECT_TRUE(IsSubtypeOf(ref(2), optRef(2), module1, module));
SUBTYPE(ref(0), optRef(0));
SUBTYPE(ref(2), optRef(2));
// optref is not a subtype of ref for the same struct/array.
EXPECT_TRUE(!IsSubtypeOf(optRef(0), ref(0), module1, module));
EXPECT_TRUE(!IsSubtypeOf(optRef(2), ref(2), module1, module));
NOT_SUBTYPE(optRef(0), ref(0));
NOT_SUBTYPE(optRef(2), ref(2));
// ref is a subtype of optref if the same is true for the underlying
// structs/arrays.
EXPECT_TRUE(IsSubtypeOf(ref(3), optRef(2), module1, module));
SUBTYPE(ref(3), optRef(2));
// Prefix subtyping for structs.
EXPECT_TRUE(IsSubtypeOf(optRef(4), optRef(0), module1, module));
SUBTYPE(optRef(4), optRef(0));
// Mutable fields are invariant.
EXPECT_TRUE(!IsSubtypeOf(ref(0), ref(5), module1, module));
NOT_SUBTYPE(ref(0), ref(5));
// Immutable fields are covariant.
EXPECT_TRUE(IsSubtypeOf(ref(1), ref(0), module1, module));
SUBTYPE(ref(1), ref(0));
// Prefix subtyping + immutable field covariance for structs.
EXPECT_TRUE(IsSubtypeOf(optRef(4), optRef(1), module1, module));
SUBTYPE(optRef(4), optRef(1));
// No subtyping between mutable/immutable fields.
EXPECT_TRUE(!IsSubtypeOf(ref(7), ref(6), module1, module));
EXPECT_TRUE(!IsSubtypeOf(ref(6), ref(7), module1, module));
NOT_SUBTYPE(ref(7), ref(6));
NOT_SUBTYPE(ref(6), ref(7));
// Recursive types.
EXPECT_TRUE(IsSubtypeOf(ref(9), ref(8), module1, module));
SUBTYPE(ref(9), ref(8));
// Identical rtts are subtypes of each other.
EXPECT_TRUE(IsSubtypeOf(ValueType::Rtt(5, 3), ValueType::Rtt(5, 3), module1,
module2));
EXPECT_TRUE(
IsSubtypeOf(ValueType::Rtt(5), ValueType::Rtt(5), module1, module2));
SUBTYPE(ValueType::Rtt(5, 3), ValueType::Rtt(5, 3));
SUBTYPE(ValueType::Rtt(5), ValueType::Rtt(5));
// Rtts of unrelated types are unrelated.
EXPECT_TRUE(!IsSubtypeOf(ValueType::Rtt(1, 1), ValueType::Rtt(2, 1),
module1, module2));
EXPECT_TRUE(
!IsSubtypeOf(ValueType::Rtt(1), ValueType::Rtt(2), module1, module2));
EXPECT_TRUE(!IsSubtypeOf(ValueType::Rtt(1, 0), ValueType::Rtt(2), module1,
module2));
NOT_SUBTYPE(ValueType::Rtt(1, 1), ValueType::Rtt(2, 1));
NOT_SUBTYPE(ValueType::Rtt(1), ValueType::Rtt(2));
NOT_SUBTYPE(ValueType::Rtt(1, 0), ValueType::Rtt(2));
// Rtts of different depth are unrelated.
EXPECT_TRUE(!IsSubtypeOf(ValueType::Rtt(5, 1), ValueType::Rtt(5, 3),
module1, module2));
EXPECT_TRUE(!IsSubtypeOf(ValueType::Rtt(5, 8), ValueType::Rtt(5, 3),
module1, module2));
NOT_SUBTYPE(ValueType::Rtt(5, 1), ValueType::Rtt(5, 3));
NOT_SUBTYPE(ValueType::Rtt(5, 8), ValueType::Rtt(5, 3));
// Rtts of identical types are subtype-related.
EXPECT_TRUE(IsSubtypeOf(ValueType::Rtt(8, 1), ValueType::Rtt(9, 1), module1,
module));
EXPECT_TRUE(
IsSubtypeOf(ValueType::Rtt(8), ValueType::Rtt(9), module1, module));
SUBTYPE(ValueType::Rtt(8, 1), ValueType::Rtt(9, 1));
SUBTYPE(ValueType::Rtt(8), ValueType::Rtt(9));
// Rtts of subtypes are not related.
EXPECT_TRUE(!IsSubtypeOf(ValueType::Rtt(1, 1), ValueType::Rtt(0, 1),
module1, module));
EXPECT_TRUE(
!IsSubtypeOf(ValueType::Rtt(1), ValueType::Rtt(0), module1, module));
NOT_SUBTYPE(ValueType::Rtt(1, 1), ValueType::Rtt(0, 1));
NOT_SUBTYPE(ValueType::Rtt(1), ValueType::Rtt(0));
// rtt(t, d) <: rtt(t)
for (uint8_t depth : {0, 1, 5}) {
EXPECT_TRUE(IsSubtypeOf(ValueType::Rtt(1, depth), ValueType::Rtt(1),
module1, module));
SUBTYPE(ValueType::Rtt(1, depth), ValueType::Rtt(1));
}
// Function subtyping depends on the selected wasm features.
// Without wasm-gc:
// Unrelated function types are unrelated.
NOT_SUBTYPE(ref(10), ref(11));
// Function type with different parameter counts are unrelated.
NOT_SUBTYPE(ref(12), ref(11));
// Parameter contravariance does not hold.
NOT_SUBTYPE(ref(14), ref(13));
// Return type covariance does not hold.
NOT_SUBTYPE(ref(15), ref(16));
// Only identical types are subtype-related.
SUBTYPE(ref(10), ref(10));
SUBTYPE(ref(11), ref(11));
{
// With wasm-gc:
EXPERIMENTAL_FLAG_SCOPE(gc);
// Unrelated function types are unrelated.
NOT_SUBTYPE(ref(10), ref(11));
// Function type with different parameter counts are unrelated.
NOT_SUBTYPE(ref(12), ref(11));
// Parameter contravariance holds.
SUBTYPE(ref(14), ref(13));
// Return type covariance holds.
SUBTYPE(ref(15), ref(16));
// Identical types are subtype-related.
SUBTYPE(ref(10), ref(10));
SUBTYPE(ref(11), ref(11));
}
}
#undef SUBTYPE
#undef NOT_SUBTYPE
#undef SUBTYPE_IFF
}
} // namespace subtyping_unittest