blob: be8da22cefa240d9081daee33c54775b2e7def6a [file] [log] [blame] [edit]
/*
* Copyright 2024 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.
*/
// This is a J2CL specific pass that merges itables into vtables. It is meant
// to be run at the beginning before structs corresponding to Java classes are
// optimized.
//
// The motivation for embedding itables into vtables is to reduce memory usage.
//
// The pass makes the following transformation on the structs related to Java
// classes. For given type `Foo` with `Foo[vtable] = { m1, m2, m3, ... }`
// and `Foo[itable] = { i1, i2, ...}`, this pass transforms it to
// `Foo[vtable] = { i1, i2, ...., m1, m2, m3, ... }`, and fixes all accesses
// and initializations accordingly.
#include <unordered_map>
#include <unordered_set>
#include "ir/effects.h"
#include "ir/localize.h"
#include "ir/ordering.h"
#include "ir/struct-utils.h"
#include "ir/subtypes.h"
#include "ir/type-updating.h"
#include "ir/utils.h"
#include "pass.h"
#include "wasm-builder.h"
#include "wasm-type.h"
#include "wasm.h"
namespace wasm {
namespace {
// Information about the structs that have vtables and itables.
struct StructInfo {
HeapType javaClass;
HeapType vtable;
HeapType itable;
};
struct J2CLItableMerging : public Pass {
// Keep track of all the structInfos so that they will be automatically
// released after the pass is done.
std::list<StructInfo> structInfos;
// Globals that hold vtables and itables indexed by their heap type.
// There is exactly 1 global for each vtable/itable type.
std::unordered_map<HeapType, Global*> tableGlobalsByType;
std::unordered_map<HeapType, StructInfo*> structInfoByVtableType;
std::unordered_map<HeapType, StructInfo*> structInfoByITableType;
unsigned long itableSize = 0;
void run(Module* module) override {
if (!module->features.hasGC()) {
return;
}
if (!getPassOptions().closedWorld) {
Fatal() << "--merge-j2cl-itables requires --closed-world";
}
collectVtableAndItableTypes(*module);
// Update the indices to access the functions in the vtables and update
// the construction of the vtable instances.
updateVtableFieldsAccesses(*module);
// And now we can transform the accesses to the itable fields into their
// corresponding vtable fields. Needs to be done after
// updateVtableFieldsAccesses.
rerouteItableAccess(*module);
// The type structures are updated last since types are used as keys in
// the maps used above.
updateTypes(*module);
// Since now vtables are initialized with `global.get` of the interface
// vtable instances, we need to reorder the globals.
PassRunner runner(module);
runner.add("reorder-globals-always");
runner.setIsNested(true);
runner.run();
}
// Collects all structs corresponding to Java classes, their vtables and
// their itables. This is very tied to the way j2cl emits these constructs.
void collectVtableAndItableTypes(Module& wasm) {
// 1. Collect all structs that correspond that a Java type.
for (auto [heapType, typeNameInfo] : wasm.typeNames) {
if (!heapType.isStruct()) {
continue;
}
auto type = heapType.getStruct();
if (typeNameInfo.fieldNames.empty() ||
!typeNameInfo.fieldNames[0].equals("vtable")) {
continue;
}
if (typeNameInfo.fieldNames.size() < 1 ||
!typeNameInfo.fieldNames[1].equals("itable")) {
continue;
}
auto vtabletype = type.fields[0].type.getHeapType();
auto itabletype = type.fields[1].type.getHeapType();
auto structItableSize = itabletype.getStruct().fields.size();
if (itableSize != 0 && itableSize != structItableSize) {
Fatal() << "--merge-j2cl-itables needs to be the first pass to run "
<< "on j2cl output. (found itables with different sizes)";
}
itableSize = structItableSize;
// Add a new StructInfo to the list by value so that its memory gets
// reclaimed automatically on exit.
structInfos.push_back(StructInfo{heapType, vtabletype, itabletype});
// Point to the StructInfo just added to the list to be able to look it
// up by its vtable and itable types.
structInfoByVtableType[vtabletype] = &structInfos.back();
structInfoByITableType[itabletype] = &structInfos.back();
}
// 2. Collect the globals for vtables and itables.
for (auto& g : wasm.globals) {
if (!g->type.isStruct()) {
continue;
}
if (structInfoByVtableType.count(g->type.getHeapType())) {
tableGlobalsByType[g->type.getHeapType()] = g.get();
} else if (structInfoByITableType.count(g->type.getHeapType())) {
tableGlobalsByType[g->type.getHeapType()] = g.get();
}
}
if (itableSize == 0) {
Fatal() << "--merge-j2cl-itables needs to be the first pass to run "
<< "on j2cl output. (no Java classes found)";
}
}
// Fix the indexes of `struct.get` for vtable fields, and prepend the
// initializers for the itable fields to `struct.new`.
// Note that there isn't any `struct.set` because the vtable fields are
// immutable.
void updateVtableFieldsAccesses(Module& wasm) {
struct Reindexer : public WalkerPass<PostWalker<Reindexer>> {
bool isFunctionParallel() override { return true; }
J2CLItableMerging& parent;
Reindexer(J2CLItableMerging& parent) : parent(parent) {}
std::unique_ptr<Pass> create() override {
return std::make_unique<Reindexer>(parent);
}
void visitStructGet(StructGet* curr) {
if (curr->ref->type == Type::unreachable) {
return;
}
if (!parent.structInfoByVtableType.count(
curr->ref->type.getHeapType())) {
return;
}
// This is a struct.get on the vtable.
// It is ok to just change the index since the field has moved but
// the type is the same.
curr->index += parent.itableSize;
}
void visitStructNew(StructNew* curr) {
if (curr->type == Type::unreachable) {
return;
}
auto it = parent.structInfoByVtableType.find(curr->type.getHeapType());
if (it == parent.structInfoByVtableType.end()) {
return;
}
// The struct.new is for a vtable type and structInfo has the
// information relating the struct types for the Java class, its vtable
// and its itable.
auto structInfo = it->second;
// Get the global that holds the corresponding itable instance.
auto* itableGlobal = parent.tableGlobalsByType[structInfo->itable];
StructNew* itableStructNew = nullptr;
if (itableGlobal && itableGlobal->init) {
if (itableGlobal->init->is<GlobalGet>()) {
// The global might get initialized with the shared empty itable,
// obtain the itable struct.new from the global.init.
auto* globalGet = itableGlobal->init->dynCast<GlobalGet>();
auto* global = getModule()->getGlobal(globalGet->name);
itableStructNew = global->init->dynCast<StructNew>();
} else {
// The global is initialized with a struct.new of the itable.
itableStructNew = itableGlobal->init->dynCast<StructNew>();
}
}
if (!itableStructNew) {
Fatal() << "--merge-j2cl-itables needs to be the first pass to run "
<< "on j2cl output. (itable initializer not found)";
}
auto& itableFieldInitializers = itableStructNew->operands;
// Add the initialization for the itable fields.
for (Index i = parent.itableSize; i > 0; i--) {
if (itableFieldInitializers.size() >= i) {
// The itable was initialized with a struct.new, copy the
// initialization values.
curr->operands.insertAt(
0,
ExpressionManipulator::copy(itableFieldInitializers[i - 1],
*getModule()));
} else {
// The itable was initialized with struct.new_default. So use
// null values to initialize the itable fields.
Builder builder(*getModule());
curr->operands.insertAt(
0,
builder.makeRefNull(itableStructNew->type.getHeapType()
.getStruct()
.fields[i - 1]
.type.getHeapType()));
}
}
}
};
Reindexer reindexer(*this);
reindexer.run(getPassRunner(), &wasm);
reindexer.runOnModuleCode(getPassRunner(), &wasm);
}
// Redirects all itable access by changing `struct.get` of the `itable` field
// to `struct.get` on the to `vtable` field.
void rerouteItableAccess(Module& wasm) {
struct Rerouter : public WalkerPass<PostWalker<Rerouter>> {
bool isFunctionParallel() override { return true; }
J2CLItableMerging& parent;
Rerouter(J2CLItableMerging& parent) : parent(parent) {}
std::unique_ptr<Pass> create() override {
return std::make_unique<Rerouter>(parent);
}
void visitStructGet(StructGet* curr) {
if (curr->ref->type == Type::unreachable) {
return;
}
if (!curr->type.isStruct() ||
!parent.structInfoByITableType.count(curr->type.getHeapType())) {
return;
}
// This is a struct.get that returns an itable type;
// Change to return the corresponding vtable type.
Builder builder(*getModule());
replaceCurrent(builder.makeStructGet(
0,
curr->ref,
MemoryOrder::Unordered,
parent.structInfoByITableType[curr->type.getHeapType()]
->javaClass.getStruct()
.fields[0]
.type));
}
};
Rerouter rerouter(*this);
rerouter.run(getPassRunner(), &wasm);
rerouter.runOnModuleCode(getPassRunner(), &wasm);
}
// Modify the struct definitions adding the itable fields to the vtable and
// preserving the vtable field names.
void updateTypes(Module& wasm) {
class TypeRewriter : public GlobalTypeRewriter {
J2CLItableMerging& parent;
public:
TypeRewriter(Module& wasm, J2CLItableMerging& parent)
: GlobalTypeRewriter(wasm), parent(parent) {}
void modifyStruct(HeapType oldStructType, Struct& struct_) override {
if (parent.structInfoByVtableType.count(oldStructType)) {
auto& newFields = struct_.fields;
auto structInfo = parent.structInfoByVtableType[oldStructType];
// Add the itable fields to the beginning of the vtable.
auto it = structInfo->itable.getStruct().fields.rbegin();
while (it != structInfo->itable.getStruct().fields.rend()) {
newFields.insert(newFields.begin(), *it++);
newFields[0].type = getTempType(newFields[0].type);
}
// Update field names as well. The Type Rewriter cannot do this for
// us, as it does not know which old fields map to which new ones
// (it just keeps the names in sequence).
auto& nameInfo = wasm.typeNames[oldStructType];
// Make a copy of the old ones before clearing them.
auto oldFieldNames = nameInfo.fieldNames;
// Clear the old names and write the new ones.
nameInfo.fieldNames.clear();
// Only need to preserve the field names for the vtable fields; the
// itable fields do not have names (in the original .wat file they
// are accessed by index).
for (Index i = 0; i < oldFieldNames.size(); i++) {
nameInfo.fieldNames[i + parent.itableSize] = oldFieldNames[i];
}
}
}
};
TypeRewriter(wasm, *this).update();
}
};
} // anonymous namespace
Pass* createJ2CLItableMergingPass() { return new J2CLItableMerging(); }
} // namespace wasm