blob: 5bc07467da8b4016640eda472630bc79d7d32e9a [file] [log] [blame] [edit]
/*
* 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.
*/
#define UNSUBTYPING_DEBUG 0
#include <cstddef>
#include <iterator>
#include <memory>
#if !UNSUBTYPING_DEBUG
#include <unordered_map>
#include <unordered_set>
#endif
#include "ir/effects.h"
#include "ir/localize.h"
#include "ir/module-utils.h"
#include "ir/names.h"
#include "ir/subtype-exprs.h"
#include "ir/type-updating.h"
#include "ir/utils.h"
#include "pass.h"
#include "support/index.h"
#include "wasm-traversal.h"
#include "wasm-type.h"
#include "wasm.h"
#if UNSUBTYPING_DEBUG
#include "support/insert_ordered.h"
#endif
#if UNSUBTYPING_DEBUG
#define DBG(x) x
#else
#define DBG(x)
#endif
// Compute and use the minimal subtype (and descriptor) relations required to
// maintain module validity and behavior. This minimal relation will be a subset
// of the original subtype (and descriptor) relations. Start by walking the IR
// and collecting pairs of types that need to be in the subtype relation for
// each expression to validate (or require a type to have a descriptor). For
// example, a local.set requires that the type of its operand be a subtype of
// the local's type. Casts do not generate subtypings at this point because it
// is not necessary for the cast target to be a subtype of the cast source for
// the cast to validate.
//
// From that initial subtype relation, we then start finding new subtypings (and
// descriptors) that are required by the subtypings we have found already. These
// transitively required subtypings (and descriptors) come from three sources.
//
// The first source is type definitions. Consider these type definitions:
//
// (type $A (sub (struct (ref $X))))
// (type $B (sub $A (struct (ref $Y))))
//
// If we have determined that $B must remain a subtype of $A, then we know that
// $Y must remain a subtype of $X as well, since the type definitions would not
// be valid otherwise. Similarly, knowing that $X must remain a subtype of $Y
// may transitively require other subtypings as well based on their type
// definitions.
//
// The second source of transitive subtyping requirements is casts. Although
// casting from one type to another does not necessarily require that those
// types are related, we do need to make sure that we do not change the
// behavior of casts by removing subtype relationships they might observe. For
// example, consider this module:
//
// (module
// ;; original subtyping: $bot <: $mid <: $top
// (type $top (sub (struct)))
// (type $mid (sub $top (struct)))
// (type $bot (sub $mid (struct)))
//
// (func $f
// (local $top (ref $top))
// (local $mid (ref $mid))
//
// ;; Requires $bot <: $top
// (local.set $top (struct.new $bot))
//
// ;; Cast $top to $mid
// (local.set $mid (ref.cast (ref $mid) (local.get $top)))
// )
// )
//
// The only subtype relation directly required by the IR for this module is $bot
// <: $top. However, if we optimized the module so that $bot <: $top was the
// only subtype relation, we would change the behavior of the cast. In the
// original module, a value of type (ref $bot) is cast to (ref $mid). The cast
// succeeds because in the original module, $bot <: $mid. If we optimize so that
// we have $bot <: $top and no other subtypings, though, the cast will fail
// because the value of type (ref $bot) no longer inhabits (ref $mid). To
// prevent the cast's behavior from changing, we need to ensure that $bot <:
// $mid.
//
// The set of subtyping requirements generated by a cast from $src to $dest is
// that for every known remaining subtype $v of $src, if $v <: $dest in the
// original module, then $v <: $dest in the optimized module. In other words,
// for every type $v of values we know can flow into the cast, if the cast would
// have succeeded for values of type $v before, then we know the cast must
// continue to succeed for values of type $v. These requirements arising from
// casts can also generate transitive requirements because we learn about new
// types of values that can flow into casts as we learn about new subtypes of
// cast sources.
//
// The third source of transitive subtyping requirements is the discovery of
// required descriptors (and vice versa). Subtyping and descriptors combine to
// form this diagram, where rightward arrows mean "described by":
//
// A -> A.desc
// ^ ^
// | |
// B -> B.desc
//
// If any three of these types exist in these relations with the others, then
// the validation rules require that the fourth type also exist and be in these
// relations. The only exception is that A.desc is allowed to be missing. This
// complex and recursive relationship between subtyping and descriptor relations
// is why we optimize out unneeded descriptors in this pass rather than e.g.
// GlobalTypeOptimization.
//
// Starting with the initial subtype and descriptor relations determined by
// walking the IR, repeatedly search for new subtypings and descriptors by
// analyzing type definitions and casts until we reach a fixed point. This is
// the minimal subtype/descriptor relation that preserves module validity and
// behavior that can be found without a more precise analysis of types that
// might flow into each cast.
namespace wasm {
namespace {
#if UNSUBTYPING_DEBUG
template<typename K, typename V> using Map = InsertOrderedMap<K, V>;
template<typename T> using Set = InsertOrderedSet<T>;
#else
template<typename K, typename V> using Map = std::unordered_map<K, V>;
template<typename T> using Set = std::unordered_set<T>;
#endif
// A tree (or rather a forest) of types with the ability to query and set
// supertypes in constant time and efficiently iterate over supertypes and
// subtypes.
struct TypeTree {
struct Node {
// The type represented by this node.
HeapType type;
// The index of the parent (supertype) in the list of nodes. Set to the
// index of this node if there is no parent.
Index parent;
// The index of this node in the parent's list of children, if any, enabling
// O(1) updates.
Index indexInParent = 0;
// The indices of the children (subtypes) in the list of nodes.
std::vector<Index> children;
// The index of the described and descriptor types, if they are necessary.
std::optional<Index> described;
std::optional<Index> descriptor;
Node(HeapType type, Index index) : type(type), parent(index) {}
};
std::vector<Node> nodes;
Map<HeapType, Index> indices;
void setSupertype(HeapType sub, HeapType super) {
auto childIndex = getIndex(sub);
auto parentIndex = getIndex(super);
auto& childNode = nodes[childIndex];
auto& parentNode = nodes[parentIndex];
// Remove sub from its old supertype if necessary.
if (auto oldParentIndex = childNode.parent; oldParentIndex != childIndex) {
auto& oldParentNode = nodes[oldParentIndex];
// Move sub to the back of its parent's children and then pop it.
auto& children = oldParentNode.children;
assert(children[childNode.indexInParent] == childIndex);
auto& swappedNode = nodes[children.back()];
assert(swappedNode.indexInParent == children.size() - 1);
// Swap the indices in the parent's child vector.
std::swap(children[childNode.indexInParent], children.back());
// Swap the index in the kept child.
swappedNode.indexInParent = childNode.indexInParent;
children.pop_back();
}
childNode.parent = parentIndex;
childNode.indexInParent = parentNode.children.size();
parentNode.children.push_back(childIndex);
}
std::optional<HeapType> getSupertype(HeapType type) const {
auto index = maybeGetIndex(type);
if (!index) {
return std::nullopt;
}
auto parentIndex = nodes[*index].parent;
if (parentIndex == *index) {
return std::nullopt;
}
return nodes[parentIndex].type;
}
void setDescriptor(HeapType described, HeapType descriptor) {
auto describedIndex = getIndex(described);
auto descriptorIndex = getIndex(descriptor);
auto& describedNode = nodes[describedIndex];
auto& descriptorNode = nodes[descriptorIndex];
// We only ever set the descriptor once.
assert(!describedNode.descriptor);
assert(!descriptorNode.described);
describedNode.descriptor = descriptorIndex;
descriptorNode.described = describedIndex;
}
std::optional<HeapType> getDescriptor(HeapType type) const {
auto index = maybeGetIndex(type);
if (!index) {
return std::nullopt;
}
if (auto descIndex = nodes[*index].descriptor) {
return nodes[*descIndex].type;
}
return std::nullopt;
}
std::optional<HeapType> getDescribed(HeapType type) const {
auto index = maybeGetIndex(type);
if (!index) {
return std::nullopt;
}
if (auto descIndex = nodes[*index].described) {
return nodes[*descIndex].type;
}
return std::nullopt;
}
struct SupertypeIterator {
using value_type = const HeapType;
using difference_type = std::ptrdiff_t;
using reference = const HeapType&;
using pointer = const HeapType*;
using iterator_category = std::input_iterator_tag;
TypeTree* parent;
std::optional<Index> index;
bool operator==(const SupertypeIterator& other) const {
return index == other.index;
}
bool operator!=(const SupertypeIterator& other) const {
return !(*this == other);
}
const HeapType& operator*() const { return parent->nodes[*index].type; }
const HeapType* operator->() const { return &*(*this); }
SupertypeIterator& operator++() {
auto parentIndex = parent->nodes[*index].parent;
if (parentIndex == *index) {
index = std::nullopt;
} else {
index = parentIndex;
}
return *this;
}
SupertypeIterator operator++(int) {
auto it = *this;
++(*this);
return it;
}
};
struct Supertypes {
TypeTree* parent;
Index index;
SupertypeIterator begin() { return {parent, index}; }
SupertypeIterator end() { return {parent, std::nullopt}; }
};
Supertypes supertypes(HeapType type) { return {this, getIndex(type)}; }
struct ImmediateSubtypeIterator {
using value_type = const HeapType;
using difference_type = std::ptrdiff_t;
using reference = const HeapType&;
using pointer = const HeapType*;
using iterator_category = std::input_iterator_tag;
TypeTree* parent;
std::vector<Index>::const_iterator child;
bool operator==(const ImmediateSubtypeIterator& other) const {
return child == other.child;
}
bool operator!=(const ImmediateSubtypeIterator& other) const {
return !(*this == other);
}
const HeapType& operator*() const { return parent->nodes[*child].type; }
const HeapType* operator->() const { return &*(*this); }
ImmediateSubtypeIterator& operator++() {
++child;
return *this;
}
ImmediateSubtypeIterator operator++(int) {
auto it = *this;
++(*this);
return it;
}
};
struct ImmediateSubtypes {
TypeTree* parent;
Index index;
ImmediateSubtypeIterator begin() {
return {parent, parent->nodes[index].children.begin()};
}
ImmediateSubtypeIterator end() {
return {parent, parent->nodes[index].children.end()};
}
};
ImmediateSubtypes immediateSubtypes(HeapType type) {
return {this, getIndex(type)};
}
struct SubtypeIterator {
using value_type = const HeapType;
using difference_type = std::ptrdiff_t;
using reference = const HeapType&;
using pointer = const HeapType*;
using iterator_category = std::input_iterator_tag;
TypeTree* parent;
// DFS stack of (node index, child index) pairs.
std::vector<std::pair<Index, Index>> stack;
bool operator==(const SubtypeIterator& other) {
return stack == other.stack;
}
bool operator!=(const SubtypeIterator& other) { return !(*this == other); }
const HeapType& operator*() const {
return parent->nodes[stack.back().first].type;
}
const HeapType* operator->() const { return &*(*this); }
SubtypeIterator& operator++() {
while (true) {
if (stack.empty()) {
return *this;
}
auto& [index, childIndex] = stack.back();
auto& children = parent->nodes[index].children;
if (childIndex == children.size()) {
stack.pop_back();
} else {
auto child = children[childIndex++];
stack.push_back({child, 0u});
return *this;
}
}
}
SubtypeIterator operator++(int) {
auto it = *this;
++(*this);
return it;
}
};
struct Subtypes {
TypeTree* parent;
Index index;
SubtypeIterator begin() { return {parent, {std::make_pair(index, 0u)}}; }
SubtypeIterator end() { return {parent, {}}; }
};
Subtypes subtypes(HeapType type) { return {this, getIndex(type)}; }
#if UNSUBTYPING_DEBUG
void dump(Module& wasm) {
for (auto& node : nodes) {
std::cerr << ModuleHeapType(wasm, node.type);
if (auto super = getSupertype(node.type)) {
std::cerr << " <: " << ModuleHeapType(wasm, *super);
}
if (auto desc = getDescribed(node.type)) {
std::cerr << ", describes " << ModuleHeapType(wasm, *desc);
}
if (auto desc = getDescriptor(node.type)) {
std::cerr << ", descriptor " << ModuleHeapType(wasm, *desc);
}
std::cerr << ", children:";
for (auto child : node.children) {
std::cerr << " " << ModuleHeapType(wasm, nodes[child].type);
}
std::cerr << '\n';
}
}
#endif
private:
Index getIndex(HeapType type) {
auto [it, inserted] = indices.insert({type, nodes.size()});
if (inserted) {
nodes.emplace_back(type, nodes.size());
}
return it->second;
}
std::optional<Index> maybeGetIndex(HeapType type) const {
if (auto it = indices.find(type); it != indices.end()) {
return it->second;
}
return std::nullopt;
}
};
struct Unsubtyping : Pass {
// The kind of work to process.
enum class Kind { Subtype, Descriptor };
// (sub, super) pairs that we have discovered but not yet processed.
std::vector<std::tuple<Kind, HeapType, HeapType>> work;
// Record the type tree with supertype and subtype relations in such a way
// that we can add new supertype relationships in constant time.
TypeTree types;
// Map from cast source types to their destinations.
Map<HeapType, std::vector<HeapType>> casts;
DBG(Module* wasm = nullptr);
void run(Module* wasm) override {
DBG(this->wasm = wasm);
if (!wasm->features.hasGC()) {
return;
}
if (!getPassOptions().closedWorld) {
Fatal() << "Unsubtyping requires --closed-world";
}
// Initialize the subtype relation based on what is immediately required to
// keep the code and public types valid.
analyzePublicTypes(*wasm);
analyzeModule(*wasm);
// Find further subtypings and iterate to a fixed point.
while (!work.empty()) {
auto [kind, a, b] = work.back();
work.pop_back();
switch (kind) {
case Kind::Subtype:
processSubtype(a, b);
break;
case Kind::Descriptor:
processDescriptor(a, b);
break;
}
}
DBG(types.dump(*wasm));
// If we removed a descriptor from a type, we may need to update its
// allocation sites accordingly.
fixupAllocations(*wasm);
rewriteTypes(*wasm);
// Cast types may be refinable if their source and target types are no
// longer related. TODO: Experiment with running this only after checking
// whether it is necessary.
ReFinalize().run(getPassRunner(), wasm);
}
void noteSubtype(HeapType sub, HeapType super) {
// Bottom types are uninteresting, but other basic heap types can be
// interesting because of their interactions with casts.
if (sub == super || sub.isBottom()) {
return;
}
DBG(std::cerr << "noting " << ModuleHeapType(*wasm, sub)
<< " <: " << ModuleHeapType(*wasm, super) << '\n');
work.push_back({Kind::Subtype, sub, super});
}
void noteSubtype(Type sub, Type super) {
if (sub.isTuple()) {
assert(super.isTuple() && sub.size() == super.size());
for (size_t i = 0, size = sub.size(); i < size; ++i) {
noteSubtype(sub[i], super[i]);
}
return;
}
if (!sub.isRef() || !super.isRef()) {
return;
}
noteSubtype(sub.getHeapType(), super.getHeapType());
}
void noteDescriptor(HeapType described, HeapType descriptor) {
DBG(std::cerr << "noting " << ModuleHeapType(*wasm, described) << " -> "
<< ModuleHeapType(*wasm, descriptor) << '\n');
work.push_back({Kind::Descriptor, described, descriptor});
}
void analyzePublicTypes(Module& wasm) {
// We cannot change supertypes for anything public.
for (auto type : ModuleUtils::getPublicHeapTypes(wasm)) {
if (auto super = type.getDeclaredSuperType()) {
noteSubtype(type, *super);
}
if (auto desc = type.getDescriptorType()) {
noteDescriptor(type, *desc);
}
}
}
void analyzeModule(Module& wasm) {
struct Info {
// (source, target) pairs for casts.
Set<std::pair<HeapType, HeapType>> casts;
// Observed (sub, super) subtype constraints.
Set<std::pair<HeapType, HeapType>> subtypings;
// Observed (described, descriptor) requirements.
Set<std::pair<HeapType, HeapType>> descriptors;
};
struct Collector
: ControlFlowWalker<Collector, SubtypingDiscoverer<Collector>> {
using Super =
ControlFlowWalker<Collector, SubtypingDiscoverer<Collector>>;
Info& info;
bool trapsNeverHappen;
Collector(Info& info, bool trapsNeverHappen)
: info(info), trapsNeverHappen(trapsNeverHappen) {}
void noteSubtype(Type sub, Type super) {
if (sub.isTuple()) {
assert(super.isTuple() && sub.size() == super.size());
for (size_t i = 0, size = sub.size(); i < size; ++i) {
noteSubtype(sub[i], super[i]);
}
return;
}
if (!sub.isRef() || !super.isRef()) {
return;
}
noteSubtype(sub.getHeapType(), super.getHeapType());
}
void noteSubtype(HeapType sub, HeapType super) {
assert(HeapType::isSubType(sub, super));
if (sub == super || sub.isBottom()) {
return;
}
info.subtypings.insert({sub, super});
}
void noteSubtype(Type sub, Expression* super) {
noteSubtype(sub, super->type);
}
void noteSubtype(Expression* sub, Type super) {
noteSubtype(sub->type, super);
}
void noteSubtype(Expression* sub, Expression* super) {
noteSubtype(sub->type, super->type);
}
void noteNonFlowSubtype(Expression* 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
// this in order to validate but it does not interact with casts. Given
// that, if super is a basic type then we can simply ignore this: we
// only remove subtyping between user types, so subtyping wrt basic
// types is unchanged, and so this constraint will never be a problem.
//
// This is sort of a hack because in general to be precise we should not
// just consider basic types here - in general, we should note for each
// constraint whether it is a flow-based one or not, and only take the
// flow-based ones into account when looking at the impact of casts.
// However, in practice this is enough as the only non-trivial case of
// |noteNonFlowSubtype| is for RefEq, which uses a basic type (eqref).
// Other cases of non-flow subtyping end up trivial, e.g., the target of
// a CallRef is compared to itself (and we ignore constraints of A :>
// A). However, if we change how |noteNonFlowSubtype| is used in
// SubtypingDiscoverer then we may need to generalize this.
if (super.isRef() && super.getHeapType().isBasic()) {
return;
}
// Otherwise, we must take this into account.
noteSubtype(sub, super);
}
void noteCast(HeapType src, Type dstType) {
auto dst = dstType.getHeapType();
// Casts to self and casts that must fail because they have incompatible
// types are uninteresting.
if (dst == src) {
return;
}
if (HeapType::isSubType(dst, src)) {
if (dstType.isExact()) {
// This cast only tests that the exact destination type is a subtype
// of the source type and does not impose additional requirements on
// subtypes of the destination type like a normal cast does.
info.subtypings.insert({dst, src});
return;
}
info.casts.insert({src, dst});
return;
}
if (HeapType::isSubType(src, dst)) {
// This is an upcast that will always succeed, but only if we ensure
// src <: dst.
info.subtypings.insert({src, dst});
}
}
void noteCast(Expression* src, Type dst) {
if (src->type.isRef() && dst.isRef()) {
noteCast(src->type.getHeapType(), dst);
}
}
void noteCast(Expression* src, Expression* dst) {
if (src->type.isRef() && dst->type.isRef()) {
noteCast(src->type.getHeapType(), dst->type);
}
}
// Visitors for finding required descriptors.
void noteDescribed(HeapType type) {
auto desc = type.getDescriptorType();
assert(desc);
info.descriptors.insert({type, *desc});
}
void noteDescriptor(HeapType type) {
auto desc = type.getDescribedType();
assert(desc);
info.descriptors.insert({*desc, type});
}
void visitRefGetDesc(RefGetDesc* curr) {
Super::visitRefGetDesc(curr);
if (!curr->ref->type.isStruct()) {
return;
}
noteDescribed(curr->ref->type.getHeapType());
}
void visitRefCast(RefCast* curr) {
Super::visitRefCast(curr);
if (!curr->desc || !curr->desc->type.isStruct()) {
return;
}
noteDescriptor(curr->desc->type.getHeapType());
}
void visitBrOn(BrOn* curr) {
Super::visitBrOn(curr);
if (!curr->desc || !curr->desc->type.isStruct()) {
return;
}
noteDescriptor(curr->desc->type.getHeapType());
}
void visitStructNew(StructNew* curr) {
Super::visitStructNew(curr);
if (curr->type == Type::unreachable || !curr->desc) {
return;
}
// Normally we do not treat struct.new as requiring a descriptor, even
// if it has one. We are happy to optimize out descriptors that are set
// in allocations and then never used. But if the descriptor is nullable
// and outside a function context and we assume it may be null and cause
// a trap, then we have no way to preserve that trap without keeping the
// descriptor around.
if (trapsNeverHappen || getFunction() ||
curr->desc->type.isNonNullable()) {
return;
}
// We must preserve the potential trap. When we update the instructions
// later we will move this allocation to a new global if necessary to
// preserve the potential trap even if a parent of the current
// expression is removed.
noteDescribed(curr->type.getHeapType());
}
};
bool trapsNeverHappen = getPassOptions().trapsNeverHappen;
// Collect subtyping constraints and casts from functions in parallel.
ModuleUtils::ParallelFunctionAnalysis<Info> analysis(
wasm, [&](Function* func, Info& info) {
if (!func->imported()) {
Collector(info, trapsNeverHappen).walkFunctionInModule(func, &wasm);
}
});
Info collectedInfo;
for (auto& [_, info] : analysis.map) {
collectedInfo.casts.insert(info.casts.begin(), info.casts.end());
collectedInfo.subtypings.insert(info.subtypings.begin(),
info.subtypings.end());
collectedInfo.descriptors.insert(info.descriptors.begin(),
info.descriptors.end());
}
// Collect constraints from module-level code as well.
Collector collector(collectedInfo, trapsNeverHappen);
collector.walkModuleCode(&wasm);
collector.setModule(&wasm);
for (auto& global : wasm.globals) {
collector.visitGlobal(global.get());
}
for (auto& segment : wasm.elementSegments) {
collector.visitElementSegment(segment.get());
}
// Prepare the collected information for the upcoming processing loop.
for (auto& [sub, super] : collectedInfo.subtypings) {
noteSubtype(sub, super);
}
for (auto [src, dst] : collectedInfo.casts) {
casts[src].push_back(dst);
}
for (auto [described, descriptor] : collectedInfo.descriptors) {
noteDescriptor(described, descriptor);
}
}
void processSubtype(HeapType sub, HeapType super) {
DBG(std::cerr << "processing " << ModuleHeapType(*wasm, sub)
<< " <: " << ModuleHeapType(*wasm, super) << '\n');
assert(HeapType::isSubType(sub, super));
auto oldSuper = types.getSupertype(sub);
if (oldSuper) {
// We already had a recorded supertype. The new supertype might be
// deeper,shallower, or equal to the old supertype. We must recursively
// note the relationship between the old and new supertypes.
if (super == *oldSuper) {
// Nothing new to do here.
return;
}
if (HeapType::isSubType(*oldSuper, super)) {
// sub <: oldSuper <: super
noteSubtype(*oldSuper, super);
// We already handled sub <: oldSuper, so we're done.
return;
}
// sub <: super <: oldSuper
// Eagerly process super <: oldSuper first. This ensures that sub and
// super will already be in the same tree when we process them below, so
// when we process casts we will know that we only need to process up to
// oldSuper.
processSubtype(super, *oldSuper);
}
types.setSupertype(sub, super);
// Complete the descriptor squares to the left and right of the new
// subtyping edge if those squares can possibly exist based on the original
// types.
if (super.getDescribedType()) {
completeDescriptorSquare(
types.getDescribed(super), super, types.getDescribed(sub), sub);
}
if (super.getDescriptorType()) {
completeDescriptorSquare(
super, types.getDescriptor(super), sub, types.getDescriptor(sub));
}
// Find the implied subtypings from the type definitions and casts.
processDefinitions(sub, super);
processCasts(sub, super, oldSuper);
}
void processDescriptor(HeapType described, HeapType descriptor) {
DBG(std::cerr << "processing " << ModuleHeapType(*wasm, described) << " -> "
<< ModuleHeapType(*wasm, descriptor) << '\n');
assert(described.getDescriptorType() &&
*described.getDescriptorType() == descriptor);
if (auto oldDesc = types.getDescriptor(described)) {
// We already know about this descriptor.
assert(*oldDesc == descriptor);
return;
}
types.setDescriptor(described, descriptor);
// Complete the descriptor squares above and below the new descriptor edge.
completeDescriptorSquare(
std::nullopt, types.getSupertype(descriptor), described, descriptor);
for (auto sub : types.immediateSubtypes(described)) {
completeDescriptorSquare(
described, descriptor, sub, types.getDescriptor(sub));
}
for (auto subDesc : types.immediateSubtypes(descriptor)) {
completeDescriptorSquare(
described, descriptor, types.getDescribed(subDesc), subDesc);
}
}
void processDefinitions(HeapType sub, HeapType super) {
if (super.isBasic()) {
return;
}
switch (sub.getKind()) {
case HeapTypeKind::Func: {
auto sig = sub.getSignature();
auto superSig = super.getSignature();
noteSubtype(superSig.params, sig.params);
noteSubtype(sig.results, superSig.results);
break;
}
case HeapTypeKind::Struct: {
const auto& fields = sub.getStruct().fields;
const auto& superFields = super.getStruct().fields;
for (size_t i = 0, size = superFields.size(); i < size; ++i) {
noteSubtype(fields[i].type, superFields[i].type);
}
break;
}
case HeapTypeKind::Array: {
auto elem = sub.getArray().element;
noteSubtype(elem.type, super.getArray().element.type);
break;
}
case HeapTypeKind::Cont:
WASM_UNREACHABLE("TODO: cont");
case HeapTypeKind::Basic:
WASM_UNREACHABLE("unexpected kind");
}
}
void
processCasts(HeapType sub, HeapType super, std::optional<HeapType> oldSuper) {
// We are either attaching the one tree rooted at `sub` under a new
// supertype in another tree, or we are reparenting `sub` below a
// descendent of `oldSuper` in the same tree. In the former case, we must
// evaluate `sub` and all its subtypes against all its new supertypes and
// their cast destinations. In the latter case, `sub` and all its subtypes
// must have already been evaluated against `oldSuper` and its supertypes,
// so we only need to additionally evaluate them against supertypes up to
// `oldSuper`.
for (auto type : types.subtypes(sub)) {
for (auto src : types.supertypes(super)) {
if (oldSuper && src == *oldSuper) {
break;
}
for (auto dst : casts[src]) {
if (HeapType::isSubType(type, dst)) {
noteSubtype(type, dst);
}
}
}
}
}
void completeDescriptorSquare(std::optional<HeapType> super,
std::optional<HeapType> superDesc,
std::optional<HeapType> sub,
std::optional<HeapType> subDesc) {
if ((super && super->isBasic()) || (superDesc && superDesc->isBasic())) {
// Basic types do not have descriptors or described types, so do not form
// descriptor squares.
return;
}
if (bool(super) + bool(superDesc) + bool(sub) + bool(subDesc) < 3) {
// We must have two adjacent edges (involving at least 3 types) for there
// to be any further requirements.
return;
}
// There may be up to one missing type. Look it up using its original
// descriptor relation with the present types and add the missing edges.
if (!super) {
super = superDesc->getDescribedType();
} else if (!sub) {
sub = subDesc->getDescribedType();
} else if (!subDesc) {
subDesc = sub->getDescriptorType();
} else if (!superDesc) {
// This is the only type that is allowed to be missing.
return;
}
// Add all the edges. Don't worry about duplicating existing edges because
// checking whether they're necessary now would be about as expensive as
// discarding them later.
// TODO: We will be able to assume this once we update the descriptor
// validation rules.
if (HeapType::isSubType(*sub, *super)) {
noteSubtype(*sub, *super);
}
noteSubtype(*subDesc, *superDesc);
noteDescriptor(*super, *superDesc);
noteDescriptor(*sub, *subDesc);
}
void rewriteTypes(Module& wasm) {
struct Rewriter : GlobalTypeRewriter {
Unsubtyping& parent;
Rewriter(Unsubtyping& parent, Module& wasm)
: GlobalTypeRewriter(wasm), parent(parent) {}
std::optional<HeapType> getDeclaredSuperType(HeapType type) override {
if (auto super = parent.types.getSupertype(type);
super && !super->isBasic()) {
return *super;
}
return std::nullopt;
}
void modifyTypeBuilderEntry(TypeBuilder& typeBuilder,
Index i,
HeapType oldType) override {
if (!parent.types.getDescribed(oldType)) {
typeBuilder[i].describes(std::nullopt);
}
if (!parent.types.getDescriptor(oldType)) {
typeBuilder[i].descriptor(std::nullopt);
}
}
};
Rewriter(*this, wasm).update();
}
void fixupAllocations(Module& wasm) {
if (!wasm.features.hasCustomDescriptors()) {
return;
}
// TODO: Consider running the fixup only if we are actually removing any
// descriptors. This would require a better way of detecting this than
// collecing and iterating over all the types, though.
struct Rewriter : WalkerPass<PostWalker<Rewriter>> {
const TypeTree& types;
// Allocations that might trap that have been removed from module-level
// initializers. These need to be placed in new globals to preserve any
// instantiation-time traps.
std::vector<Expression*> removedTrappingInits;
Rewriter(const TypeTree& types) : types(types) {}
bool isFunctionParallel() override { return true; }
// Only introduces locals that are set immediately before they are used.
bool requiresNonNullableLocalFixups() override { return false; }
std::unique_ptr<Pass> create() override {
return std::make_unique<Rewriter>(types);
}
void visitStructNew(StructNew* curr) {
if (curr->type == Type::unreachable) {
return;
}
if (!curr->desc) {
return;
}
if (types.getDescriptor(curr->type.getHeapType())) {
return;
}
// We need to drop the descriptor argument. In a function context, use
// ChildLocalizer. Outside a function context just drop the operand
// because there can be no side effects anyway.
if (auto* func = getFunction()) {
// Preserve a trap from a null descriptor if necessary.
if (!getPassOptions().trapsNeverHappen &&
curr->desc->type.isNullable()) {
curr->desc =
Builder(*getModule()).makeRefAs(RefAsNonNull, curr->desc);
}
auto* block =
ChildLocalizer(curr, func, *getModule(), getPassOptions())
.getChildrenReplacement();
block->list.push_back(curr);
block->type = curr->type;
replaceCurrent(block);
} else {
// We are dropping this descriptor, but it might have a potential trap
// nested inside it. In that case we need to preserve the trap by
// moving this descriptor to a new global.
if (curr->desc->is<StructNew>() &&
EffectAnalyzer(getPassOptions(), *getModule(), curr->desc).trap) {
removedTrappingInits.push_back(curr->desc);
}
}
curr->desc = nullptr;
}
};
Rewriter rewriter(types);
rewriter.run(getPassRunner(), &wasm);
rewriter.runOnModuleCode(getPassRunner(), &wasm);
// Insert globals necessary to preserve instantiation-time trapping of
// removed allocations.
for (Index i = 0; i < rewriter.removedTrappingInits.size(); ++i) {
auto* curr = rewriter.removedTrappingInits[i];
auto name = Names::getValidGlobalName(
wasm, std::string("unsubtyping-removed-") + std::to_string(i));
wasm.addGlobal(
Builder::makeGlobal(name, curr->type, curr, Builder::Immutable));
}
}
};
} // anonymous namespace
Pass* createUnsubtypingPass() { return new Unsubtyping(); }
} // namespace wasm