Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 83 additions & 80 deletions src/ir/module-splitting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
// export the primary function if it is not already exported and import it
// into each secondary module using it.
//
// 8. For each secondary module, create new active table segments in the
// 8. For each secondary module, create new dispatch table segments in the
// module that will replace all the placeholder function references in the
// table with references to their corresponding secondary functions upon
// instantiation.
Expand All @@ -64,7 +64,7 @@
//
// 2. It assumes that either all table segment offsets are constants or there
// is exactly one segment that may have a non-constant offset. It also
// assumes that all segments are active segments.
// assumes that all segments are dispatch segments.
//
// 3. It assumes that if exact function references are required for validity
// (because they are stored in a local with an exact function type, for
Expand Down Expand Up @@ -114,11 +114,11 @@ struct TableSlotManager {
};
Module& module;
const std::vector<std::unique_ptr<Module>>& secondaries;
Table* activeTable = nullptr;
ElementSegment* activeSegment = nullptr;
Slot activeBase;
Table* dispatchTable = nullptr;
ElementSegment* dispatchSegment = nullptr;
Slot dispatchBase;
std::map<Name, Slot> funcIndices;
std::vector<ElementSegment*> activeTableSegments;
std::vector<ElementSegment*> dispatchTableSegments;

TableSlotManager(Module& module,
const std::vector<std::unique_ptr<Module>>& secondaries);
Expand Down Expand Up @@ -187,43 +187,43 @@ TableSlotManager::TableSlotManager(
return;
}

activeTable = it->get();
dispatchTable = it->get();
ModuleUtils::iterTableSegments(
module, activeTable->name, [&](ElementSegment* segment) {
activeTableSegments.push_back(segment);
module, dispatchTable->name, [&](ElementSegment* segment) {
dispatchTableSegments.push_back(segment);
});

if (activeTableSegments.empty()) {
// There are no active segments, so we will lazily create one and start
if (dispatchTableSegments.empty()) {
// There are no dispatch segments, so we will lazily create one and start
// filling it at index 0.
activeBase = {activeTable->name, Name(), 0};
} else if (activeTableSegments.size() == 1 &&
activeTableSegments[0]->type == funcref &&
!activeTableSegments[0]->offset->is<Const>()) {
dispatchBase = {dispatchTable->name, Name(), 0};
} else if (dispatchTableSegments.size() == 1 &&
dispatchTableSegments[0]->type == funcref &&
!dispatchTableSegments[0]->offset->is<Const>()) {
// If there is exactly one table segment and that segment has a non-constant
// offset, append new items to the end of that segment. In all other cases,
// append new items at constant offsets after all existing items at constant
// offsets.
assert(activeTableSegments[0]->offset->is<GlobalGet>() &&
assert(dispatchTableSegments[0]->offset->is<GlobalGet>() &&
"Unexpected initializer instruction");
activeSegment = activeTableSegments[0];
activeBase = {activeTable->name,
activeTableSegments[0]->offset->cast<GlobalGet>()->name,
0};
dispatchSegment = dispatchTableSegments[0];
dispatchBase = {dispatchTable->name,
dispatchTableSegments[0]->offset->cast<GlobalGet>()->name,
0};
} else {
// Finds the segment with the highest occupied table slot so that new items
// can be inserted contiguously at the end of it without accidentally
// overwriting any other items. TODO: be more clever about filling gaps in
// the table, if that is ever useful.
Index maxIndex = 0;
for (auto& segment : activeTableSegments) {
for (auto& segment : dispatchTableSegments) {
assert(segment->offset->is<Const>() &&
"Unexpected non-const segment offset with multiple segments");
Index segmentBase = segment->offset->cast<Const>()->value.getInteger();
if (segmentBase + segment->data.size() >= maxIndex) {
maxIndex = segmentBase + segment->data.size();
activeSegment = segment;
activeBase = {activeTable->name, Name(), segmentBase};
dispatchSegment = segment;
dispatchBase = {dispatchTable->name, Name(), segmentBase};
}
}
}
Expand All @@ -238,7 +238,7 @@ TableSlotManager::TableSlotManager(
}

Table* TableSlotManager::makeTable() {
// Because the active table will be imported in secondary modules, its name
// Because the dispatch table will be imported in secondary modules, its name
// should not collide with any existing tables in primary and secondary
// modules.
std::unordered_set<Name> secondaryTableNames;
Expand All @@ -262,10 +262,10 @@ Table* TableSlotManager::makeTable() {
ElementSegment* TableSlotManager::makeElementSegment() {
Builder builder(module);
Expression* offset =
builder.makeConst(Literal::makeFromInt32(0, activeTable->addressType));
builder.makeConst(Literal::makeFromInt32(0, dispatchTable->addressType));
return module.addElementSegment(Builder::makeElementSegment(
Names::getValidElementSegmentName(module, Name::fromInt(0)),
activeTable->name,
dispatchTable->name,
offset));
}

Expand All @@ -276,40 +276,40 @@ TableSlotManager::Slot TableSlotManager::getSlot(Name func, HeapType type) {
}

// If there are no segments yet, allocate one.
if (activeSegment == nullptr) {
if (activeTable == nullptr) {
activeTable = makeTable();
activeBase = {activeTable->name, Name(), 0};
if (dispatchSegment == nullptr) {
if (dispatchTable == nullptr) {
dispatchTable = makeTable();
dispatchBase = {dispatchTable->name, Name(), 0};
}

// None of the existing segments should refer to the active table
// None of the existing segments should refer to the dispatch table
assert(std::all_of(module.elementSegments.begin(),
module.elementSegments.end(),
[&](std::unique_ptr<ElementSegment>& segment) {
return segment->table != activeTable->name;
return segment->table != dispatchTable->name;
}));

activeSegment = makeElementSegment();
dispatchSegment = makeElementSegment();
}

Slot newSlot = {activeBase.tableName,
activeBase.global,
activeBase.index + Index(activeSegment->data.size())};
Slot newSlot = {dispatchBase.tableName,
dispatchBase.global,
dispatchBase.index + Index(dispatchSegment->data.size())};

Builder builder(module);
auto funcType = Type(type, NonNullable, Inexact);
activeSegment->data.push_back(builder.makeRefFunc(func, funcType));
dispatchSegment->data.push_back(builder.makeRefFunc(func, funcType));

addSlot(func, newSlot);
if (activeTable->initial <= newSlot.index) {
activeTable->initial = newSlot.index + 1;
// TODO: handle the active table not being the dylink table (#3823)
if (dispatchTable->initial <= newSlot.index) {
dispatchTable->initial = newSlot.index + 1;
// TODO: handle the dispatch table not being the dylink table (#3823)
if (module.dylinkSection) {
module.dylinkSection->tableSize = activeTable->initial;
module.dylinkSection->tableSize = dispatchTable->initial;
}
}
if (activeTable->max <= newSlot.index) {
activeTable->max = newSlot.index + 1;
if (dispatchTable->max <= newSlot.index) {
dispatchTable->max = newSlot.index + 1;
}
return newSlot;
}
Expand Down Expand Up @@ -343,7 +343,7 @@ struct ModuleSplitter {
// Map from original secondary function name to its trampoline
std::unordered_map<Name, Name> trampolineMap;

void shareActiveTable(Module* secondary);
void shareDispatchTable(Module* secondary);

// Initialization helpers
static std::unique_ptr<Module> initSecondary(const Module& primary);
Expand Down Expand Up @@ -593,24 +593,26 @@ Name ModuleSplitter::getTrampoline(Name funcName) {
return trampoline;
}

void ModuleSplitter::shareActiveTable(Module* secondary) {
assert(tableManager.activeTable);
void ModuleSplitter::shareDispatchTable(Module* secondary) {
assert(tableManager.dispatchTable);
auto secondaryTable =
secondary->getTableOrNull(tableManager.activeTable->name);
secondary->getTableOrNull(tableManager.dispatchTable->name);
if (secondaryTable) {
// In case it's already in the secondary module, sync the initial/max
secondaryTable->initial = tableManager.activeTable->initial;
secondaryTable->max = tableManager.activeTable->max;
secondaryTable->initial = tableManager.dispatchTable->initial;
secondaryTable->max = tableManager.dispatchTable->max;
} else {
secondaryTable =
ModuleUtils::copyTable(tableManager.activeTable, *secondary);
makeImportExport(
*tableManager.activeTable, *secondaryTable, "table", ExternalKind::Table);
ModuleUtils::copyTable(tableManager.dispatchTable, *secondary);
makeImportExport(*tableManager.dispatchTable,
*secondaryTable,
"table",
ExternalKind::Table);
}
if (tableManager.activeBase.global) {
auto* primaryGlobal = primary.getGlobal(tableManager.activeBase.global);
if (tableManager.dispatchBase.global) {
auto* primaryGlobal = primary.getGlobal(tableManager.dispatchBase.global);
auto* secondaryGlobal =
secondary->getGlobalOrNull(tableManager.activeBase.global);
secondary->getGlobalOrNull(tableManager.dispatchBase.global);
if (!secondaryGlobal) {
secondaryGlobal = ModuleUtils::copyGlobal(primaryGlobal, *secondary);
makeImportExport(
Expand Down Expand Up @@ -803,13 +805,13 @@ void ModuleSplitter::shareImportableItems() {
secondaryUsed.push_back(getUsedNames(*secondaryPtr));
}

// We need to assume the active table and its base global are used in the
// We need to assume the dispatch table and its base global are used in the
// primary module, because we will create segments there later.
if (tableManager.activeTable) {
primaryUsed.tables.insert(tableManager.activeTable->name);
if (tableManager.dispatchTable) {
primaryUsed.tables.insert(tableManager.dispatchTable->name);
}
if (tableManager.activeBase.global) {
primaryUsed.globals.insert(tableManager.activeBase.global);
if (tableManager.dispatchBase.global) {
primaryUsed.globals.insert(tableManager.dispatchBase.global);
}

// If custom-descirptors is enabled, global initializers can trap. Trapping
Expand Down Expand Up @@ -979,12 +981,13 @@ void ModuleSplitter::indirectReferencesToSecondaryFunctions() {
gatherer.walkModule(secondaryPtr.get());
}

// Ignore references to secondary functions that occur in the active segments
// that will contain the imported placeholders. Indirect calls to table slots
// initialized by those segments will already go to the right place once the
// secondary module has been loaded and the table has been patched.
// Ignore references to secondary functions that occur in the dispatch
// segments that will contain the imported placeholders. Indirect calls to
// table slots initialized by those segments will already go to the right
// place once the secondary module has been loaded and the table has been
// patched.
std::unordered_set<RefFunc*> ignore;
for (auto* segment : tableManager.activeTableSegments) {
for (auto* segment : tableManager.dispatchTableSegments) {
for (auto* expr : segment->data) {
if (auto* ref = expr->dynCast<RefFunc>()) {
ignore.insert(ref);
Expand Down Expand Up @@ -1023,7 +1026,7 @@ void ModuleSplitter::indirectCallsToSecondaryFunctions() {
// corresponding table indices instead.
struct CallIndirector : public PostWalker<CallIndirector> {
ModuleSplitter& parent;
std::unordered_set<Module*> activeTableUsingSecondaries;
std::unordered_set<Module*> dispatchTableUsingSecondaries;
CallIndirector(ModuleSplitter& parent) : parent(parent) {}
void visitCall(Call* curr) {
// Return if the call's target is not in one of the secondary module.
Expand Down Expand Up @@ -1051,10 +1054,10 @@ void ModuleSplitter::indirectCallsToSecondaryFunctions() {
func->type.getHeapType(),
curr->isReturn));

// Share the active table with the current module (caller). We share the
// active table with with calleeModule later in setupTablePathing.
// Share the dispatch table with the current module (caller). We share the
// dispatch table with with calleeModule later in setupTablePathing.
if (currModule != &parent.primary) {
activeTableUsingSecondaries.insert(currModule);
dispatchTableUsingSecondaries.insert(currModule);
}
}
};
Expand All @@ -1064,8 +1067,8 @@ void ModuleSplitter::indirectCallsToSecondaryFunctions() {
callIndirector.walkModule(secondaryPtr.get());
}

for (auto* secondary : callIndirector.activeTableUsingSecondaries) {
shareActiveTable(secondary);
for (auto* secondary : callIndirector.dispatchTableUsingSecondaries) {
shareDispatchTable(secondary);
}
}

Expand Down Expand Up @@ -1116,7 +1119,7 @@ void ModuleSplitter::exportImportCalledPrimaryFunctions() {
}

void ModuleSplitter::setupTablePatching() {
if (!tableManager.activeTable) {
if (!tableManager.dispatchTable) {
return;
}

Expand All @@ -1135,7 +1138,7 @@ void ModuleSplitter::setupTablePatching() {
if (!allSecondaryFuncs.contains(ref->func)) {
return;
}
assert(table == tableManager.activeTable->name);
assert(table == tableManager.dispatchTable->name);

placeholderMap[table][index] = ref->func;
Index secondaryIndex = funcToSecondaryIndex.at(ref->func);
Expand All @@ -1159,7 +1162,7 @@ void ModuleSplitter::setupTablePatching() {

} else { // !config.usePlaceholders
if (primary.features.hasReferenceTypes()) {
// TODO: This can create active element segments with lots of nulls.
// TODO: This can create dispatch element segments with lots of nulls.
// We should optimize them like we do data segments with zeros.
elem = Builder(primary).makeRefNull(HeapType::nofunc);
return;
Expand All @@ -1184,11 +1187,11 @@ void ModuleSplitter::setupTablePatching() {

for (auto& [secondaryPtr, replacedElems] : moduleToReplacedElems) {
Module& secondary = *secondaryPtr;
shareActiveTable(&secondary);
auto* secondaryTable = secondary.getTable(tableManager.activeTable->name);
shareDispatchTable(&secondary);
auto* secondaryTable = secondary.getTable(tableManager.dispatchTable->name);

if (tableManager.activeBase.global) {
assert(tableManager.activeTableSegments.size() == 1 &&
if (tableManager.dispatchBase.global) {
assert(tableManager.dispatchTableSegments.size() == 1 &&
"Unexpected number of segments with non-const base");
assert(secondary.tables.size() == 1 && secondary.elementSegments.empty());
// Since addition is not currently allowed in initializer expressions, we
Expand All @@ -1199,7 +1202,7 @@ void ModuleSplitter::setupTablePatching() {
// to be imported into the second module. TODO: use better strategies
// here, such as using ref.func in the start function or standardizing
// addition in initializer expressions.
ElementSegment* primarySeg = tableManager.activeTableSegments.front();
ElementSegment* primarySeg = tableManager.dispatchTableSegments.front();
std::vector<Expression*> secondaryElems;
secondaryElems.reserve(primarySeg->data.size());

Expand Down Expand Up @@ -1233,7 +1236,7 @@ void ModuleSplitter::setupTablePatching() {
return;
}

// Create active table segments in the secondary module to patch in the
// Create dispatch table segments in the secondary module to patch in the
// original functions when it is instantiated.
Index currBase = replacedElems.begin()->first;
std::vector<Expression*> currData;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
;; We need to disable reference-types to reuse the existing table as the active
;; table
;; We need to disable reference-types to reuse the existing table as the
;; dispatch table
;; RUN: wasm-split %s --disable-reference-types --split-funcs=split -g -o1 %t.1.wasm -o2 %t.2.wasm
;; RUN: wasm-dis %t.1.wasm | filecheck %s --check-prefix PRIMARY
;; RUN: wasm-dis %t.2.wasm | filecheck %s --check-prefix SECONDARY

;; This tests the case when an existing table is used as the active table, and
;; the active table and its base global already has existing uses in the
;; This tests the case when an existing table is used as the dispatch table, and
;; the dispatch table and its base global already has existing uses in the
;; secondary module.

(module
Expand Down
4 changes: 2 additions & 2 deletions test/lit/wasm-split/multi-split2.wast
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
;; RUN: wasm-dis %t2.wasm | filecheck %s --check-prefix=MOD2
;; RUN: wasm-dis %t3.wasm | filecheck %s --check-prefix=MOD3

;; A regresion test for the case the active table was not correctly shared with
;; MOD1. Because func $A is not called by any function, MOD1 is a secondary
;; A regresion test for the case the dispatch table was not correctly shared
;; with MOD1. Because func $A is not called by any function, MOD1 is a secondary
;; module who only acts as a caller.

(module
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.

;; Test that splitting succeeds even if there are no active segments for the function table.
;; Test that splitting succeeds even if there are no dispatch segments for the
;; function table.

;; RUN: wasm-split %s --split-funcs=foo -g -o1 %t.1.wasm -o2 %t.2.wasm
;; RUN: wasm-dis %t.1.wasm | filecheck %s --check-prefix PRIMARY
Expand Down
Loading
Loading