summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/tools/wasm-ctor-eval.cpp433
-rw-r--r--src/wasm/literal.cpp1
-rw-r--r--test/ctor-eval/gc-2.wast.out10
-rw-r--r--test/ctor-eval/gc.wast.out10
-rw-r--r--test/lit/ctor-eval/gc-cycle-multi.wast144
-rw-r--r--test/lit/ctor-eval/gc-cycle.wast1297
-rw-r--r--test/lit/ctor-eval/partial-global.wat39
7 files changed, 1867 insertions, 67 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.
diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp
index 230eeae66..53740c773 100644
--- a/src/wasm/literal.cpp
+++ b/src/wasm/literal.cpp
@@ -618,6 +618,7 @@ std::ostream& operator<<(std::ostream& o, Literal literal) {
assert(literal.isData());
auto data = literal.getGCData();
assert(data);
+ // TODO: infinite recursion is possible here, if the data is cyclic
o << "[ref " << data->type << ' ' << data->values << ']';
}
}
diff --git a/test/ctor-eval/gc-2.wast.out b/test/ctor-eval/gc-2.wast.out
index 5d91ca7d3..370faec16 100644
--- a/test/ctor-eval/gc-2.wast.out
+++ b/test/ctor-eval/gc-2.wast.out
@@ -1,15 +1,15 @@
(module
(type $struct (struct (field i32)))
(type $none_=>_i32 (func (result i32)))
- (global $ctor-eval$global (ref $struct) (struct.new $struct
- (i32.const 1337)
- ))
- (global $global1 (ref any) (global.get $ctor-eval$global))
(global $ctor-eval$global_4 (ref $struct) (struct.new $struct
(i32.const 9999)
))
- (global $global2 (mut (ref null $struct)) (global.get $ctor-eval$global_4))
(global $global3 (ref $struct) (global.get $ctor-eval$global_4))
+ (global $global2 (mut (ref null $struct)) (global.get $ctor-eval$global_4))
+ (global $ctor-eval$global (ref $struct) (struct.new $struct
+ (i32.const 1337)
+ ))
+ (global $global1 (ref any) (global.get $ctor-eval$global))
(export "keepalive" (func $1))
(func $1 (type $none_=>_i32) (result i32)
(select
diff --git a/test/ctor-eval/gc.wast.out b/test/ctor-eval/gc.wast.out
index 2995f84ba..3d99199be 100644
--- a/test/ctor-eval/gc.wast.out
+++ b/test/ctor-eval/gc.wast.out
@@ -4,14 +4,14 @@
(type $none_=>_i32 (func (result i32)))
(type $none_=>_none (func))
(import "import" "import" (func $import (param anyref)))
+ (global $ctor-eval$global_5 (ref $struct) (struct.new $struct
+ (i32.const 42)
+ ))
+ (global $global2 (mut (ref null $struct)) (global.get $ctor-eval$global_5))
(global $global1 (ref $struct) (struct.new $struct
(i32.const 1337)
))
(global $ctor-eval$global_4 (ref $struct) (struct.new $struct
- (i32.const 42)
- ))
- (global $global2 (mut (ref null $struct)) (global.get $ctor-eval$global_4))
- (global $ctor-eval$global_5 (ref $struct) (struct.new $struct
(i32.const 99)
))
(export "test1" (func $0_3))
@@ -29,7 +29,7 @@
(func $0_3 (type $none_=>_none)
(local $0 (ref $struct))
(local.set $0
- (global.get $ctor-eval$global_5)
+ (global.get $ctor-eval$global_4)
)
(call $import
(ref.null none)
diff --git a/test/lit/ctor-eval/gc-cycle-multi.wast b/test/lit/ctor-eval/gc-cycle-multi.wast
new file mode 100644
index 000000000..62a888dbd
--- /dev/null
+++ b/test/lit/ctor-eval/gc-cycle-multi.wast
@@ -0,0 +1,144 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+;; RUN: foreach %s %t wasm-ctor-eval --ctors=test1,test2,test3 --kept-exports=test1,test2,test3 --quiet -all -S -o - | filecheck %s
+
+;; Similar to gc-cycle.wast, but now there are three exported ctors that we
+;; call.
+
+(module
+ ;; Multiple independent cycles.
+ ;; CHECK: (type $A (struct (field (mut (ref null $A))) (field i32)))
+ (type $A (struct (field (mut (ref null $A))) (field i32)))
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (global $ctor-eval$global_8 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 30)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $c (mut (ref null $A)) (global.get $ctor-eval$global_8))
+
+ ;; CHECK: (global $ctor-eval$global_7 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 10)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_7))
+ (global $a (mut (ref null $A)) (ref.null $A))
+ ;; CHECK: (global $ctor-eval$global_6 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 20)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $b (mut (ref null $A)) (global.get $ctor-eval$global_6))
+ (global $b (mut (ref null $A)) (ref.null $A))
+ (global $c (mut (ref null $A)) (ref.null $A))
+
+ (func $makeCycle (param $i i32) (result (ref $A))
+ (local $x (ref $A))
+ (local.set $x
+ (struct.new $A
+ (ref.null $A)
+ (local.get $i)
+ )
+ )
+ (struct.set $A 0
+ (local.get $x)
+ (local.get $x)
+ )
+ (local.get $x)
+ )
+
+ (func $test1 (export "test1")
+ (global.set $a
+ (call $makeCycle
+ (i32.const 10)
+ )
+ )
+ )
+
+ (func $test2 (export "test2")
+ (global.set $b
+ (call $makeCycle
+ (i32.const 20)
+ )
+ )
+ )
+
+ (func $test3 (export "test3")
+ (global.set $c
+ (call $makeCycle
+ (i32.const 30)
+ )
+ )
+ )
+
+ ;; CHECK: (export "test1" (func $test1_6))
+
+ ;; CHECK: (export "test2" (func $test2_7))
+
+ ;; CHECK: (export "test3" (func $test3_8))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+
+ ;; CHECK: (func $keepalive (type $none_=>_i32) (result i32)
+ ;; CHECK-NEXT: (i32.add
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (i32.add
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $b)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $c)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result i32)
+ (i32.add
+ (struct.get $A 1
+ (global.get $a)
+ )
+ (i32.add
+ (struct.get $A 1
+ (global.get $b)
+ )
+ (struct.get $A 1
+ (global.get $c)
+ )
+ )
+ )
+ )
+)
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_8)
+;; CHECK-NEXT: (global.get $ctor-eval$global_8)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_7)
+;; CHECK-NEXT: (global.get $ctor-eval$global_7)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_6)
+;; CHECK-NEXT: (global.get $ctor-eval$global_6)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test1_6 (type $none_=>_none)
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test2_7 (type $none_=>_none)
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test3_8 (type $none_=>_none)
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
diff --git a/test/lit/ctor-eval/gc-cycle.wast b/test/lit/ctor-eval/gc-cycle.wast
new file mode 100644
index 000000000..a6edc28aa
--- /dev/null
+++ b/test/lit/ctor-eval/gc-cycle.wast
@@ -0,0 +1,1297 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+;; RUN: foreach %s %t wasm-ctor-eval --ctors=test --kept-exports=test --quiet -all -S -o - | filecheck %s
+
+(module
+ ;; CHECK: (type $A (struct (field (mut (ref null $A))) (field i32)))
+ (type $A (struct (field (mut (ref null $A))) (field i32)))
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (global $ctor-eval$global_3 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 42)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_3))
+ (global $a (mut (ref null $A)) (ref.null $A))
+
+ (func $test (export "test")
+ (local $a (ref $A))
+ ;; This generates a self-cycle where the global $a's ref field points to
+ ;; itself. To handle this, wasm-ctor-eval will emit a new global with a null
+ ;; in the ref field, and add a start function that adds the self-reference.
+ (global.set $a
+ (local.tee $a
+ (struct.new $A
+ (ref.null $A)
+ (i32.const 42)
+ )
+ )
+ )
+ (struct.set $A 0
+ (local.get $a)
+ (local.get $a)
+ )
+ )
+
+ ;; CHECK: (export "test" (func $test_3))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+
+ ;; CHECK: (func $keepalive (type $none_=>_i32) (result i32)
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (struct.get $A 0
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result i32)
+ ;; Getting $A.0.1 (reading from the reference in the global's first field)
+ ;; checks that we have a proper reference there. If we could do --fuzz-exec
+ ;; here we could validate that (but atm we can't use --output=fuzz-exec at the
+ ;; same time as --all-items in the update_lit_checks.py note).
+ (struct.get $A 1
+ (struct.get $A 0
+ (global.get $a)
+ )
+ )
+ )
+)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_3)
+;; CHECK-NEXT: (global.get $ctor-eval$global_3)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (local $a (ref $A))
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+(module
+ ;; As above, but with $A's fields reversed. This verifies we use the right
+ ;; field index in the start function.
+
+ ;; CHECK: (type $A (struct (field i32) (field (mut (ref null $A)))))
+ (type $A (struct (field i32) (field (mut (ref null $A)))))
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (global $ctor-eval$global_3 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (i32.const 42)
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_3))
+ (global $a (mut (ref null $A)) (ref.null $A))
+
+ (func $test (export "test")
+ (local $a (ref $A))
+ (global.set $a
+ (local.tee $a
+ (struct.new $A
+ (i32.const 42)
+ (ref.null $A)
+ )
+ )
+ )
+ (struct.set $A 1
+ (local.get $a)
+ (local.get $a)
+ )
+ )
+
+ ;; CHECK: (export "test" (func $test_3))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+
+ ;; CHECK: (func $keepalive (type $none_=>_i32) (result i32)
+ ;; CHECK-NEXT: (struct.get $A 0
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result i32)
+ (struct.get $A 0
+ (struct.get $A 1
+ (global.get $a)
+ )
+ )
+ )
+)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (struct.set $A 1
+;; CHECK-NEXT: (global.get $ctor-eval$global_3)
+;; CHECK-NEXT: (global.get $ctor-eval$global_3)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (local $a (ref $A))
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+(module
+ ;; A cycle between two globals.
+
+ ;; CHECK: (type $A (struct (field (mut (ref null $A))) (field i32)))
+ (type $A (struct (field (mut (ref null $A))) (field i32)))
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (global $ctor-eval$global_8 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 42)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_8))
+ (global $a (mut (ref null $A)) (ref.null $A))
+
+ ;; CHECK: (global $ctor-eval$global_7 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_8)
+ ;; CHECK-NEXT: (i32.const 1337)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $b (mut (ref null $A)) (global.get $ctor-eval$global_7))
+ (global $b (mut (ref null $A)) (ref.null $A))
+
+ (func $test (export "test")
+ (local $a (ref $A))
+ (local $b (ref $A))
+ (global.set $a
+ (local.tee $a
+ (struct.new $A
+ (ref.null $A)
+ (i32.const 42)
+ )
+ )
+ )
+ (global.set $b
+ (local.tee $b
+ (struct.new $A
+ (global.get $a) ;; $b can refer to $a since we've created $a already.
+ (i32.const 1337)
+ )
+ )
+ )
+ ;; $a needs a set to allow us to create the cycle.
+ (struct.set $A 0
+ (local.get $a)
+ (local.get $b)
+ )
+ )
+
+ ;; CHECK: (export "test" (func $test_3))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+
+ ;; CHECK: (func $keepalive (type $none_=>_i32) (result i32)
+ ;; CHECK-NEXT: (i32.add
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $b)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result i32)
+ (i32.add
+ (struct.get $A 1
+ (global.get $a)
+ )
+ (struct.get $A 1
+ (global.get $b)
+ )
+ )
+ )
+)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_8)
+;; CHECK-NEXT: (global.get $ctor-eval$global_7)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (local $a (ref $A))
+;; CHECK-NEXT: (local $b (ref $A))
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+(module
+ ;; A cycle between two globals of different types. One of them has an
+ ;; immutable field in the cycle.
+
+ (rec
+ ;; CHECK: (rec
+ ;; CHECK-NEXT: (type $A (struct (field (mut (ref null $B))) (field i32)))
+ (type $A (struct (field (mut (ref null $B))) (field i32)))
+
+ ;; CHECK: (type $B (struct (field (ref null $A)) (field i32)))
+ (type $B (struct (field (ref null $A)) (field i32)))
+ )
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (global $ctor-eval$global_8 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 42)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_8))
+ (global $a (mut (ref null $A)) (ref.null $A))
+
+ ;; CHECK: (global $ctor-eval$global_7 (ref $B) (struct.new $B
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_8)
+ ;; CHECK-NEXT: (i32.const 1337)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $b (mut (ref null $B)) (global.get $ctor-eval$global_7))
+ (global $b (mut (ref null $B)) (ref.null $B))
+
+ (func $test (export "test")
+ (local $a (ref $A))
+ (local $b (ref $B))
+ (global.set $a
+ (local.tee $a
+ (struct.new $A
+ (ref.null $A)
+ (i32.const 42)
+ )
+ )
+ )
+ (global.set $b
+ (local.tee $b
+ (struct.new $B
+ (global.get $a)
+ (i32.const 1337)
+ )
+ )
+ )
+ (struct.set $A 0
+ (local.get $a)
+ (local.get $b)
+ )
+ )
+
+ ;; CHECK: (export "test" (func $test_3))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+
+ ;; CHECK: (func $keepalive (type $none_=>_i32) (result i32)
+ ;; CHECK-NEXT: (i32.add
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (struct.get $B 1
+ ;; CHECK-NEXT: (global.get $b)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result i32)
+ (i32.add
+ (struct.get $A 1
+ (global.get $a)
+ )
+ (struct.get $B 1
+ (global.get $b)
+ )
+ )
+ )
+)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_8)
+;; CHECK-NEXT: (global.get $ctor-eval$global_7)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (local $a (ref $A))
+;; CHECK-NEXT: (local $b (ref $B))
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+(module
+ ;; As above, but with the order of globals reversed.
+
+ (rec
+ ;; CHECK: (rec
+ ;; CHECK-NEXT: (type $A (struct (field (mut (ref null $B))) (field i32)))
+ (type $A (struct (field (mut (ref null $B))) (field i32)))
+
+ ;; CHECK: (type $B (struct (field (ref null $A)) (field i32)))
+ (type $B (struct (field (ref null $A)) (field i32)))
+ )
+
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (global $ctor-eval$global_8 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 42)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_8))
+
+ ;; CHECK: (global $ctor-eval$global_7 (ref $B) (struct.new $B
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_8)
+ ;; CHECK-NEXT: (i32.const 1337)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $b (mut (ref null $B)) (global.get $ctor-eval$global_7))
+ (global $b (mut (ref null $B)) (ref.null $B))
+
+ (global $a (mut (ref null $A)) (ref.null $A))
+
+ (func $test (export "test")
+ (local $a (ref $A))
+ (local $b (ref $B))
+ (global.set $a
+ (local.tee $a
+ (struct.new $A
+ (ref.null $A)
+ (i32.const 42)
+ )
+ )
+ )
+ (global.set $b
+ (local.tee $b
+ (struct.new $B
+ (global.get $a)
+ (i32.const 1337)
+ )
+ )
+ )
+ (struct.set $A 0
+ (local.get $a)
+ (local.get $b)
+ )
+ )
+
+ ;; CHECK: (export "test" (func $test_3))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+
+ ;; CHECK: (func $keepalive (type $none_=>_i32) (result i32)
+ ;; CHECK-NEXT: (i32.add
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (struct.get $B 1
+ ;; CHECK-NEXT: (global.get $b)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result i32)
+ (i32.add
+ (struct.get $A 1
+ (global.get $a)
+ )
+ (struct.get $B 1
+ (global.get $b)
+ )
+ )
+ )
+)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_8)
+;; CHECK-NEXT: (global.get $ctor-eval$global_7)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (local $a (ref $A))
+;; CHECK-NEXT: (local $b (ref $B))
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+(module
+ ;; A cycle as above, but with non-nullability rather than immutability.
+
+ (rec
+ ;; CHECK: (rec
+ ;; CHECK-NEXT: (type $A (struct (field (mut (ref null $B))) (field i32)))
+ (type $A (struct (field (mut (ref null $B))) (field i32)))
+
+ ;; CHECK: (type $B (struct (field (mut (ref $A))) (field i32)))
+ (type $B (struct (field (mut (ref $A))) (field i32)))
+ )
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (global $ctor-eval$global_8 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 42)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_8))
+ (global $a (mut (ref null $A)) (ref.null $A))
+
+ (global $b (mut (ref null $B)) (ref.null $B))
+
+ (func $test (export "test")
+ (local $a (ref $A))
+ (local $b (ref $B))
+ (global.set $a
+ (local.tee $a
+ (struct.new $A
+ (ref.null $A)
+ (i32.const 42)
+ )
+ )
+ )
+ (global.set $b
+ (local.tee $b
+ (struct.new $B
+ (local.get $a)
+ (i32.const 1337)
+ )
+ )
+ )
+ (struct.set $A 0
+ (local.get $a)
+ (local.get $b)
+ )
+ )
+
+ ;; CHECK: (global $ctor-eval$global_7 (ref $B) (struct.new $B
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_8)
+ ;; CHECK-NEXT: (i32.const 1337)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (export "test" (func $test_3))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+
+ ;; CHECK: (func $keepalive (type $none_=>_i32) (result i32)
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result i32)
+ (struct.get $A 1
+ (global.get $a)
+ )
+ )
+)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_8)
+;; CHECK-NEXT: (global.get $ctor-eval$global_7)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (local $a (ref $A))
+;; CHECK-NEXT: (local $b (ref $B))
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+(module
+ ;; A cycle as above, but with globals in reverse order and with both non-
+ ;; nullability and immutability.
+
+ (rec
+ ;; CHECK: (rec
+ ;; CHECK-NEXT: (type $A (struct (field (mut (ref null $B))) (field i32)))
+ (type $A (struct (field (mut (ref null $B))) (field i32)))
+
+ ;; CHECK: (type $B (struct (field (ref $A)) (field i32)))
+ (type $B (struct (field (ref $A)) (field i32)))
+ )
+
+
+ (global $b (mut (ref null $B)) (ref.null $B))
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (global $ctor-eval$global_8 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 42)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_8))
+ (global $a (mut (ref null $A)) (ref.null $A))
+
+ (func $test (export "test")
+ (local $a (ref $A))
+ (local $b (ref $B))
+ (global.set $a
+ (local.tee $a
+ (struct.new $A
+ (ref.null $A)
+ (i32.const 42)
+ )
+ )
+ )
+ (global.set $b
+ (local.tee $b
+ (struct.new $B
+ (local.get $a)
+ (i32.const 1337)
+ )
+ )
+ )
+ (struct.set $A 0
+ (local.get $a)
+ (local.get $b)
+ )
+ )
+
+ ;; CHECK: (global $ctor-eval$global_7 (ref $B) (struct.new $B
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_8)
+ ;; CHECK-NEXT: (i32.const 1337)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (export "test" (func $test_3))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+
+ ;; CHECK: (func $keepalive (type $none_=>_i32) (result i32)
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result i32)
+ (struct.get $A 1
+ (global.get $a)
+ )
+ )
+)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_8)
+;; CHECK-NEXT: (global.get $ctor-eval$global_7)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (local $a (ref $A))
+;; CHECK-NEXT: (local $b (ref $B))
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+(module
+ ;; A cycle between three globals.
+
+ ;; CHECK: (type $A (struct (field (mut (ref null $A))) (field i32)))
+ (type $A (struct (field (mut (ref null $A))) (field i32)))
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (global $ctor-eval$global_13 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 1337)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $ctor-eval$global_14 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 42)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_14))
+ (global $a (mut (ref null $A)) (ref.null $A))
+
+ (global $b (mut (ref null $A)) (ref.null $A))
+
+ (global $c (mut (ref null $A)) (ref.null $A))
+
+ (func $test (export "test")
+ (local $a (ref $A))
+ (local $b (ref $A))
+ (local $c (ref $A))
+ (global.set $a
+ (local.tee $a
+ (struct.new $A
+ (ref.null $A)
+ (i32.const 42)
+ )
+ )
+ )
+ (global.set $b
+ (local.tee $b
+ (struct.new $A
+ (global.get $a)
+ (i32.const 1337)
+ )
+ )
+ )
+ (global.set $c
+ (local.tee $c
+ (struct.new $A
+ (global.get $b)
+ (i32.const 99999)
+ )
+ )
+ )
+ (struct.set $A 0
+ (local.get $a)
+ (local.get $c)
+ )
+ )
+
+ ;; CHECK: (global $ctor-eval$global_12 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_13)
+ ;; CHECK-NEXT: (i32.const 99999)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (export "test" (func $test_3))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+
+ ;; CHECK: (func $keepalive (type $none_=>_i32) (result i32)
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result i32)
+ (struct.get $A 1
+ (global.get $a)
+ )
+ )
+)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_13)
+;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+;; CHECK-NEXT: (global.get $ctor-eval$global_12)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (local $a (ref $A))
+;; CHECK-NEXT: (local $b (ref $A))
+;; CHECK-NEXT: (local $c (ref $A))
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+(module
+ ;; A cycle between three globals as above, but now using different types and
+ ;; also both structs and arrays. Also reverse the order of globals, make
+ ;; one array immutable and one non-nullable, and make one array refer to the
+ ;; other two.
+
+ (rec
+ ;; CHECK: (rec
+ ;; CHECK-NEXT: (type $A (struct (field (mut (ref null $C))) (field i32)))
+ (type $A (struct (field (mut (ref null $C))) (field i32)))
+ ;; CHECK: (type $B (array (ref null $A)))
+ (type $B (array (ref null $A)))
+ ;; CHECK: (type $C (array (mut (ref any))))
+ (type $C (array (mut (ref any))))
+ )
+
+ (global $c (mut (ref null $C)) (ref.null $C))
+
+ (global $b (mut (ref null $B)) (ref.null $B))
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (global $ctor-eval$global_14 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 42)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $ctor-eval$global_13 (ref $B) (array.new_fixed $B
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_14))
+ (global $a (mut (ref null $A)) (ref.null $A))
+
+ (func $test (export "test")
+ (local $a (ref $A))
+ (local $b (ref $B))
+ (local $c (ref $C))
+ (global.set $a
+ (local.tee $a
+ (struct.new $A
+ (ref.null $C)
+ (i32.const 42)
+ )
+ )
+ )
+ (global.set $b
+ (local.tee $b
+ (array.new $B
+ (global.get $a)
+ (i32.const 10)
+ )
+ )
+ )
+ (global.set $c
+ (local.tee $c
+ (array.new_fixed $C
+ (local.get $b)
+ (local.get $a)
+ )
+ )
+ )
+ (struct.set $A 0
+ (local.get $a)
+ (global.get $c)
+ )
+ )
+
+ ;; CHECK: (global $ctor-eval$global_12 (ref $C) (array.new_fixed $C
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_13)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (export "test" (func $test_3))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+
+ ;; CHECK: (func $keepalive (type $none_=>_i32) (result i32)
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result i32)
+ (struct.get $A 1
+ (global.get $a)
+ )
+ )
+)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+;; CHECK-NEXT: (global.get $ctor-eval$global_12)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (local $a (ref $A))
+;; CHECK-NEXT: (local $b (ref $B))
+;; CHECK-NEXT: (local $c (ref $C))
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+(module
+ ;; As above but with the order of globals reversed once more.
+
+ (rec
+ ;; CHECK: (rec
+ ;; CHECK-NEXT: (type $A (struct (field (mut (ref null $C))) (field i32)))
+ (type $A (struct (field (mut (ref null $C))) (field i32)))
+ ;; CHECK: (type $B (array (ref null $A)))
+ (type $B (array (ref null $A)))
+ ;; CHECK: (type $C (array (mut (ref any))))
+ (type $C (array (mut (ref any))))
+ )
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (global $ctor-eval$global_14 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 42)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $ctor-eval$global_13 (ref $B) (array.new_fixed $B
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_14))
+ (global $a (mut (ref null $A)) (ref.null $A))
+
+ (global $b (mut (ref null $B)) (ref.null $B))
+
+ (global $c (mut (ref null $C)) (ref.null $C))
+
+ (func $test (export "test")
+ (local $a (ref $A))
+ (local $b (ref $B))
+ (local $c (ref $C))
+ (global.set $a
+ (local.tee $a
+ (struct.new $A
+ (ref.null $C)
+ (i32.const 42)
+ )
+ )
+ )
+ (global.set $b
+ (local.tee $b
+ (array.new $B
+ (global.get $a)
+ (i32.const 10)
+ )
+ )
+ )
+ (global.set $c
+ (local.tee $c
+ (array.new_fixed $C
+ (local.get $b)
+ (local.get $a)
+ )
+ )
+ )
+ (struct.set $A 0
+ (local.get $a)
+ (global.get $c)
+ )
+ )
+
+ ;; CHECK: (global $ctor-eval$global_12 (ref $C) (array.new_fixed $C
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_13)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (export "test" (func $test_3))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+
+ ;; CHECK: (func $keepalive (type $none_=>_i32) (result i32)
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result i32)
+ (struct.get $A 1
+ (global.get $a)
+ )
+ )
+)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+;; CHECK-NEXT: (global.get $ctor-eval$global_12)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (local $a (ref $A))
+;; CHECK-NEXT: (local $b (ref $B))
+;; CHECK-NEXT: (local $c (ref $C))
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+(module
+ ;; A cycle between two globals, where some of the fields participate in the
+ ;; cycle and some do not.
+
+ (rec
+ ;; CHECK: (rec
+ ;; CHECK-NEXT: (type $A (struct (field (mut (ref null $B))) (field (mut (ref null $B))) (field (mut (ref null $B)))))
+ (type $A (struct (field (mut (ref null $B))) (field (mut (ref null $B))) (field (mut (ref null $B)))))
+ ;; CHECK: (type $B (array (ref null $A)))
+ (type $B (array (ref null $A)))
+ )
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_anyref (func (result anyref)))
+
+ ;; CHECK: (global $ctor-eval$global_16 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_16))
+ (global $a (mut (ref null $A)) (ref.null $A))
+ (global $b (mut (ref null $B)) (ref.null $B))
+
+ (func $test (export "test")
+ (local $a (ref $A))
+ (global.set $a
+ (local.tee $a
+ (struct.new $A
+ (array.new_default $B
+ (i32.const 0)
+ )
+ (ref.null $B)
+ (array.new_default $B
+ (i32.const 0)
+ )
+ )
+ )
+ )
+ (global.set $b
+ (array.new_fixed $B
+ (struct.new_default $A)
+ (global.get $a)
+ (struct.new_default $A)
+ )
+ )
+ (struct.set $A 1
+ (local.get $a)
+ (global.get $b)
+ )
+ )
+
+ ;; CHECK: (global $ctor-eval$global_19 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $ctor-eval$global_15 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $ctor-eval$global_14 (ref $B) (array.new_fixed $B
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_15)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_16)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_19)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $ctor-eval$global_17 (ref $B) (array.new_fixed $B))
+
+ ;; CHECK: (global $ctor-eval$global_18 (ref $B) (array.new_fixed $B))
+
+ ;; CHECK: (export "test" (func $test_3))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+
+ ;; CHECK: (func $keepalive (type $none_=>_anyref) (result anyref)
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result (ref null any))
+ (struct.get $A 1
+ (global.get $a)
+ )
+ )
+)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global_16)
+;; CHECK-NEXT: (global.get $ctor-eval$global_17)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (struct.set $A 1
+;; CHECK-NEXT: (global.get $ctor-eval$global_16)
+;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (struct.set $A 2
+;; CHECK-NEXT: (global.get $ctor-eval$global_16)
+;; CHECK-NEXT: (global.get $ctor-eval$global_18)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (local $a (ref $A))
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+(module
+ ;; As above, with the cycle creation logic reversed.
+
+ (rec
+ ;; CHECK: (rec
+ ;; CHECK-NEXT: (type $A (struct (field (ref null $B)) (field (ref null $B)) (field (ref null $B))))
+ (type $A (struct (field (ref null $B)) (field (ref null $B)) (field (ref null $B))))
+ ;; CHECK: (type $B (array (mut (ref null $A))))
+ (type $B (array (mut (ref null $A))))
+ )
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_anyref (func (result anyref)))
+
+ ;; CHECK: (global $ctor-eval$global_16 (ref $B) (array.new_fixed $B
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $ctor-eval$global_19 (ref $B) (array.new_fixed $B))
+
+ ;; CHECK: (global $ctor-eval$global_15 (ref $B) (array.new_fixed $B))
+
+ ;; CHECK: (global $ctor-eval$global_14 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_15)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_16)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_19)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_14))
+ (global $a (mut (ref null $A)) (ref.null $A))
+ (global $b (mut (ref null $B)) (ref.null $B))
+
+ (func $test (export "test")
+ (local $b (ref $B))
+ (global.set $b
+ (local.tee $b
+ (array.new_fixed $B
+ (struct.new_default $A)
+ (ref.null $A)
+ (struct.new_default $A)
+ )
+ )
+ )
+ (global.set $a
+ (struct.new $A
+ (array.new_default $B
+ (i32.const 0)
+ )
+ (global.get $b)
+ (array.new_default $B
+ (i32.const 0)
+ )
+ )
+ )
+ (array.set $B
+ (local.get $b)
+ (i32.const 1)
+ (global.get $a)
+ )
+ )
+
+ ;; CHECK: (global $ctor-eval$global_17 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $ctor-eval$global_18 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (export "test" (func $test_3))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+
+ ;; CHECK: (func $keepalive (type $none_=>_anyref) (result anyref)
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result (ref null any))
+ (struct.get $A 1
+ (global.get $a)
+ )
+ )
+)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (array.set $B
+;; CHECK-NEXT: (global.get $ctor-eval$global_16)
+;; CHECK-NEXT: (i32.const 0)
+;; CHECK-NEXT: (global.get $ctor-eval$global_17)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (array.set $B
+;; CHECK-NEXT: (global.get $ctor-eval$global_16)
+;; CHECK-NEXT: (i32.const 1)
+;; CHECK-NEXT: (global.get $ctor-eval$global_14)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (array.set $B
+;; CHECK-NEXT: (global.get $ctor-eval$global_16)
+;; CHECK-NEXT: (i32.const 2)
+;; CHECK-NEXT: (global.get $ctor-eval$global_18)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (local $b (ref $B))
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+(module
+ ;; The start function already exists here. We must prepend to it.
+
+ ;; CHECK: (type $A (struct (field (mut (ref null $A))) (field i32)))
+ (type $A (struct (field (mut (ref null $A))) (field i32)))
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (global $ctor-eval$global_4 (ref $A) (struct.new $A
+ ;; CHECK-NEXT: (ref.null none)
+ ;; CHECK-NEXT: (i32.const 42)
+ ;; CHECK-NEXT: ))
+
+ ;; CHECK: (global $a (mut (ref null $A)) (global.get $ctor-eval$global_4))
+ (global $a (mut (ref null $A)) (ref.null $A))
+
+ ;; CHECK: (global $b (mut (ref null $A)) (ref.null none))
+ (global $b (mut (ref null $A)) (ref.null $A))
+
+ ;; CHECK: (export "test" (func $test_3))
+
+ ;; CHECK: (export "keepalive" (func $keepalive))
+
+ ;; CHECK: (start $start)
+ (start $start)
+
+ (func $test (export "test")
+ (local $a (ref $A))
+ (global.set $a
+ (local.tee $a
+ (struct.new $A
+ (ref.null $A)
+ (i32.const 42)
+ )
+ )
+ )
+ (struct.set $A 0
+ (local.get $a)
+ (local.get $a)
+ )
+ )
+
+ ;; CHECK: (func $keepalive (type $none_=>_i32) (result i32)
+ ;; CHECK-NEXT: (i32.add
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (struct.get $A 1
+ ;; CHECK-NEXT: (global.get $b)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $keepalive (export "keepalive") (result i32)
+ (i32.add
+ (struct.get $A 1
+ (global.get $a)
+ )
+ (struct.get $A 1
+ (global.get $b)
+ )
+ )
+ )
+
+ ;; CHECK: (func $start (type $none_=>_none)
+ ;; CHECK-NEXT: (struct.set $A 0
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_4)
+ ;; CHECK-NEXT: (global.get $ctor-eval$global_4)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $b
+ ;; CHECK-NEXT: (global.get $a)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $start
+ (global.set $b
+ (global.get $a)
+ )
+ )
+)
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (local $a (ref $A))
+;; CHECK-NEXT: (nop)
+;; CHECK-NEXT: )
+(module
+ ;; CHECK: (type $A (struct (field (mut (ref null $A)))))
+ (type $A (struct (field (mut (ref null $A)))))
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $anyref_=>_none (func (param anyref)))
+
+ ;; CHECK: (import "a" "b" (func $import (param anyref)))
+ (import "a" "b" (func $import (param anyref)))
+
+ (func $test (export "test")
+ (local $a (ref $A))
+ (struct.set $A 0
+ (local.tee $a
+ (struct.new_default $A)
+ )
+ (local.get $a)
+ )
+ ;; The previous instructions created a cycle, which we now send to an import.
+ ;; The import will block us from evalling the entire function, and we will
+ ;; only partially eval it, removing the statements before the call. Note that
+ ;; the cycle only exists in local state - there is no global it is copied to -
+ ;; and so this test verifies that we handle cycles in local state.
+ (call $import
+ (local.get $a)
+ )
+ )
+)
+;; CHECK: (global $ctor-eval$global (ref $A) (struct.new $A
+;; CHECK-NEXT: (ref.null none)
+;; CHECK-NEXT: ))
+
+;; CHECK: (export "test" (func $test_3))
+
+;; CHECK: (start $start)
+
+;; CHECK: (func $start (type $none_=>_none)
+;; CHECK-NEXT: (struct.set $A 0
+;; CHECK-NEXT: (global.get $ctor-eval$global)
+;; CHECK-NEXT: (global.get $ctor-eval$global)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
+
+;; CHECK: (func $test_3 (type $none_=>_none)
+;; CHECK-NEXT: (call $import
+;; CHECK-NEXT: (global.get $ctor-eval$global)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: )
diff --git a/test/lit/ctor-eval/partial-global.wat b/test/lit/ctor-eval/partial-global.wat
new file mode 100644
index 000000000..fdd196d02
--- /dev/null
+++ b/test/lit/ctor-eval/partial-global.wat
@@ -0,0 +1,39 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+;; RUN: wasm-ctor-eval %s --ctors=test --kept-exports=test --quiet -all -S -o - | filecheck %s
+
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $global (mut i32) (i32.const 0))
+ (global $global (mut i32) (i32.const 0))
+
+ (func $test (export "test")
+ ;; The nop can be evalled away, but not the loop. We should not apply any
+ ;; partial results from the loop - in particular, the global must remain at
+ ;; 0. That is, the global.set of 999 below must not be applied to the global.
+ ;;
+ ;; (It is true that in this simple module it would be ok to set 999 to the
+ ;; global, but if the global were exported for example then that would not
+ ;; be the case, nor would it be the case if the code did $global = $global + 1
+ ;; or such. That is, since the global.set is not evalled away, its effects
+ ;; must not be applied; we do both atomically or neither, so that the
+ ;; global.set's execution only happens once.)
+
+ (nop)
+ (loop
+ (global.set $global
+ (i32.const 999)
+ )
+ (unreachable)
+ )
+ )
+)
+
+;; CHECK: (export "test" (func $test_1))
+
+;; CHECK: (func $test_1 (type $none_=>_none)
+;; CHECK-NEXT: (global.set $global
+;; CHECK-NEXT: (i32.const 999)
+;; CHECK-NEXT: )
+;; CHECK-NEXT: (unreachable)
+;; CHECK-NEXT: )