diff options
Diffstat (limited to 'src/tools/wasm-ctor-eval.cpp')
-rw-r--r-- | src/tools/wasm-ctor-eval.cpp | 433 |
1 files changed, 376 insertions, 57 deletions
diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index 5e1bf6b22..a935475ff 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -25,6 +25,7 @@ #include <memory> #include "asmjs/shared-constants.h" +#include "ir/gc-type-utils.h" #include "ir/global-utils.h" #include "ir/import-utils.h" #include "ir/literal-utils.h" @@ -33,8 +34,10 @@ #include "pass.h" #include "support/colors.h" #include "support/file.h" +#include "support/insert_ordered.h" #include "support/small_set.h" #include "support/string.h" +#include "support/topological_sort.h" #include "tool-options.h" #include "wasm-builder.h" #include "wasm-interpreter.h" @@ -50,6 +53,15 @@ struct FailToEvalException { FailToEvalException(std::string why) : why(why) {} }; +// Check whether a field is both nullable and mutable. This is a useful +// property for breaking cycles of GC data, see below. +bool isNullableAndMutable(Expression* ref, Index fieldIndex) { + // Find the field for the given reference, and check its properties. + auto field = GCTypeUtils::getField(ref->type, fieldIndex); + assert(field); + return field->type.isNullable() && field->mutable_ == Mutable; +} + // The prefix for a recommendation, so it is aligned properly with the rest of // the output. #define RECOMMENDATION "\n recommendation: " @@ -470,9 +482,10 @@ private: // each time (things live before may no longer be). definingGlobals.clear(); - // When we start to apply the state there should be no previous state left - // over. - assert(seenDataStack.empty()); + // Clear any startup operations as well (which may apply to globals that + // become no longer live; we'll create new start operations as we need + // them). + clearStartBlock(); } void applyMemoryToModule() { @@ -510,8 +523,6 @@ private: // data in the interpreter's memory can then be serialized by simply emitting // a global.get of that defining global. void applyGlobalsToModule() { - Builder builder(*wasm); - if (!wasm->features.hasGC()) { // Without GC, we can simply serialize the globals in place as they are. for (const auto& [name, values] : instance->globals) { @@ -528,6 +539,7 @@ private: // and refer to it. Put another way, we place the existing globals back into // the module one at a time, adding their dependencies as we go. auto oldGlobals = std::move(wasm->globals); + // After clearing the globals vector, clear the map as well. wasm->updateMaps(); for (auto& oldGlobal : oldGlobals) { @@ -553,21 +565,245 @@ private: // Add the global back to the module. wasm->addGlobal(std::move(oldGlobal)); } + + // Finally, we need to fix up cycles. The serialization we just emitted + // ignores them, so we can end up with things like this: + // + // (global $a (struct.new $A (global.get $a))) + // + // That global refers to an object that should have a self-reference, and + // the serialization logic simply emits global.gets for all references, so + // we end up with a situation like this where a global.get refers to a + // global before it is valid to do so. To fix this up, we can reorder + // globals as needed, and break up cycles by writing a null in the initial + // struct.new in the global's definition, and later in the start function we + // can perform additional struct.sets that cause cycles to form. + // + // The existing algorithm here is rather simple: we find things that + // definitely force a certain order and sort according to them. Then in that + // order we break forward references with fixups as described above. This is + // not always the best, as there may be a more optimal order, and we may end + // up doing more fixups than are absolutely necessary, but this algorithm is + // simple and works in linear time (or nlogn including the sort). The + // general problem here is NP-hard (the maximum acyclic subgraph problem), + // but there are probably greedy algorithms we could consider if we need to + // do better. + + Builder builder(*wasm); + + // First, find what constraints we have on the ordering of the globals. We + // will build up a map of each global to the globals it must be after. + using MustBeAfter = InsertOrderedMap<Name, InsertOrderedSet<Name>>; + MustBeAfter mustBeAfter; + + for (auto& global : wasm->globals) { + if (!global->init) { + continue; + } + + struct InitScanner : PostWalker<InitScanner> { + // All the global.gets that we can't fix up by replacing the value with + // a null and adding a set in the start function. These will be hard + // constraints on our sorting (if we could fix things up with a null + + // set then we would not need to reorder). + InsertOrderedSet<GlobalGet*> unfixableGets; + + void visitGlobalGet(GlobalGet* curr) { + // Assume this is unfixable, unless we reach the parent and see that + // it is. + unfixableGets.insert(curr); + } + + // Checks if a child is a global.get that we need to handle, and if we + // can fix it if so. The index is the position of the child in the + // parent (which is 0 for all array children, as their position does not + // matter, they all have the same field info). + void handleChild(Expression* child, + Expression* parent, + Index fieldIndex = 0) { + if (!child) { + return; + } + + if (auto* get = child->dynCast<GlobalGet>()) { + if (isNullableAndMutable(parent, fieldIndex)) { + // We can replace the child with a null, and set the value later + // (in the start function), so this is not a constraint on our + // sorting - we'll just fix it up later, and the order won't be + // an issue. + unfixableGets.erase(get); + } + } + } + + void visitStructNew(StructNew* curr) { + Index i = 0; + for (auto* child : curr->operands) { + handleChild(child, curr, i++); + } + } + void visitArrayNew(ArrayNew* curr) { handleChild(curr->init, curr); } + void visitArrayNewFixed(ArrayNewFixed* curr) { + for (auto* child : curr->values) { + handleChild(child, curr); + } + } + }; + + InitScanner scanner; + scanner.walk(global->init); + + // Any global.gets that cannot be fixed up are constraints. + for (auto* get : scanner.unfixableGets) { + mustBeAfter[global->name].insert(get->name); + } + } + + if (!mustBeAfter.empty()) { + // We found constraints that require reordering, so do so. + struct MustBeAfterSort : TopologicalSort<Name, MustBeAfterSort> { + MustBeAfter& mustBeAfter; + + MustBeAfterSort(MustBeAfter& mustBeAfter) : mustBeAfter(mustBeAfter) { + for (auto& [global, _] : mustBeAfter) { + push(global); + } + } + + void pushPredecessors(Name global) { + auto iter = mustBeAfter.find(global); + if (iter != mustBeAfter.end()) { + for (auto other : iter->second) { + push(other); + } + } + } + }; + + auto oldGlobals = std::move(wasm->globals); + // After clearing the globals vector, clear the map as well. + wasm->updateMaps(); + + std::unordered_map<Name, Index> globalIndexes; + for (Index i = 0; i < oldGlobals.size(); i++) { + globalIndexes[oldGlobals[i]->name] = i; + } + // Add the globals that had an important ordering, in the right order. + for (auto global : MustBeAfterSort(mustBeAfter)) { + wasm->addGlobal(std::move(oldGlobals[globalIndexes[global]])); + } + // Add all other globals after them. + for (auto& global : oldGlobals) { + if (global) { + wasm->addGlobal(std::move(global)); + } + } + } + + // After sorting (*), perform the fixups that we need, that is, replace the + // relevant fields in cycles with a null and prepare a set in the start + // function. + // + // We'll track the set of readable globals as we go (which are the globals + // we've seen already, and fully finished processing). + // + // (*) Note that we may need these fixups even if we didn't need to do any + // sorting. There may be a single global with a cycle in it, for + // example. + std::unordered_set<Name> readableGlobals; + + for (auto& global : wasm->globals) { + if (!global->init) { + continue; + } + + struct InitFixer : PostWalker<InitFixer> { + CtorEvalExternalInterface& evaller; + std::unique_ptr<Global>& global; + std::unordered_set<Name>& readableGlobals; + + InitFixer(CtorEvalExternalInterface& evaller, + std::unique_ptr<Global>& global, + std::unordered_set<Name>& readableGlobals) + : evaller(evaller), global(global), readableGlobals(readableGlobals) { + } + + // Handles a child by fixing things up if needed. Returns true if we + // did in fact fix things up. + bool handleChild(Expression*& child, + Expression* parent, + Index fieldIndex = 0) { + if (!child) { + return false; + } + + if (auto* get = child->dynCast<GlobalGet>()) { + if (!readableGlobals.count(get->name)) { + // This get cannot be read - it is a global that appears after + // us - and so we must fix it up, using the method mentioned + // before (setting it to null now, and later in the start + // function writing to it). + assert(isNullableAndMutable(parent, fieldIndex)); + evaller.addStartFixup( + {global->name, global->type}, fieldIndex, get); + child = + Builder(*getModule()).makeRefNull(get->type.getHeapType()); + return true; + } + } + + return false; + } + + // This code will need to be updated for all new GC-creating + // instructions that we use when serializing GC data, that is, things we + // put in defining globals. (All other instructions, even constant ones + // in globals, will simply end up referring to them using a global.get, + // but will not be referred to. That is, cycles will only appear in + // defining globals.) + + void visitStructNew(StructNew* curr) { + Index i = 0; + for (auto*& child : curr->operands) { + handleChild(child, curr, i++); + } + } + void visitArrayNew(ArrayNew* curr) { + if (handleChild(curr->init, curr)) { + // Handling array.new is tricky as the number of items may be + // unknown at compile time, so we'd need to loop at runtime. But, + // in practice we emit an array.new_fixed anyhow, so this should + // not be needed for now. + WASM_UNREACHABLE("TODO: ArrayNew in ctor-eval cycles"); + } + } + void visitArrayNewFixed(ArrayNewFixed* curr) { + Index i = 0; + for (auto*& child : curr->values) { + handleChild(child, curr, i++); + } + } + }; + + InitFixer fixer(*this, global, readableGlobals); + fixer.setModule(wasm); + fixer.walk(global->init); + + // Only after we've fully processed this global is it ok to be read from + // by later globals. + readableGlobals.insert(global->name); + } } public: // Maps each GC data in the interpreter to its defining global: the global in // which it is created, and then all other users of it can just global.get - // that. - std::unordered_map<GCData*, Name> definingGlobals; - - // The data we have seen so far on the stack. This is used to guard against - // infinite recursion, which would otherwise happen if there is a cycle among - // the live objects, which we don't handle yet. - // - // Pick a constant of 2 here to handle the common case of an object with a - // reference to another object that is already in a defining global. - SmallSet<GCData*, 2> seenDataStack; + // that. For each such global we track its name and type. + struct DefiningGlobalInfo { + Name name; + Type type; + }; + std::unordered_map<GCData*, DefiningGlobalInfo> definingGlobals; // If |possibleDefiningGlobal| is provided, it is the name of a global that we // are in the init expression of, and which can be reused as defining global, @@ -600,27 +836,40 @@ public: auto* data = value.getGCData().get(); assert(data); - // There was actual GC data allocated here. auto type = value.type; - auto& definingGlobal = definingGlobals[data]; - if (!definingGlobal.is()) { - // This is the first usage of this allocation. Generate a struct.new / + Name definingGlobalName; + + if (auto it = definingGlobals.find(data); it != definingGlobals.end()) { + // Use the existing defining global. + definingGlobalName = it->second.name; + } else { + // This is the first usage of this data. Generate a struct.new / // array.new for it. auto& values = value.getGCData()->values; std::vector<Expression*> args; // The initial values for this allocation may themselves be GC - // allocations. Recurse and add globals as necessary. - // TODO: Handle cycles. That will require code in the start function. For - // now, just error if we detect an infinite recursion. - if (seenDataStack.count(data)) { - Fatal() << "Cycle in live GC data, which we cannot serialize yet."; + // allocations. Recurse and add globals as necessary. First, pick the + // global name (note that we must do so first, as we may need to read from + // definingGlobals to find where this global will be, in the case of a + // cycle; see below). + if (possibleDefiningGlobal.is()) { + // No need to allocate a new global, as we are in the definition of + // one, which will be the defining global. + definingGlobals[data] = + DefiningGlobalInfo{possibleDefiningGlobal, type}; + definingGlobalName = possibleDefiningGlobal; + } else { + // Allocate a new defining global. + definingGlobalName = + Names::getValidNameGivenExisting("ctor-eval$global", usedGlobalNames); + usedGlobalNames.insert(definingGlobalName); + definingGlobals[data] = DefiningGlobalInfo{definingGlobalName, type}; } - seenDataStack.insert(data); + for (auto& value : values) { args.push_back(getSerialization(value)); } - seenDataStack.erase(data); Expression* init; auto heapType = type.getHeapType(); @@ -634,25 +883,23 @@ public: } if (possibleDefiningGlobal.is()) { - // No need to allocate a new global, as we are in the definition of - // one. Just return the initialization expression, which will be - // placed in that global's |init| field, and first note this as the - // defining global. - definingGlobal = possibleDefiningGlobal; + // We didn't need to allocate a new global, as we are in the definition + // of one, so just return the initialization expression, which will be + // placed in that global's |init| field. return init; } - // Allocate a new defining global. - auto name = - Names::getValidNameGivenExisting("ctor-eval$global", usedGlobalNames); - usedGlobalNames.insert(name); - wasm->addGlobal(builder.makeGlobal(name, type, init, Builder::Immutable)); - definingGlobal = name; + // There is no existing defining global, so we must allocate a new one. + // + // We set the global's init to null temporarily, and we'll fix it up + // later down after we create the init expression. + wasm->addGlobal( + builder.makeGlobal(definingGlobalName, type, init, Builder::Immutable)); } // Refer to this GC allocation by reading from the global that is // designated to contain it. - Expression* ret = builder.makeGlobalGet(definingGlobal, value.type); + Expression* ret = builder.makeGlobalGet(definingGlobalName, value.type); if (original != value) { // The original is externalized. assert(original.type.getHeapType() == HeapType::ext); @@ -675,6 +922,70 @@ public: assert(values.size() == 1); return getSerialization(values[0], possibleDefiningGlobal); } + + // This is called when we hit a cycle in setting up defining globals. For + // example, if the data we want to emit is + // + // global globalA = new A{ field = &A }; // A has a reference to itself + // + // then we'll emit + // + // global globalA = new A{ field = null }; + // + // and put this in the start function: + // + // globalA.field = globalA; + // + // The parameters here are a global and a field index to that global, and the + // global we want to assign to it, that is, our goal is to have + // + // global[index] = valueGlobal + // + // run during the start function. + void addStartFixup(DefiningGlobalInfo global, Index index, GlobalGet* value) { + if (!startBlock) { + createStartBlock(); + } + + Builder builder(*wasm); + auto* getGlobal = builder.makeGlobalGet(global.name, global.type); + + Expression* set; + if (global.type.isStruct()) { + set = builder.makeStructSet(index, getGlobal, value); + } else { + set = builder.makeArraySet( + getGlobal, builder.makeConst(int32_t(index)), value); + } + + (*startBlock)->list.push_back(set); + } + + // A block in the start function where we put the operations we need to occur + // during startup. + std::optional<Block*> startBlock; + + void createStartBlock() { + Builder builder(*wasm); + startBlock = builder.makeBlock(); + if (wasm->start.is()) { + // Put our block before any user start code. + auto* existingStart = wasm->getFunction(wasm->start); + existingStart->body = + builder.makeSequence(*startBlock, existingStart->body); + } else { + // Make a new start function. + wasm->start = Names::getValidFunctionName(*wasm, "start"); + wasm->addFunction(builder.makeFunction( + wasm->start, Signature{Type::none, Type::none}, {}, *startBlock)); + } + } + + void clearStartBlock() { + if (startBlock) { + (*startBlock)->list.clear(); + } + } }; // Whether to emit informative logging to stdout about the eval process. @@ -754,14 +1065,18 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, // in a single function scope for all the executions. EvallingModuleRunner::FunctionScope scope(func, params, instance); - // After we successfully eval a line we will apply the changes here. This is - // the same idea as applyToModule() - we must only do it after an entire - // atomic "chunk" has been processed, we do not want partial updates from - // an item in the block that we only partially evalled. - std::vector<Literals> appliedLocals; + // After we successfully eval a line we will store the operations to set up + // the locals here. That is, we need to save the local state in the + // function, which we do by setting up at the entry. We update this list of + // local.sets at the same time as applyToModule() - we must only do it after + // an entire atomic "chunk" has been processed succesfully, we do not want + // partial updates from an item in the block that we only partially evalled. + std::vector<Expression*> localSets; + Builder builder(wasm); Literals results; Index successes = 0; + for (auto* curr : block->list) { Flow flow; try { @@ -780,9 +1095,25 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, break; } - // So far so good! Apply the results. + // So far so good! Serialize the values of locals, and apply to the + // module. Note that we must serialize the locals now as doing so may + // cause changes that must be applied to the module (e.g. GC data may + // cause globals to be added). And we must apply to the module now, and + // not later, as we must do so right after a successfull partial eval + // (after any failure to eval, the global state is no long valid to be + // applied to the module, as incomplete changes may have occurred). + // + // Note that we make no effort to optimize locals: we just write out all + // of them, and leave it to the optimizer to remove redundant or + // unnecessary operations. We just recompute the entire local + // serialization sets from scratch each time here, for all locals. + localSets.clear(); + for (Index i = 0; i < func->getNumLocals(); i++) { + auto value = scope.locals[i]; + localSets.push_back( + builder.makeLocalSet(i, interface.getSerialization(value))); + } interface.applyToModule(); - appliedLocals = scope.locals; successes++; // Note the values here, if any. If we are exiting the function now then @@ -818,23 +1149,11 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, wasm.getExport(exportName)->value = copyName; // Remove the items we've evalled. - Builder builder(wasm); auto* copyBlock = copyFunc->body->cast<Block>(); for (Index i = 0; i < successes; i++) { copyBlock->list[i] = builder.makeNop(); } - // Write out the values of locals, that is the local state after evalling - // the things we've just nopped. For simplicity we just write out all of - // locals, and leave it to the optimizer to remove redundant or - // unnecessary operations. - std::vector<Expression*> localSets; - for (Index i = 0; i < copyFunc->getNumLocals(); i++) { - auto value = appliedLocals[i]; - localSets.push_back( - builder.makeLocalSet(i, interface.getSerialization(value))); - } - // Put the local sets at the front of the block. We know there must be a // nop in that position (since we've evalled at least one item in the // block, and replaced it with a nop), so we can overwrite it. |