/*
* Copyright 2023 WebAssembly Community Group participants
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef wasm_ir_subtype_exprs_h
#define wasm_ir_subtype_exprs_h
#include "ir/branch-utils.h"
#include "wasm-traversal.h"
#include "wasm-type.h"
#include "wasm.h"
namespace wasm {
//
// Analyze subtyping relationships between expressions. This must CRTP with a
// class that implements:
//
// * noteSubtype(A, B) indicating A must be a subtype of B
// * noteCast(A, B) indicating A is cast to B
//
// There must be multiple versions of each of those, supporting A and B being
// either a Type, which indicates a fixed type requirement, or an Expression*,
// indicating a flexible requirement that depends on the type of that
// expression. Specifically:
//
// * noteSubtype(Type, Type) - A constraint not involving expressions at all,
// for example, an element segment's type must be
// a subtype of the corresponding table's.
// * noteSubtype(HeapType, HeapType) - Ditto, with heap types, for example in a
// CallIndirect.
// * noteSubtype(Type, Expression) - A fixed type must be a subtype of an
// expression's type, for example, in BrOn
// (the declared sent type must be a subtype
// of the block we branch to).
// * noteSubtype(Expression, Type) - An expression's type must be a subtype of
// a fixed type, for example, a Call operand
// must be a subtype of the signature's
// param.
// * noteSubtype(Expression, Expression) - An expression's type must be a
// subtype of another's, for example,
// a block and its last child.
//
// * noteCast(HeapType, Type) - A fixed type is cast to another, for example,
// in a CallIndirect. The destination is a Type
// rather than HeapType because it may be exact.
// * noteCast(Expression, Type) - An expression's type is cast to a fixed type,
// for example, in RefTest.
// * noteCast(Expression, Expression) - An expression's type is cast to
// another, for example, in RefCast.
//
// In addition, we need to differentiate two situations that cause subtyping:
// * Flow-based subtyping: E.g. when a value flows out from a block, in which
// case the value must be a subtype of the block's type.
// * Non-flow-based subtyping: E.g. in RefEq, being in one of the arms means
// you must be a subtype of eqref, but your value does not flow anywhere,
// because it is processed by the RefEq and does not send it anywhere.
// The difference between the two matters in some users of this class, and so
// the above functions all handle flow-based subtyping, while there is also the
// following:
//
// * noteNonFlowSubtype(Expression, Type)
//
// This is the only signature we need for the non-flowing case since it always
// stems from an expression that is compared against a type.
//
// The concrete signatures are:
//
// void noteSubtype(Type, Type);
// void noteSubtype(HeapType, HeapType);
// void noteSubtype(Type, Expression*);
// void noteSubtype(Expression*, Type);
// void noteSubtype(Expression*, Expression*);
// void noteNonFlowSubtype(Expression*, Type);
// void noteCast(HeapType, HeapType);
// void noteCast(Expression*, Type);
// void noteCast(Expression*, Expression*);
//
// Note that noteCast(Type, Type) and noteCast(Type, Expression) never occur and
// do not need to be implemented.
//
// The class must also inherit from ControlFlowWalker (for findBreakTarget).
//
template<typename SubType>
struct SubtypingDiscoverer : public OverriddenVisitor<SubType> {
SubType* self() { return static_cast<SubType*>(this); }
void visitFunction(Function* func) {
if (func->body) {
self()->noteSubtype(func->body, func->getResults());
}
}
void visitGlobal(Global* global) {
if (global->init) {
self()->noteSubtype(global->init, global->type);
}
}
void visitTable(Table* table) {
if (table->init) {
self()->noteSubtype(table->init, table->type);
}
}
void visitElementSegment(ElementSegment* seg) {
if (seg->offset) {
self()->noteSubtype(seg->type,
self()->getModule()->getTable(seg->table)->type);
}
for (auto init : seg->data) {
self()->noteSubtype(init->type, seg->type);
}
}
void visitNop(Nop* curr) {}
void visitBlock(Block* curr) {
if (!curr->list.empty()) {
self()->noteSubtype(curr->list.back(), curr);
}
}
void visitIf(If* curr) {
if (curr->ifFalse && curr->type != Type::unreachable) {
self()->noteSubtype(curr->ifTrue, curr);
self()->noteSubtype(curr->ifFalse, curr);
}
}
void visitLoop(Loop* curr) { self()->noteSubtype(curr->body, curr); }
void visitBreak(Break* curr) {
if (curr->value) {
self()->noteSubtype(curr->value, self()->findBreakTarget(curr->name));
if (curr->condition && curr->type != Type::unreachable) {
self()->noteSubtype(curr->value, curr);
}
}
}
void visitSwitch(Switch* curr) {
if (curr->value) {
for (auto name : BranchUtils::getUniqueTargets(curr)) {
self()->noteSubtype(curr->value, self()->findBreakTarget(name));
}
}
}
template<typename T> void handleCall(T* curr, Signature sig) {
assert(curr->operands.size() == sig.params.size());
for (size_t i = 0, size = sig.params.size(); i < size; ++i) {
self()->noteSubtype(curr->operands[i], sig.params[i]);
}
if (curr->isReturn) {
self()->noteSubtype(sig.results, self()->getFunction()->getResults());
}
}
void visitCall(Call* curr) {
handleCall(curr, self()->getModule()->getFunction(curr->target)->getSig());
}
void visitCallIndirect(CallIndirect* curr) {
handleCall(curr, curr->heapType.getSignature());
auto* table = self()->getModule()->getTable(curr->table);
auto tableType = table->type.getHeapType();
if (HeapType::isSubType(tableType, curr->heapType)) {
// Unlike other casts, where cast targets are always subtypes of cast
// sources, call_indirect target types may be supertypes of their source
// table types. In this case, the cast will always succeed, but only if we
// keep the types related.
// TODO: No value flows here, so we could use |noteNonFlowSubtype|, but
// this is a trivial situation that is not worth optimizing.
self()->noteSubtype(tableType, curr->heapType);
} else if (HeapType::isSubType(curr->heapType, tableType)) {
self()->noteCast(tableType, Type(curr->heapType, NonNullable, Inexact));
} else {
// The types are unrelated and the cast will fail. We can keep the types
// unrelated.
}
}
void visitLocalGet(LocalGet* curr) {}
void visitLocalSet(LocalSet* curr) {
self()->noteSubtype(curr->value,
self()->getFunction()->getLocalType(curr->index));
}
void visitGlobalGet(GlobalGet* curr) {}
void visitGlobalSet(GlobalSet* curr) {
self()->noteSubtype(curr->value,
self()->getModule()->getGlobal(curr->name)->type);
}
void visitLoad(Load* curr) {}
void visitStore(Store* curr) {}
void visitAtomicRMW(AtomicRMW* curr) {}
void visitAtomicCmpxchg(AtomicCmpxchg* curr) {}
void visitAtomicWait(AtomicWait* curr) {}
void visitAtomicNotify(AtomicNotify* curr) {}
void visitAtomicFence(AtomicFence* curr) {}
void visitPause(Pause* curr) {}
void visitSIMDExtract(SIMDExtract* curr) {}
void visitSIMDReplace(SIMDReplace* curr) {}
void visitSIMDShuffle(SIMDShuffle* curr) {}
void visitSIMDTernary(SIMDTernary* curr) {}
void visitSIMDShift(SIMDShift* curr) {}
void visitSIMDLoad(SIMDLoad* curr) {}
void visitSIMDLoadStoreLane(SIMDLoadStoreLane* curr) {}
void visitMemoryInit(MemoryInit* curr) {}
void visitDataDrop(DataDrop* curr) {}
void visitMemoryCopy(MemoryCopy* curr) {}
void visitMemoryFill(MemoryFill* curr) {}
void visitConst(Const* curr) {}
void visitUnary(Unary* curr) {}
void visitBinary(Binary* curr) {}
void visitWideIntAddSub(WideIntAddSub* curr) {}
void visitWideIntMul(WideIntMul* curr) {}
void visitSelect(Select* curr) {
self()->noteSubtype(curr->ifTrue, curr);
self()->noteSubtype(curr->ifFalse, curr);
}
void visitDrop(Drop* curr) {}
void visitReturn(Return* curr) {
if (curr->value) {
self()->noteSubtype(curr->value, self()->getFunction()->getResults());
}
}
void visitMemorySize(MemorySize* curr) {}
void visitMemoryGrow(MemoryGrow* curr) {}
void visitUnreachable(Unreachable* curr) {}
void visitPop(Pop* curr) {}
void visitRefNull(RefNull* curr) {}
void visitRefIsNull(RefIsNull* curr) {}
void visitRefFunc(RefFunc* curr) {}
void visitRefEq(RefEq* curr) {
// Match the shareability of the current content (if it exists).
// TODO: This could also allow both sides to flip.
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(HeapTypes::eq.getBasic(share), Nullable);
self()->noteNonFlowSubtype(curr->left, type);
self()->noteNonFlowSubtype(curr->right, type);
}
void visitTableGet(TableGet* curr) {}
void visitTableSet(TableSet* curr) {
self()->noteSubtype(curr->value,
self()->getModule()->getTable(curr->table)->type);
}
void visitTableSize(TableSize* curr) {}
void visitTableGrow(TableGrow* curr) {
self()->noteSubtype(curr->value,
self()->getModule()->getTable(curr->table)->type);
}
void visitTableFill(TableFill* curr) {
self()->noteSubtype(curr->value,
self()->getModule()->getTable(curr->table)->type);
}
void visitTableCopy(TableCopy* curr) {
self()->noteSubtype(self()->getModule()->getTable(curr->sourceTable)->type,
self()->getModule()->getTable(curr->destTable)->type);
}
void visitTableInit(TableInit* curr) {
auto* seg = self()->getModule()->getElementSegment(curr->segment);
self()->noteSubtype(seg->type,
self()->getModule()->getTable(curr->table)->type);
}
void visitElemDrop(ElemDrop* curr) {}
void visitTry(Try* curr) {
self()->noteSubtype(curr->body, curr);
for (auto*& body : curr->catchBodies) {
self()->noteSubtype(body, curr);
}
}
void visitTryTable(TryTable* curr) {
self()->noteSubtype(curr->body, curr);
for (Index i = 0; i < curr->catchTags.size(); i++) {
self()->noteSubtype(curr->sentTypes[i],
self()->findBreakTarget(curr->catchDests[i]));
}
}
void visitThrow(Throw* curr) {
Type params = self()->getModule()->getTag(curr->tag)->params();
assert(params.size() == curr->operands.size());
for (size_t i = 0, size = curr->operands.size(); i < size; ++i) {
self()->noteSubtype(curr->operands[i], params[i]);
}
}
void visitRethrow(Rethrow* curr) {}
void visitThrowRef(ThrowRef* curr) {}
void visitTupleMake(TupleMake* curr) {}
void visitTupleExtract(TupleExtract* curr) {}
void visitRefI31(RefI31* curr) {}
void visitI31Get(I31Get* curr) {
// This could be |noteNonFlowSubtype| but as there are no subtypes of i31
// it does not matter.
// TODO: This should be i31 with a sharedness type variable, but we will
// have to refactor this to use principal types to represent that
// accurately. For now look at the operand to guess the proper sharedness.
auto share = Unshared;
if (curr->i31->type.isRef() && curr->i31->type.getHeapType().isShared()) {
share = Shared;
}
self()->noteSubtype(curr->i31,
Type(HeapTypes::i31.getBasic(share), Nullable));
}
void visitCallRef(CallRef* curr) {
// Even if we are unreachable, the target must be valid, and in particular
// it cannot be funcref - it must be a proper signature type. We could
// perhaps have |addStrictSubtype| to handle that, but for now just require
// that the target keep its type.
//
// Note that even if we are reachable, there is an interaction between the
// target and the the types of the parameters and results (the target's type
// must support the parameter and result types properly), and so it is not
// obvious how users would want to optimize here (if they are trying to
// generalize, should they generalize the target more or the parameters
// more? etc.), so we do the simple thing here for now of requiring the
// target type not generalize.
//
// Note that this could be |noteNonFlowSubtype| but since we are comparing
// a type to itself here, that does not matter.
self()->noteSubtype(curr->target, curr->target->type);
if (curr->target->type.isSignature()) {
handleCall(curr, curr->target->type.getHeapType().getSignature());
}
}
void visitRefTest(RefTest* curr) {
self()->noteCast(curr->ref, curr->castType);
}
void visitRefCast(RefCast* curr) { self()->noteCast(curr->ref, curr); }
void visitRefGetDesc(RefGetDesc* curr) {}
void visitBrOn(BrOn* curr) {
switch (curr->op) {
case BrOnNull:
case BrOnNonNull:
break;
case BrOnCast:
case BrOnCastFail:
case BrOnCastDescEq:
case BrOnCastDescEqFail:
self()->noteCast(curr->ref, curr->castType);
break;
}
self()->noteSubtype(curr->getSentType(),
self()->findBreakTarget(curr->name));
}
void visitStructNew(StructNew* curr) {
if (!curr->type.isStruct() || curr->isWithDefault()) {
return;
}
const auto& fields = curr->type.getHeapType().getStruct().fields;
assert(fields.size() == curr->operands.size());
for (size_t i = 0, size = fields.size(); i < size; ++i) {
self()->noteSubtype(curr->operands[i], fields[i].type);
}
}
void visitStructGet(StructGet* curr) {}
void visitStructSet(StructSet* curr) {
if (!curr->ref->type.isStruct()) {
return;
}
const auto& fields = curr->ref->type.getHeapType().getStruct().fields;
self()->noteSubtype(curr->value, fields[curr->index].type);
}
void visitStructRMW(StructRMW* curr) {
if (!curr->ref->type.isStruct()) {
return;
}
const auto& fields = curr->ref->type.getHeapType().getStruct().fields;
self()->noteSubtype(curr->value, fields[curr->index].type);
}
void visitStructCmpxchg(StructCmpxchg* curr) {
if (!curr->ref->type.isStruct()) {
return;
}
const auto& fields = curr->ref->type.getHeapType().getStruct().fields;
auto type = fields[curr->index].type;
Type expectedType = type;
if (type.isRef()) {
expectedType =
Type(HeapTypes::eq.getBasic(type.getHeapType().getShared()), Nullable);
}
self()->noteSubtype(curr->expected, expectedType);
self()->noteSubtype(curr->replacement, type);
}
void visitStructWait(StructWait* curr) {
self()->noteSubtype(curr->waitqueue,
Type(HeapTypes::sharedWaitqueue, Nullable));
if (!curr->ref->type.isStruct()) {
return;
}
const auto& fields = curr->ref->type.getHeapType().getStruct().fields;
auto expectedType = fields[curr->index].type;
if (expectedType.isRef()) {
expectedType = Type(HeapTypes::eq.getBasic(Shared), Nullable);
}
self()->noteSubtype(curr->expected, expectedType);
}
void visitWaitqueueNew(WaitqueueNew* curr) {}
void visitWaitqueueNotify(WaitqueueNotify* curr) {
self()->noteSubtype(curr->waitqueue,
Type(HeapTypes::sharedWaitqueue, Nullable));
}
void visitPublish(Publish* curr) {}
void visitArrayNew(ArrayNew* curr) {
if (!curr->type.isArray() || curr->isWithDefault()) {
return;
}
auto array = curr->type.getHeapType().getArray();
self()->noteSubtype(curr->init, array.element.type);
}
void visitArrayNewData(ArrayNewData* curr) {}
void visitArrayNewElem(ArrayNewElem* curr) {
if (!curr->type.isArray()) {
return;
}
auto array = curr->type.getHeapType().getArray();
auto* seg = self()->getModule()->getElementSegment(curr->segment);
self()->noteSubtype(seg->type, array.element.type);
}
void visitArrayNewFixed(ArrayNewFixed* curr) {
if (!curr->type.isArray()) {
return;
}
auto array = curr->type.getHeapType().getArray();
for (auto*& value : curr->values) {
self()->noteSubtype(value, array.element.type);
}
}
void visitArrayGet(ArrayGet* curr) {}
void visitArraySet(ArraySet* curr) {
if (!curr->ref->type.isArray()) {
return;
}
auto array = curr->ref->type.getHeapType().getArray();
self()->noteSubtype(curr->value, array.element.type);
}
void visitArrayLoad(ArrayLoad* curr) {}
void visitArrayStore(ArrayStore* curr) {}
void visitArrayLen(ArrayLen* curr) {}
void visitArrayCopy(ArrayCopy* curr) {
if (!curr->srcRef->type.isArray() || !curr->destRef->type.isArray()) {
return;
}
auto src = curr->srcRef->type.getHeapType().getArray();
auto dest = curr->destRef->type.getHeapType().getArray();
self()->noteSubtype(src.element.type, dest.element.type);
}
void visitArrayFill(ArrayFill* curr) {
if (!curr->ref->type.isArray()) {
return;
}
auto array = curr->ref->type.getHeapType().getArray();
self()->noteSubtype(curr->value, array.element.type);
}
void visitArrayInitData(ArrayInitData* curr) {}
void visitArrayInitElem(ArrayInitElem* curr) {
if (!curr->ref->type.isArray()) {
return;
}
auto array = curr->ref->type.getHeapType().getArray();
auto* seg = self()->getModule()->getElementSegment(curr->segment);
self()->noteSubtype(seg->type, array.element.type);
}
void visitArrayRMW(ArrayRMW* curr) {
if (!curr->ref->type.isArray()) {
return;
}
auto array = curr->ref->type.getHeapType().getArray();
self()->noteSubtype(curr->value, array.element.type);
}
void visitArrayCmpxchg(ArrayCmpxchg* curr) {
if (!curr->ref->type.isArray()) {
return;
}
auto type = curr->ref->type.getHeapType().getArray().element.type;
Type expectedType = type;
if (type.isRef()) {
expectedType =
Type(HeapTypes::eq.getBasic(type.getHeapType().getShared()), Nullable);
}
self()->noteSubtype(curr->expected, expectedType);
self()->noteSubtype(curr->replacement, type);
}
void visitRefAs(RefAs* curr) {
switch (curr->op) {
case RefAsNonNull:
self()->noteCast(curr->value, curr);
return;
case AnyConvertExtern:
// any.convert_extern and extern.convert_any return nullable types iff
// their inputs are nullable. To preserve the existing non-nullable
// output type, the input must remain non-nullable, but if the existing
// output type is nullable, the input can be, too.
if (curr->type != Type::unreachable) {
auto ext =
HeapTypes::ext.getBasic(curr->type.getHeapType().getShared());
auto nullability = curr->type.getNullability();
self()->noteSubtype(curr->value, Type(ext, nullability));
}
return;
case ExternConvertAny:
if (curr->type != Type::unreachable) {
auto any =
HeapTypes::any.getBasic(curr->type.getHeapType().getShared());
auto nullability = curr->type.getNullability();
self()->noteSubtype(curr->value, Type(any, nullability));
}
return;
}
}
void visitStringNew(StringNew* curr) {}
void visitStringConst(StringConst* curr) {}
void visitStringMeasure(StringMeasure* curr) {}
void visitStringEncode(StringEncode* curr) {}
void visitStringConcat(StringConcat* curr) {}
void visitStringEq(StringEq* curr) {}
void visitStringTest(StringTest* curr) {}
void visitStringWTF16Get(StringWTF16Get* curr) {}
void visitStringSliceWTF(StringSliceWTF* curr) {}
void visitContNew(ContNew* curr) {
if (!curr->type.isContinuation()) {
return;
}
// The type of the function reference must remain a subtype of the function
// type expected by the continuation.
self()->noteSubtype(curr->func->type.getHeapType(),
curr->type.getHeapType().getContinuation().type);
}
void visitContBind(ContBind* curr) {
if (!curr->cont->type.isContinuation() || !curr->type.isContinuation()) {
return;
}
auto inType = curr->cont->type.getHeapType();
auto outType = curr->type.getHeapType();
auto sigIn = inType.getContinuation().type.getSignature();
auto sigOut = outType.getContinuation().type.getSignature();
// Each of the bound arguments must remain subtypes of their expected
// parameters.
size_t numBound = curr->operands.size();
for (Index i = 0; i < numBound; ++i) {
self()->noteSubtype(curr->operands[i], sigIn.params[i]);
}
// Each of the unbound output parameters must remain subtypes of their
// corresponding input parameters.
size_t numRemaining = sigIn.params.size() - numBound;
for (Index i = 0; i < numRemaining; ++i) {
self()->noteSubtype(sigOut.params[i], sigIn.params[numBound + i]);
}
// The original input results must remain a subtype of the new output
// results.
self()->noteSubtype(sigIn.results, sigOut.results);
}
void visitSuspend(Suspend* curr) {
auto sig = self()->getModule()->getTag(curr->tag)->type.getSignature();
// The operands must remain subtypes of the parameters given by the tag.
assert(curr->operands.size() == sig.params.size());
for (Index i = 0; i < curr->operands.size(); ++i) {
self()->noteSubtype(curr->operands[i], sig.params[i]);
}
}
void processResumeHandlers(Type contType,
const ArenaVector<Name>& handlerTags,
const ArenaVector<Name>& handlerBlocks) {
assert(contType.isContinuation());
auto contSig = contType.getHeapType().getContinuation().type.getSignature();
assert(handlerTags.size() == handlerBlocks.size());
auto& wasm = *self()->getModule();
// Process each handler in turn.
for (Index i = 0; i < handlerTags.size(); ++i) {
if (!handlerBlocks[i]) {
// Switch handlers do not constrain types in any way.
continue;
}
auto tagSig = wasm.getTag(handlerTags[i])->type.getSignature();
// The types sent on suspensions with this tag must remain subtypes of the
// types expected at the target block.
auto expected = self()->findBreakTarget(handlerBlocks[i])->type;
assert(tagSig.params.size() + 1 == expected.size());
for (Index j = 0; j < tagSig.params.size(); ++j) {
self()->noteSubtype(tagSig.params[j], expected[j]);
}
auto nextSig = expected[expected.size() - 1]
.getHeapType()
.getContinuation()
.type.getSignature();
// The types we send to the next continuation must remain subtypes of the
// types the continuation is expecting based on the tag results.
self()->noteSubtype(nextSig.params, tagSig.results);
// The types returned by the current continuation must remain subtypes of
// the types returned by the next continuation.
self()->noteSubtype(contSig.results, nextSig.results);
}
}
void visitResume(Resume* curr) {
if (!curr->cont->type.isContinuation()) {
return;
}
processResumeHandlers(
curr->cont->type, curr->handlerTags, curr->handlerBlocks);
// The types we send to the resumed continuation must remain subtypes of the
// types expected by the continuation.
auto params = curr->cont->type.getHeapType()
.getContinuation()
.type.getSignature()
.params;
assert(curr->operands.size() == params.size());
for (Index i = 0; i < curr->operands.size(); ++i) {
self()->noteSubtype(curr->operands[i], params[i]);
}
}
void visitResumeThrow(ResumeThrow* curr) {
if (!curr->cont->type.isContinuation()) {
return;
}
processResumeHandlers(
curr->cont->type, curr->handlerTags, curr->handlerBlocks);
if (curr->tag) {
// The types we use to create the exception package must remain subtypes
// of the types expected by the exception tag.
auto params =
self()->getModule()->getTag(curr->tag)->type.getSignature().params;
assert(curr->operands.size() == params.size());
for (Index i = 0; i < curr->operands.size(); ++i) {
self()->noteSubtype(curr->operands[i], params[i]);
}
}
}
void visitStackSwitch(StackSwitch* curr) {
if (!curr->cont->type.isContinuation()) {
return;
}
// The types sent when switching must remain subtypes of the types expected
// by the target continuation.
auto contSig =
curr->cont->type.getHeapType().getContinuation().type.getSignature();
assert(curr->operands.size() + 1 == contSig.params.size());
for (Index i = 0; i < curr->operands.size(); ++i) {
self()->noteSubtype(curr->operands[i], contSig.params[i]);
}
// The type returned by the target continuation must remain a subtype of the
// type the current continuation returns as indicated by the tag result.
auto currResult =
self()->getModule()->getTag(curr->tag)->type.getSignature().results;
self()->noteSubtype(contSig.results, currResult);
// The type returned by the current continuation must remain a subtype of
// the type returned by the return continuation.
auto retSig = contSig.params[contSig.params.size() - 1]
.getHeapType()
.getContinuation()
.type.getSignature();
self()->noteSubtype(currResult, retSig.results);
}
};
} // namespace wasm
#endif // #define wasm_ir_subtype_exprs_h