diff --git a/scripts/test/fuzzing.py b/scripts/test/fuzzing.py index 5e716107fc5..895c5b02539 100644 --- a/scripts/test/fuzzing.py +++ b/scripts/test/fuzzing.py @@ -111,6 +111,8 @@ 'precompute-stack-switching.wast', 'unsubtyping-stack-switching.wast', 'vacuum-stack-switching.wast', + 'cont.wast', + 'cont_simple.wast', # TODO: fuzzer support for custom descriptors 'remove-unused-module-elements-refs-descriptors.wast', 'custom-descriptors.wast', diff --git a/src/ir/iteration.h b/src/ir/iteration.h index bfbf3bb6263..00f2a7383e4 100644 --- a/src/ir/iteration.h +++ b/src/ir/iteration.h @@ -32,7 +32,7 @@ namespace wasm { // In general, it is preferable not to use this class and to directly access the // children (using e.g. iff->ifTrue etc.), as that is faster. However, in cases // where speed does not matter, this can be convenient. TODO: reimplement these -// to avoid materializing all the chilren at once. +// to avoid materializing all the children at once. // // ChildIterator - Iterates over all children // diff --git a/src/literal.h b/src/literal.h index 10f2158a403..fb7d5183e7b 100644 --- a/src/literal.h +++ b/src/literal.h @@ -33,6 +33,7 @@ namespace wasm { class Literals; struct GCData; struct ExnData; +struct ContData; class Literal { // store only integers, whose bits are deterministic. floats @@ -63,6 +64,8 @@ class Literal { std::shared_ptr gcData; // A reference to Exn data. std::shared_ptr exnData; + // A reference to a Continuation. + std::shared_ptr contData; }; public: @@ -93,6 +96,7 @@ class Literal { } explicit Literal(std::shared_ptr gcData, HeapType type); explicit Literal(std::shared_ptr exnData); + explicit Literal(std::shared_ptr contData); explicit Literal(std::string_view string); Literal(const Literal& other); Literal& operator=(const Literal& other); @@ -105,6 +109,7 @@ class Literal { // a null or i31). This includes structs, arrays, and also strings. bool isData() const { return type.isData(); } bool isExn() const { return type.isExn(); } + bool isContinuation() const { return type.isContinuation(); } bool isString() const { return type.isString(); } bool isNull() const { return type.isNull(); } @@ -312,6 +317,7 @@ class Literal { } std::shared_ptr getGCData() const; std::shared_ptr getExnData() const; + std::shared_ptr getContData() const; // careful! int32_t* geti32Ptr() { diff --git a/src/parser/wast-parser.cpp b/src/parser/wast-parser.cpp index 0f7887f8fe2..9e12a855456 100644 --- a/src/parser/wast-parser.cpp +++ b/src/parser/wast-parser.cpp @@ -381,6 +381,25 @@ MaybeResult assertTrap(Lexer& in) { return Assertion{AssertModule{ModuleAssertionType::Trap, *mod}}; } +// (assert_suspension action msg) +MaybeResult assertSuspension(Lexer& in) { + if (!in.takeSExprStart("assert_suspension"sv)) { + return {}; + } + if (auto a = maybeAction(in)) { + CHECK_ERR(a); + auto msg = in.takeString(); + if (!msg) { + return in.err("expected error message"); + } + if (!in.takeRParen()) { + return in.err("expected end of assertion"); + } + return Assertion{AssertAction{ActionAssertionType::Suspension, *a}}; + } + return in.err("invalid assert_suspension"); +} + MaybeResult assertion(Lexer& in) { if (auto a = assertReturn(in)) { CHECK_ERR(a); @@ -402,6 +421,10 @@ MaybeResult assertion(Lexer& in) { CHECK_ERR(a); return *a; } + if (auto a = assertSuspension(in)) { + CHECK_ERR(a); + return *a; + } return {}; } diff --git a/src/parser/wat-parser.h b/src/parser/wat-parser.h index b0074faa6e1..34e9b69f8f0 100644 --- a/src/parser/wat-parser.h +++ b/src/parser/wat-parser.h @@ -78,7 +78,7 @@ struct AssertReturn { ExpectedResults expected; }; -enum class ActionAssertionType { Trap, Exhaustion, Exception }; +enum class ActionAssertionType { Trap, Exhaustion, Exception, Suspension }; struct AssertAction { ActionAssertionType type; diff --git a/src/shell-interface.h b/src/shell-interface.h index 3a8b6d23314..421c76f76b3 100644 --- a/src/shell-interface.h +++ b/src/shell-interface.h @@ -144,7 +144,9 @@ struct ShellExternalInterface : ModuleRunner::ExternalInterface { std::cout << "exit()\n"; throw ExitException(); } else if (auto* inst = getImportInstance(import)) { - return inst->callExport(import->base, arguments); + auto flow = inst->callExport(import->base, arguments); + assert(!flow.suspendTag); // TODO: support stack switching on calls + return flow.values; } Fatal() << "callImport: unknown import: " << import->module.str << "." << import->name.str; @@ -191,7 +193,9 @@ struct ShellExternalInterface : ModuleRunner::ExternalInterface { if (func->imported()) { return callImport(func, arguments); } else { - return instance.callFunction(func->name, arguments); + auto flow = instance.callFunction(func->name, arguments); + assert(!flow.suspendTag); // TODO: support stack switching on calls + return flow.values; } } diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index 03e9ccb47aa..0e1456cdee0 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -254,7 +254,9 @@ struct LoggingExternalInterface : public ShellExternalInterface { } // Call the function. - return instance->callFunction(func->name, arguments); + auto flow = instance->callFunction(func->name, arguments); + assert(!flow.suspendTag); + return flow.values; } void setModuleRunner(ModuleRunner* instance_) { instance = instance_; } @@ -471,7 +473,12 @@ struct ExecutionResults { } arguments.push_back(Literal::makeZero(param)); } - return instance.callFunction(func->name, arguments); + auto flow = instance.callFunction(func->name, arguments); + if (flow.suspendTag) { // TODO: support stack switching here + std::cout << "[exception thrown: unhandled suspend]" << std::endl; + return Exception{}; + } + return flow.values; } catch (const TrapException&) { return Trap{}; } catch (const WasmException& e) { diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index c7ed64cc1f5..c92b815eb6c 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -350,7 +350,11 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { targetFunc.toString()); } if (!func->imported()) { - return instance.callFunction(targetFunc, arguments); + auto flow = instance.callFunction(targetFunc, arguments); + if (flow.suspendTag) { + throw FailToEvalException("unhandled suspend"); + } + return flow.values; } else { throw FailToEvalException( std::string("callTable on imported function: ") + diff --git a/src/tools/wasm-shell.cpp b/src/tools/wasm-shell.cpp index 9139983c947..40e4ddc67df 100644 --- a/src/tools/wasm-shell.cpp +++ b/src/tools/wasm-shell.cpp @@ -186,8 +186,12 @@ struct Shell { struct TrapResult {}; struct HostLimitResult {}; struct ExceptionResult {}; - using ActionResult = - std::variant; + struct SuspensionResult {}; + using ActionResult = std::variant; std::string resultToString(ActionResult& result) { if (std::get_if(&result)) { @@ -196,6 +200,8 @@ struct Shell { return "exceeded host limit"; } else if (std::get_if(&result)) { return "exception"; + } else if (std::get_if(&result)) { + return "suspension"; } else if (auto* vals = std::get_if(&result)) { std::stringstream ss; ss << *vals; @@ -213,8 +219,9 @@ struct Shell { return TrapResult{}; } auto& instance = it->second; + Flow flow; try { - return instance->callExport(invoke->name, invoke->args); + flow = instance->callExport(invoke->name, invoke->args); } catch (TrapException&) { return TrapResult{}; } catch (HostLimitException&) { @@ -224,6 +231,10 @@ struct Shell { } catch (...) { WASM_UNREACHABLE("unexpected error"); } + if (flow.suspendTag) { + return SuspensionResult{}; + } + return flow.values; } else if (auto* get = std::get_if(&act)) { auto it = instances.find(get->base ? *get->base : lastModule); if (it == instances.end()) { @@ -390,6 +401,12 @@ struct Shell { } err << "expected exception"; break; + case ActionAssertionType::Suspension: + if (std::get_if(&result)) { + return Ok{}; + } + err << "expected suspension"; + break; } err << ", got " << resultToString(result); return Err{err.str()}; diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index 75a4c03018a..01fb2b05816 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -15,9 +15,14 @@ */ // -// Simple WebAssembly interpreter. This operates directly on the AST, -// for simplicity and clarity. A goal is for it to be possible for -// people to read this code and understand WebAssembly semantics. +// Simple WebAssembly interpreter. This operates directly (in-place) on our IR, +// and our IR is a structured form of Wasm, so this is similar to an AST +// interpreter. Operating directly on our IR makes us efficient in the +// Precompute pass, which tries to execute every bit of code. +// +// As a side benefit, interpreting the IR directly makes the code an easy way to +// understand WebAssembly semantics (see e.g. visitLoop(), which is basically +// just a simple loop). // #ifndef wasm_wasm_interpreter_h @@ -30,7 +35,9 @@ #include "fp16.h" #include "ir/intrinsics.h" +#include "ir/iteration.h" #include "ir/module-utils.h" +#include "ir/properties.h" #include "support/bits.h" #include "support/safe_integer.h" #include "support/stdckdint.h" @@ -59,7 +66,7 @@ struct NonconstantException {}; // Utilities -extern Name RETURN_FLOW, RETURN_CALL_FLOW, NONCONSTANT_FLOW; +extern Name RETURN_FLOW, RETURN_CALL_FLOW, NONCONSTANT_FLOW, SUSPEND_FLOW; // Stuff that flows around during executing expressions: a literal, or a change // in control flow. @@ -73,9 +80,15 @@ class Flow { Flow(Name breakTo, Literal value) : values{value}, breakTo(breakTo) {} Flow(Name breakTo, Literals&& values) : values(std::move(values)), breakTo(breakTo) {} + Flow(Name breakTo, Name suspendTag, Literals&& values) + : values(std::move(values)), breakTo(breakTo), suspendTag(suspendTag) { + assert(breakTo == SUSPEND_FLOW); + } Literals values; Name breakTo; // if non-null, a break is going on + Name suspendTag; // if non-null, breakTo must be SUSPEND_FLOW, and this is the + // tag being suspended // A helper function for the common case where there is only one value const Literal& getSingleValue() { @@ -91,6 +104,8 @@ class Flow { return builder.makeConstantExpression(values); } + // Returns true if we are breaking out of normal execution. This can be + // because of a break/continue, or a continuation. bool breaking() const { return breakTo.is(); } void clearIf(Name target) { @@ -107,11 +122,82 @@ class Flow { } o << flow.values[i]; } + if (flow.suspendTag) { + o << " [suspend:" << flow.suspendTag << ']'; + } o << "})"; return o; } }; +// Suspend/resume support. +// +// As we operate directly on our structured IR, we do not have a program counter +// (bytecode offset to execute, or such), nor can we use continuation-passing +// style. Instead, we implement suspending and resuming code in a parallel way +// to how Asyncify does so, see src/passes/Asyncify.cpp (as well as +// https://kripken.github.io/blog/wasm/2019/07/16/asyncify.html). That +// transformation modifies wasm, while we are an interpreter that executes wasm, +// but the shared idea is that to resume code we simply need to get to where we +// were when we suspended, so we have a "resuming" mode in which we walk the IR +// but do not execute normally. While resuming we basically re-wind the stack, +// using data we stashed on the side while unwinding. For example, if we unwind +// an If instruction then we note which arm of the If we unwound from, and then +// when we re-wind we enter that proper arm, etc. +// +// This is not the most efficient way to pause and resume execution (a program +// counter/goto would be much faster!) but this is very simple to implement in +// our interpreter, and in a way that does not make the interpreter slower when +// not pausing/resuming. As with Asyncify, the assumption is that pauses/resumes +// are rare, and it is acceptable for them to be less efficient. +// +// Key parts of this support: +// * |ContData| is the key data structure that represents continuations. Each +// continuation Literal has a reference to one of these. +// * Inside the interpreter itself: +// * |currContinuation| is the continuation we are currently executing, if +// any. +// * |resuming| is set when we are in the special "resuming" mode mentioned +// above. +// * When we suspend, everything on the stack will save the necessary info +// to recreate itself later during resume. That is done by calling +// |pushResumeEntry|, which saves info on the continuation, and which is +// read during resume using |popResumeEntry|. +// * |valueStack| preserves values on the stack, so that we can save them +// later if we suspend. +// * When we resume, the old |valuesStack| is converted into +// |restoredValuesMap|. When a visit() sees that we have a value to +// restore, it simply returns it. +// * The main suspend/resume logic is in |visit|. That handles everything +// except for control flow structure-specific handling, which is done in +// |visitIf| etc. (each such structure handles itself). + +struct ContData { + // The function this continuation begins in. + // TODO: handle cross-module calls using something other than a Name here. + Name func; + + // The continuation type. + HeapType type; + + // The expression to resume execution at, which is where we suspended. Or, if + // we are just starting to execute this continuation, this is nullptr (and we + // will resume at the very start). + Expression* resumeExpr = nullptr; + + // Information about how to resume execution, a list of instruction and data + // that we "replay" into the value and call stacks. For convenience we split + // this into separate entries, each one a Literals. Typically an instruction + // will emit a single Literals for itself, or possibly a few bundles. + std::vector resumeInfo; + + // Whether we executed. Continuations are one-shot, so they may not be + // executed a second time. + bool executed = false; + + ContData(Name func, HeapType type) : func(func), type(type) {} +}; + // Execute an expression template class ExpressionRunner : public OverriddenVisitor { @@ -199,6 +285,89 @@ class ExpressionRunner : public OverriddenVisitor { } #endif + // Suspend/resume support. + + // We save the value stack, so that we can stash it if we suspend. Normally, + // each instruction just calls visit() on its children, so the values are + // saved in those local stack frames in an efficient manner, but also we + // cannot scan those stack frames efficiently. Saving those values in + // this location (in addition to the normal place) does not add significant + // overhead (and we skip it entirely when not in a coroutine), and it is + // trivial to use when suspending. + // + // Each entry here is for an instruction in the stack of executing + // expressions, and contains all the values from its children that we have + // seen thus far. In other words, the invariant we preserve is this: when an + // instruction executes, the top of the stack contains the values of its + // children, e.g., + // + // (i32.add (A) (B)) + // + // After executing A and getting its value, valueStack looks like this: + // + // [[..], ..scopes for parents of the add.., [..], [value of A]] + // ^^^^^^^^^^^^ + // scope for the + // add, with one + // child so far + // + // Imagine that B then suspends. Then using the top of valueStack, we know the + // value of A, and can stash it. When we resume, we just apply that value, and + // proceed to execute B. + std::vector> valueStack; + + // RAII helper for |valueStack|: Adds a scope for an instruction, where the + // values of its children will be saved, and cleans it up later. + struct StackValueNoter { + ExpressionRunner* parent; + + StackValueNoter(ExpressionRunner* parent) : parent(parent) { + parent->valueStack.emplace_back(); + } + + ~StackValueNoter() { + assert(!parent->valueStack.empty()); + parent->valueStack.pop_back(); + } + }; + + // When we resume, we will apply the saved values from |valueStack| to this + // map, so we can "replay" them. Whenever visit() is asked to execute an + // expression that is in this map, then it will just return that value. + std::unordered_map restoredValuesMap; + + // The current continuation (this is set when executing it, resuming it, and + // suspending it, that is, both when executing normally and when + // unwinding/rewinding the stack). + std::shared_ptr currContinuation; + + // Set when we are resuming execution, that is, re-winding the stack. + bool resuming = false; + + // Add an entry to help us resume this continuation later. Instructions call + // this as we unwind. + void pushResumeEntry(const Literals& entry, const char* what) { + assert(currContinuation); +#if WASM_INTERPRETER_DEBUG + std::cout << indent() << "push resume entry [" << what << "]: " << entry + << "\n"; +#endif + currContinuation->resumeInfo.push_back(entry); + } + + // Fetch an entry as we resume. Instructions call this as we rewind. + Literals popResumeEntry(const char* what) { + assert(currContinuation); + assert(!currContinuation->resumeInfo.empty()); + auto entry = currContinuation->resumeInfo.back(); + currContinuation->resumeInfo.pop_back(); +#if WASM_INTERPRETER_DEBUG + std::cout << indent() << "pop resume entry [" << what << "]: " << entry + << "\n"; +#endif + return entry; + } + public: ExpressionRunner(Module* module = nullptr, Index maxDepth = NO_LIMIT, @@ -219,20 +388,107 @@ class ExpressionRunner : public OverriddenVisitor { hostLimit("interpreter recursion limit"); } - Flow ret = OverriddenVisitor::visit(curr); + // Execute the instruction. + Flow ret; + if (!currContinuation) { + // We are not in a continuation, so we cannot suspend/resume. Just execute + // normally. + ret = OverriddenVisitor::visit(curr); + } else { + // We may suspend/resume. + bool hasValue = false; + if (resuming) { + // Perhaps we have a known value to just apply here, without executing + // the instruction. + auto iter = restoredValuesMap.find(curr); + if (iter != restoredValuesMap.end()) { + ret = iter->second; + restoredValuesMap.erase(iter); + hasValue = true; + } + } + if (!hasValue) { + // We must execute this instruction. Set up the logic to note the values + // of children (we mainly need this for non-control flow structures, + // but even control flow ones must add a scope on the value stack, to + // not confuse the others). + StackValueNoter noter(this); + + if (Properties::isControlFlowStructure(curr)) { + // Control flow structures have their own logic for suspend/resume. + ret = OverriddenVisitor::visit(curr); + } else { + // A general non-control-flow instruction, with generic suspend/ + // resume support implemented here. + if (resuming) { + // Some children may have executed, and we have values stashed for + // them (see below where we suspend). Get those values, and populate + // |restoredValuesMap| so that when visit() is called on them, we + // can return those values rather than run them. + auto numEntry = popResumeEntry("num executed children"); + assert(numEntry.size() == 1); + auto num = numEntry[0].geti32(); + for (auto* child : ChildIterator(curr)) { + if (num == 0) { + // We have restored all the children that executed (any others + // were not suspended, and we have no values for them). + break; + } + --num; + auto value = popResumeEntry("child value"); + restoredValuesMap[child] = value; + } + } + + // We are ready to return the right values for the children, and + // can visit this instruction. + ret = OverriddenVisitor::visit(curr); + + if (ret.suspendTag) { + // We are suspending a continuation. All we need to do for a + // general instruction is stash the values of executed children + // from the value stack, and their number (as we may have + // suspended after executing only some). + assert(!valueStack.empty()); + auto& values = valueStack.back(); + auto num = values.size(); + while (!values.empty()) { + // TODO: std::move, &elsewhere? + pushResumeEntry(values.back(), "child value"); + values.pop_back(); + } + pushResumeEntry({Literal(int32_t(num))}, "num executed children"); + } + } + } + + // Outside the scope of StackValueNoter, the scope of our own child values + // has been removed (we don't need those values any more). What is now on + // the top of |valueStack| is the list of child values of our parent, + // which is the place our own value can go, if we have one (and if we are + // not suspending - suspending is handled above). + if (!ret.suspendTag && ret.getType().isConcrete()) { + assert(!valueStack.empty()); + auto& values = valueStack.back(); + values.push_back(ret.values); +#if WASM_INTERPRETER_DEBUG + std::cout << indent() << "added to valueStack: " << ret.values << '\n'; +#endif + } + } +#ifndef NDEBUG if (!ret.breaking()) { Type type = ret.getType(); if (type.isConcrete() || curr->type.isConcrete()) { -#ifndef NDEBUG if (!Type::isSubType(type, curr->type)) { Fatal() << "expected " << ModuleType(*module, curr->type) << ", seeing " << ModuleType(*module, type) << " from\n" << ModuleExpression(*module, curr) << '\n'; } -#endif } } +#endif depth--; #if WASM_INTERPRETER_DEBUG std::cout << indent() << "=> returning: " << ret << '\n'; @@ -253,6 +509,26 @@ class ExpressionRunner : public OverriddenVisitor { stack.push_back(curr); } + // Suspend/resume support. + auto suspend = [&](Index blockIndex) { + Literals entry; + // To return to the same place when we resume, we add an entry with two + // pieces of information: the index in the stack of blocks, and the index + // in the block. + entry.push_back(Literal(uint32_t(stack.size()))); + entry.push_back(Literal(uint32_t(blockIndex))); + pushResumeEntry(entry, "block"); + }; + Index blockIndex = 0; + if (resuming) { + auto entry = popResumeEntry("block"); + assert(entry.size() == 2); + Index stackIndex = entry[0].geti32(); + blockIndex = entry[1].geti32(); + assert(stack.size() > stackIndex); + stack.resize(stackIndex + 1); + } + Flow flow; auto* top = stack.back(); while (stack.size() > 0) { @@ -263,38 +539,80 @@ class ExpressionRunner : public OverriddenVisitor { continue; } auto& list = curr->list; - for (size_t i = 0; i < list.size(); i++) { + for (size_t i = blockIndex; i < list.size(); i++) { if (curr != top && i == 0) { // one of the block recursions we already handled continue; } flow = visit(list[i]); + if (flow.suspendTag) { + suspend(i); + return flow; + } if (flow.breaking()) { flow.clearIf(curr->name); break; } } + // If there was a value here, we only need it for the top iteration. + blockIndex = 0; } return flow; } Flow visitIf(If* curr) { - Flow flow = visit(curr->condition); - if (flow.breaking()) { - return flow; + // Suspend/resume support. + auto suspend = [&](Index resumeIndex) { + // To return to the same place when we resume, we stash an index: + // 0 - suspended in the condition + // 1 - suspended in the ifTrue arm + // 2 - suspended in the ifFalse arm + pushResumeEntry({Literal(int32_t(resumeIndex))}, "if"); + }; + Index resumeIndex = -1; + if (resuming) { + auto entry = popResumeEntry("if"); + assert(entry.size() == 1); + resumeIndex = entry[0].geti32(); } - if (flow.getSingleValue().geti32()) { - Flow flow = visit(curr->ifTrue); - if (!flow.breaking() && !curr->ifFalse) { - flow = Flow(); // if_else returns a value, but if does not + + Flow flow; + // The value of the if's condition (whether to take the ifTrue arm or not). + Index condition; + + if (resuming && resumeIndex > 0) { + // We are resuming into one of the arms. Just set the right condition. + condition = (resumeIndex == 1); + } else { + // We are executing normally, or we are resuming into the condition. + // Either way, enter the condition. + flow = visit(curr->condition); + if (flow.suspendTag) { + suspend(0); + return flow; } - return flow; + if (flow.breaking()) { + return flow; + } + condition = flow.getSingleValue().geti32(); } - if (curr->ifFalse) { - return visit(curr->ifFalse); + + if (condition) { + flow = visit(curr->ifTrue); + } else { + if (curr->ifFalse) { + flow = visit(curr->ifFalse); + } else { + flow = Flow(); + } } - return Flow(); + if (flow.suspendTag) { + suspend(condition ? 1 : 2); + return flow; + } + return flow; } Flow visitLoop(Loop* curr) { + // NB: No special support is need for suspend/resume. Index loopCount = 0; while (1) { Flow flow = visit(curr->body); @@ -2932,7 +3250,7 @@ class ModuleRunnerBase : public ExpressionRunner { } // call an exported function - Literals callExport(Name name, const Literals& arguments) { + Flow callExport(Name name, const Literals& arguments) { Export* export_ = wasm.getExportOrNull(name); if (!export_ || export_->kind != ExternalKind::Function) { externalInterface->trap("callExport not found"); @@ -2940,7 +3258,7 @@ class ModuleRunnerBase : public ExpressionRunner { return callFunction(*export_->getInternalName(), arguments); } - Literals callExport(Name name) { return callExport(name, Literals()); } + Flow callExport(Name name) { return callExport(name, Literals()); } // get an exported global Literals getExport(Name name) { @@ -4213,6 +4531,7 @@ class ModuleRunnerBase : public ExpressionRunner { return {}; } Flow visitTry(Try* curr) { + assert(!self()->resuming); // TODO try { return self()->visit(curr->body); } catch (const WasmException& e) { @@ -4261,6 +4580,7 @@ class ModuleRunnerBase : public ExpressionRunner { } } Flow visitTryTable(TryTable* curr) { + assert(!self()->resuming); // TODO try { return self()->visit(curr->body); } catch (const WasmException& e) { @@ -4300,11 +4620,105 @@ class ModuleRunnerBase : public ExpressionRunner { multiValues.pop_back(); return ret; } - Flow visitContNew(ContNew* curr) { return Flow(NONCONSTANT_FLOW); } + Flow visitContNew(ContNew* curr) { + auto funcFlow = self()->visit(curr->func); + if (funcFlow.breaking()) { + return funcFlow; + } + // Create a new continuation for the target function. + Name func = funcFlow.getSingleValue().getFunc(); + return Literal(std::make_shared(func, curr->type.getHeapType())); + } Flow visitContBind(ContBind* curr) { return Flow(NONCONSTANT_FLOW); } + Flow visitSuspend(Suspend* curr) { + if (self()->resuming) { + // This is a resume, so we have found our way back to where we + // suspended. + assert(curr == self()->currContinuation->resumeExpr); + // We finished resuming, and will continue from here normally. + self()->resuming = false; + // We should have consumed all the resumeInfo and all the + // restoredValues map. + assert(self()->currContinuation->resumeInfo.empty()); + assert(self()->restoredValuesMap.empty()); + return Flow(); + } + + // We were not resuming, so this is a new suspend that we must execute. + Literals arguments; + Flow flow = self()->generateArguments(curr->operands, arguments); + if (flow.breaking()) { + return flow; + } - Flow visitSuspend(Suspend* curr) { return Flow(NONCONSTANT_FLOW); } - Flow visitResume(Resume* curr) { return Flow(NONCONSTANT_FLOW); } + // Copy the continuation (the old one cannot be resumed again). Note that no + // old one may exist, in which case we still emit a continuation, but it is + // meaningless (it will error when it reaches the host). + auto old = self()->currContinuation; + assert(!old || old->executed); + auto new_ = std::make_shared(old ? old->func : Name(), + old ? old->type : HeapType::none); + // Switch to the new continuation, so that as we unwind, we will save the + // information we need to resume it later in the proper place. + self()->currContinuation = new_; + // We will resume from this precise spot, when the new continuation is + // resumed. + new_->resumeExpr = curr; + return Flow(SUSPEND_FLOW, curr->tag, std::move(arguments)); + } + Flow visitResume(Resume* curr) { + Literals arguments; + Flow flow = self()->generateArguments(curr->operands, arguments); + if (flow.breaking()) { + return flow; + } + flow = self()->visit(curr->cont); + if (flow.breaking()) { + return flow; + } + + // Get and execute the continuation. + auto contData = flow.getSingleValue().getContData(); + if (contData->executed) { + trap("continuation already executed"); + } + contData->executed = true; + Name func = contData->func; + self()->currContinuation = contData; + if (contData->resumeExpr) { + // There is an expression to resume execution at, so this is not the first + // time we run this function. Mark us as resuming, until we reach that + // expression. + self()->resuming = true; + } +#if WASM_INTERPRETER_DEBUG + std::cout << self()->indent() << "resuming func " << func << '\n'; +#endif + Flow ret = callFunction(func, arguments); +#if WASM_INTERPRETER_DEBUG + std::cout << self()->indent() << "finished resuming, with " << ret << '\n'; +#endif + if (ret.suspendTag) { + // See if a suspension arrived that we support. + for (size_t i = 0; i < curr->handlerTags.size(); i++) { + auto handlerTag = curr->handlerTags[i]; + if (handlerTag == ret.suspendTag) { + // Switch the flow from suspending to branching. + ret.suspendTag = Name(); + ret.breakTo = curr->handlerBlocks[i]; + // Add the continuation as the final value being sent. + ret.values.push_back(Literal(self()->currContinuation)); + // We are not longer processing that continuation. + self()->currContinuation.reset(); + return ret; + } + } + // No handler worked out, keep propagating. + return ret; + } + // No suspension; all done. + return ret; + } Flow visitResumeThrow(ResumeThrow* curr) { return Flow(NONCONSTANT_FLOW); } Flow visitStackSwitch(StackSwitch* curr) { return Flow(NONCONSTANT_FLOW); } @@ -4356,7 +4770,7 @@ class ModuleRunnerBase : public ExpressionRunner { return value; } - Literals callFunction(Name name, Literals arguments) { + Flow callFunction(Name name, Literals arguments) { if (callDepth > maxDepth) { hostLimit("stack limit"); } @@ -4382,6 +4796,17 @@ class ModuleRunnerBase : public ExpressionRunner { FunctionScope scope(function, arguments, *self()); + if (self()->resuming) { + // Restore the local state (see below for the ordering, we push/pop). + for (Index i = 0; i < scope.locals.size(); i++) { + auto l = scope.locals.size() - 1 - i; + scope.locals[l] = self()->popResumeEntry("function"); + // Must have restored valid data. + assert(Type::isSubType(scope.locals[l].getType(), + function->getLocalType(l))); + } + } + #if WASM_INTERPRETER_DEBUG std::cout << self()->indent() << "entering " << function->name << '\n' << self()->indent() << " with arguments:\n"; @@ -4398,6 +4823,13 @@ class ModuleRunnerBase : public ExpressionRunner { << flow.values << '\n'; #endif + if (flow.suspendTag) { + // Save the local state. + for (auto& local : scope.locals) { + self()->pushResumeEntry(local, "function"); + } + } + if (flow.breakTo != RETURN_CALL_FLOW) { break; } @@ -4414,17 +4846,27 @@ class ModuleRunnerBase : public ExpressionRunner { throw NonconstantException(); } - // We cannot still be breaking, which would mean we missed our stop. - assert(!flow.breaking() || flow.breakTo == RETURN_FLOW); -#ifndef NDEBUG - auto type = flow.getType(); - if (!Type::isSubType(type, *resultType)) { - Fatal() << "calling " << name << " resulted in " << type - << " but the function type is " << *resultType << '\n'; + if (flow.breakTo == RETURN_FLOW) { + // We are no longer returning out of that function (but the value + // remains the same). + flow.breakTo = Name(); } + + if (flow.breakTo != SUSPEND_FLOW) { + // We are normally executing (not suspending), and therefore cannot still + // be breaking, which would mean we missed our stop. + assert(!flow.breaking() || flow.breakTo == RETURN_FLOW); +#ifndef NDEBUG + // In normal execution, the result is the expected one. + auto type = flow.getType(); + if (!Type::isSubType(type, *resultType)) { + Fatal() << "calling " << name << " resulted in " << type + << " but the function type is " << *resultType << '\n'; + } #endif + } - return flow.values; + return flow; } // The maximum call stack depth to evaluate into. diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index c9a1f29be25..f6f4c3ff316 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -14,18 +14,18 @@ * limitations under the License. */ -#include "literal.h" - #include #include #include "emscripten-optimizer/simple_ast.h" #include "fp16.h" #include "ir/bits.h" +#include "literal.h" #include "pretty_printing.h" #include "support/bits.h" #include "support/string.h" #include "support/utilities.h" +#include "wasm-interpreter.h" namespace wasm { @@ -91,6 +91,9 @@ Literal::Literal(std::shared_ptr exnData) assert(exnData); } +Literal::Literal(std::shared_ptr contData) + : contData(contData), type(contData->type, NonNullable, Exact) {} + Literal::Literal(std::string_view string) : gcData(nullptr), type(Type(HeapType::string, NonNullable)) { // TODO: we could in theory internalize strings @@ -140,6 +143,10 @@ Literal::Literal(const Literal& other) : type(other.type) { func = other.func; return; } + if (type.isContinuation()) { + new (&contData) std::shared_ptr(other.contData); + return; + } switch (heapType.getBasic(Unshared)) { case HeapType::i31: i32 = other.i32; @@ -182,6 +189,8 @@ Literal::~Literal() { gcData.~shared_ptr(); } else if (isExn()) { exnData.~shared_ptr(); + } else if (isContinuation()) { + contData.~shared_ptr(); } } @@ -337,6 +346,12 @@ std::shared_ptr Literal::getExnData() const { return exnData; } +std::shared_ptr Literal::getContData() const { + assert(isContinuation()); + assert(contData); + return contData; +} + Literal Literal::castToF32() { assert(type == Type::i32); Literal ret(Type::f32); @@ -694,6 +709,16 @@ std::ostream& operator<<(std::ostream& o, Literal literal) { } } else if (heapType.isSignature()) { o << "funcref(" << literal.getFunc() << ")"; + } else if (heapType.isContinuation()) { + auto data = literal.getContData(); + o << "cont(" << data->func << ' ' << data->type; + if (data->resumeExpr) { + o << " resumeExpr=" << getExpressionName(data->resumeExpr); + } + if (!data->resumeInfo.empty()) { + o << " |resumeInfo|=" << data->resumeInfo.size(); + } + o << " executed=" << data->executed << ')'; } else { assert(literal.isData()); auto data = literal.getGCData(); diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index 7a9157bc847..4628f41c4f8 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -27,6 +27,7 @@ namespace wasm { Name RETURN_FLOW("*return:)*"); Name RETURN_CALL_FLOW("*return-call:)*"); Name NONCONSTANT_FLOW("*nonconstant:)*"); +Name SUSPEND_FLOW("*suspend:)*"); namespace BinaryConsts::CustomSections { @@ -1526,10 +1527,17 @@ void Resume::finalize() { if (handleUnreachableOperands(this)) { return; } + if (cont->type.isNull()) { + // This will never be executed and the instruction will not be emitted. + // Model this with an uninhabitable cast type. + // TODO: This is not quite right yet. + type = cont->type.with(NonNullable); + return; + } - assert(this->cont->type.isContinuation()); + assert(cont->type.isContinuation()); const Signature& contSig = - this->cont->type.getHeapType().getContinuation().type.getSignature(); + cont->type.getHeapType().getContinuation().type.getSignature(); type = contSig.results; } @@ -1541,10 +1549,17 @@ void ResumeThrow::finalize() { if (handleUnreachableOperands(this)) { return; } + if (cont->type.isNull()) { + // This will never be executed and the instruction will not be emitted. + // Model this with an uninhabitable cast type. + // TODO: This is not quite right yet. + type = cont->type.with(NonNullable); + return; + } - assert(this->cont->type.isContinuation()); + assert(cont->type.isContinuation()); const Signature& contSig = - this->cont->type.getHeapType().getContinuation().type.getSignature(); + cont->type.getHeapType().getContinuation().type.getSignature(); type = contSig.results; } @@ -1556,10 +1571,17 @@ void StackSwitch::finalize() { if (handleUnreachableOperands(this)) { return; } + if (cont->type.isNull()) { + // This will never be executed and the instruction will not be emitted. + // Model this with an uninhabitable cast type. + // TODO: This is not quite right yet. + type = cont->type.with(NonNullable); + return; + } - assert(this->cont->type.isContinuation()); + assert(cont->type.isContinuation()); Type params = - this->cont->type.getHeapType().getContinuation().type.getSignature().params; + cont->type.getHeapType().getContinuation().type.getSignature().params; assert(params.size() > 0); Type cont = params[params.size() - 1]; assert(cont.isContinuation()); diff --git a/test/lit/exec/cont_simple.wast b/test/lit/exec/cont_simple.wast new file mode 100644 index 00000000000..7b6aedb2075 --- /dev/null +++ b/test/lit/exec/cont_simple.wast @@ -0,0 +1,555 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --output=fuzz-exec and should not be edited. + +;; RUN: foreach %s %t wasm-opt -all --fuzz-exec-before -q -o /dev/null 2>&1 | filecheck %s + +(module $state + (type $f (func)) + (type $k (cont $f)) + + (import "fuzzing-support" "log" (func $log (param i32))) + + (tag $more) + + (func $run (param $k (ref $k)) + ;; Run a coroutine, continuing to resume it until it is complete. + (call $log (i32.const 100)) ;; start + (loop $loop + (block $on (result (ref $k)) + (resume $k (on $more $on) + (local.get $k) + ) + (call $log (i32.const 300)) ;; stop + (return) + ) + (call $log (i32.const 200)) ;; continue + (local.set $k) + (br $loop) + ) + (unreachable) + ) + + ;; A coroutine with only control flow in a single basic block (no locals, no + ;; params, no branching, no value stack). When $run-block, below, runs this, + ;; the result should be to log -1, -2, -3 (with interleaved logging from + ;; $run itself, above, 100, 200, 200, 300). + (func $block + (call $log (i32.const -1)) + (suspend $more) + (call $log (i32.const -2)) + (suspend $more) + (call $log (i32.const -3)) + ) + + ;; CHECK: [fuzz-exec] calling run-block + ;; CHECK-NEXT: [LoggingExternalInterface logging 100] + ;; CHECK-NEXT: [LoggingExternalInterface logging -1] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -2] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -3] + ;; CHECK-NEXT: [LoggingExternalInterface logging 300] + (func $run-block (export "run-block") + (call $run + (cont.new $k (ref.func $block)) + ) + ) + + ;; Nested blocks, so when we suspend/resume we must traverse that stack + ;; properly. + (func $block-nested + (block $a + (call $log (i32.const -1)) + (suspend $more) + (block $b + (block $c + (call $log (i32.const -2)) + (suspend $more) + (call $log (i32.const -3)) + ) + (call $log (i32.const -4)) + ) + (suspend $more) + (call $log (i32.const -5)) + (suspend $more) + ) + (call $log (i32.const -6)) + ) + + ;; CHECK: [fuzz-exec] calling run-block-nested + ;; CHECK-NEXT: [LoggingExternalInterface logging 100] + ;; CHECK-NEXT: [LoggingExternalInterface logging -1] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -2] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -3] + ;; CHECK-NEXT: [LoggingExternalInterface logging -4] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -5] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -6] + ;; CHECK-NEXT: [LoggingExternalInterface logging 300] + (func $run-block-nested (export "run-block-nested") + (call $run + (cont.new $k (ref.func $block-nested)) + ) + ) + + ;; The local's state must be saved and restored. + (func $local + (local $x i32) + (local.set $x (i32.const 42)) + (suspend $more) + (call $log (local.get $x)) + (local.set $x (i32.const 1337)) + (suspend $more) + (call $log (local.get $x)) + ) + + ;; CHECK: [fuzz-exec] calling run-local + ;; CHECK-NEXT: [LoggingExternalInterface logging 100] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 42] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 1337] + ;; CHECK-NEXT: [LoggingExternalInterface logging 300] + (func $run-local (export "run-local") + (call $run + (cont.new $k (ref.func $local)) + ) + ) + + (func $multi-locals + (local $i32 i32) + (local $f64 f64) + (local.set $i32 (i32.const 42)) + (local.set $f64 (f64.const 3.14159)) + (suspend $more) + (call $log + (local.get $i32) + ) + (call $log + (i32.trunc_f64_s + (local.get $f64) + ) + ) + ) + + ;; CHECK: [fuzz-exec] calling run-multi-locals + ;; CHECK-NEXT: [LoggingExternalInterface logging 100] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 42] + ;; CHECK-NEXT: [LoggingExternalInterface logging 3] + ;; CHECK-NEXT: [LoggingExternalInterface logging 300] + (func $run-multi-locals (export "run-multi-locals") + (call $run + (cont.new $k (ref.func $multi-locals)) + ) + ) + + ;; This loop should suspend 4 times and log 3, 2, 1, 0. + (func $loop + (local $x i32) + (local.set $x (i32.const 4)) + (loop $loop + (local.set $x + (i32.sub + (local.get $x) + (i32.const 1) + ) + ) + (call $log (local.get $x)) + (suspend $more) + (br_if $loop + (local.get $x) + ) + ) + ) + + ;; CHECK: [fuzz-exec] calling run-loop + ;; CHECK-NEXT: [LoggingExternalInterface logging 100] + ;; CHECK-NEXT: [LoggingExternalInterface logging 3] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 2] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 1] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 0] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 300] + (func $run-loop (export "run-loop") + (call $run + (cont.new $k (ref.func $loop)) + ) + ) + + ;; We should log -1, -2, -3, -4 + (func $if + (local $x i32) + (if + (local.get $x) + (then + (unreachable) + ) + (else + ;; We should get here. + (call $log (i32.const -1)) + (local.set $x (i32.const 1)) + (suspend $more) + ;; A nested if. + (if + (local.get $x) + (then + ;; We should get here + (suspend $more) + (call $log (i32.const -2)) + ) + (else + (unreachable) + ) + ) + ) + ) + ;; If with one arm. + (if + (local.get $x) + (then + ;; We should get here. + (call $log (i32.const -3)) + (suspend $more) + (call $log (i32.const -4)) + ) + ) + (if + (i32.eqz + (local.get $x) + ) + (then + (unreachable) + ) + ) + ) + + ;; CHECK: [fuzz-exec] calling run-if + ;; CHECK-NEXT: [LoggingExternalInterface logging 100] + ;; CHECK-NEXT: [LoggingExternalInterface logging -1] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -2] + ;; CHECK-NEXT: [LoggingExternalInterface logging -3] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -4] + ;; CHECK-NEXT: [LoggingExternalInterface logging 300] + (func $run-if (export "run-if") + (call $run + (cont.new $k (ref.func $if)) + ) + ) + + ;; Suspend in the if's condition. + (func $if-condition + (if + (block (result i32) + (call $log (i32.const -1)) + (suspend $more) + (call $log (i32.const -2)) + (i32.const 1) + ) + (then + (call $log (i32.const -3)) + ) + ) + ) + + ;; CHECK: [fuzz-exec] calling run-if-condition + ;; CHECK-NEXT: [LoggingExternalInterface logging 100] + ;; CHECK-NEXT: [LoggingExternalInterface logging -1] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -2] + ;; CHECK-NEXT: [LoggingExternalInterface logging -3] + ;; CHECK-NEXT: [LoggingExternalInterface logging 300] + (func $run-if-condition (export "run-if-condition") + (call $run + (cont.new $k (ref.func $if-condition)) + ) + ) + + ;; Check that we properly stash things on the value stack. + (func $value-stack + ;; Suspend on the left. No value is actually saved on the stack, as we + ;; resume before we execute the right side. + (call $log + (i32.sub ;; 1 - 2 => -1 + (block (result i32) + (suspend $more) + (i32.const 1) + ) + (i32.const 2) + ) + ) + ;; On the right. Now we save the 2 when we suspend. + (call $log + (i32.sub ;; 2 - 4 => -2 + (i32.const 2) + (block (result i32) + (suspend $more) + (i32.const 4) + ) + ) + ) + ;; Both sides suspend. + (call $log + (i32.sub ;; 3 - 6 => -3 + (block (result i32) + (suspend $more) + (i32.const 3) + ) + (block (result i32) + (suspend $more) + (i32.const 6) + ) + ) + ) + ) + + ;; CHECK: [fuzz-exec] calling run-value-stack + ;; CHECK-NEXT: [LoggingExternalInterface logging 100] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -1] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -2] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -3] + ;; CHECK-NEXT: [LoggingExternalInterface logging 300] + (func $run-value-stack (export "run-value-stack") + (call $run + (cont.new $k (ref.func $value-stack)) + ) + ) + + (func $nested-unary + ;; Suspend at the top. + (call $log + (i32.eqz + (i32.eqz + (i32.eqz + (block (result i32) + (suspend $more) + (i32.const 1) + ) + ) + ) + ) + ) + ;; Suspend everywhere. + (call $log + (block (result i32) + (suspend $more) + (i32.eqz + (block (result i32) + (suspend $more) + (i32.eqz + (block (result i32) + (suspend $more) + (i32.eqz + (block (result i32) + (suspend $more) + (i32.const 0) + ) + ) + ) + ) + ) + ) + ) + ) + ) + + ;; CHECK: [fuzz-exec] calling run-nested-unary + ;; CHECK-NEXT: [LoggingExternalInterface logging 100] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 0] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 1] + ;; CHECK-NEXT: [LoggingExternalInterface logging 300] + (func $run-nested-unary (export "run-nested-unary") + (call $run + (cont.new $k (ref.func $nested-unary)) + ) + ) + + (func $nested-unary-more + (local $temp i32) + ;; Suspend before and after each operation. + (call $log + (block (result i32) + i32.const 0 + suspend $more + i32.eqz + suspend $more + i32.eqz + suspend $more + i32.eqz + suspend $more + ) + ) + ) + + ;; CHECK: [fuzz-exec] calling run-nested-unary-more + ;; CHECK-NEXT: [LoggingExternalInterface logging 100] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 1] + ;; CHECK-NEXT: [LoggingExternalInterface logging 300] + (func $run-nested-unary-more (export "run-nested-unary-more") + (call $run + (cont.new $k (ref.func $nested-unary-more)) + ) + ) + + (func $nested-binary + (local $temp i32) + ;; Both sides suspend, in different places. + (call $log ;; (2 + 1) - (4 + 2) => -3 + (i32.sub + (block (result i32) + (i32.add + (block (result i32) + (suspend $more) + (i32.const 2) + ) + (i32.const 1) + ) + ) + (block (result i32) + (suspend $more) + (i32.add + (i32.const 4) + (i32.const 2) + ) + ) + ) + ) + ;; Ditto, but with suspensions moved in the arms, and others on the + ;; outside. Also add 1. + (call $log + (block (result i32) + (suspend $more) + (local.set $temp + (i32.sub + (block (result i32) + (suspend $more) + (i32.add + (i32.const 3) + (i32.const 1) + ) + ) + (block (result i32) + (i32.add + (i32.const 4) + (block (result i32) + (suspend $more) + (i32.const 2) + ) + ) + ) + ) + ) + (suspend $more) + (local.get $temp) + ) + ) + ) + + ;; CHECK: [fuzz-exec] calling run-nested-binary + ;; CHECK-NEXT: [LoggingExternalInterface logging 100] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -3] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging -2] + ;; CHECK-NEXT: [LoggingExternalInterface logging 300] + (func $run-nested-binary (export "run-nested-binary") + (call $run + (cont.new $k (ref.func $nested-binary)) + ) + ) + + (func $trinary + ;; Suspend in one of the arms. + (call $log + (select + (block (result i32) + (suspend $more) + (i32.const 1) + ) + (i32.const 2) + (i32.const 3) + ) + ) + (call $log + (select + (i32.const 4) + (block (result i32) + (suspend $more) + (i32.const 5) + ) + (i32.const 6) + ) + ) + (call $log + (select + (i32.const 7) + (i32.const 8) + (block (result i32) + (suspend $more) + (i32.const 9) + ) + ) + ) + ;; Suspend in them all. + (call $log + (select + (block (result i32) + (suspend $more) + (i32.const 10) + ) + (block (result i32) + (suspend $more) + (i32.const 11) + ) + (block (result i32) + (suspend $more) + (i32.const 12) + ) + ) + ) + ) + + ;; CHECK: [fuzz-exec] calling run-trinary + ;; CHECK-NEXT: [LoggingExternalInterface logging 100] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 1] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 4] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 7] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 200] + ;; CHECK-NEXT: [LoggingExternalInterface logging 10] + ;; CHECK-NEXT: [LoggingExternalInterface logging 300] + (func $run-trinary (export "run-trinary") + (call $run + (cont.new $k (ref.func $trinary)) + ) + ) +) diff --git a/test/spec/cont.wast b/test/spec/cont.wast new file mode 100644 index 00000000000..952fcf82a0c --- /dev/null +++ b/test/spec/cont.wast @@ -0,0 +1,229 @@ +;; Unhandled tags & guards + +(module + (tag $exn) + (tag $e1) + (tag $e2) + + (type $f1 (func)) + (type $k1 (cont $f1)) + + (func $f1 (export "unhandled-1") + (suspend $e1) + ) + + (func (export "unhandled-2") + (resume $k1 (cont.new $k1 (ref.func $f1))) + ) + + (func (export "unhandled-3") + (block $h (result (ref $k1)) + (resume $k1 (on $e2 $h) (cont.new $k1 (ref.func $f1))) + (unreachable) + ) + (drop) + ) + + (func (export "handled") + (block $h (result (ref $k1)) + (resume $k1 (on $e1 $h) (cont.new $k1 (ref.func $f1))) + (unreachable) + ) + (drop) + ) + + (elem declare func $f2) + (func $f2 + (throw $exn) + ) + + (func (export "uncaught-1") + (block $h (result (ref $k1)) + (resume $k1 (on $e1 $h) (cont.new $k1 (ref.func $f2))) + (unreachable) + ) + (drop) + ) + + (func (export "uncaught-2") + (block $h (result (ref $k1)) + (resume $k1 (on $e1 $h) (cont.new $k1 (ref.func $f1))) + (unreachable) + ) + (resume_throw $k1 $exn) + ) + + (elem declare func $f3) + (func $f3 + (call $f4) + ) + (func $f4 + (suspend $e1) + ) + + (func (export "uncaught-3") + (block $h (result (ref $k1)) + (resume $k1 (on $e1 $h) (cont.new $k1 (ref.func $f3))) + (unreachable) + ) + (resume_throw $k1 $exn) + ) + + (elem declare func $r0 $r1) + (func $r0) + (func $r1 (suspend $e1) (suspend $e1)) + + (func $nl1 (param $k (ref $k1)) + (resume $k1 (local.get $k)) + (resume $k1 (local.get $k)) + ) + (func $nl2 (param $k (ref $k1)) + (block $h (result (ref $k1)) + (resume $k1 (on $e1 $h) (local.get $k)) + (unreachable) + ) + (resume $k1 (local.get $k)) + (unreachable) + ) + (func $nl3 (param $k (ref $k1)) + (local $k' (ref null $k1)) + (block $h1 (result (ref $k1)) + (resume $k1 (on $e1 $h1) (local.get $k)) + (unreachable) + ) + (local.set $k') + (block $h2 (result (ref $k1)) + (resume $k1 (on $e1 $h2) (local.get $k')) + (unreachable) + ) + (resume $k1 (local.get $k')) + (unreachable) + ) + (func $nl4 (param $k (ref $k1)) + (drop (cont.bind $k1 $k1 (local.get $k))) + (resume $k1 (local.get $k)) + ) + + (func (export "non-linear-1") + (call $nl1 (cont.new $k1 (ref.func $r0))) + ) + (func (export "non-linear-2") + (call $nl2 (cont.new $k1 (ref.func $r1))) + ) + (func (export "non-linear-3") + (call $nl3 (cont.new $k1 (ref.func $r1))) + ) + (func (export "non-linear-4") + (call $nl4 (cont.new $k1 (ref.func $r1))) + ) +) + +(assert_suspension (invoke "unhandled-1") "unhandled") +(assert_suspension (invoke "unhandled-2") "unhandled") +(assert_suspension (invoke "unhandled-3") "unhandled") +(assert_return (invoke "handled")) + +(assert_exception (invoke "uncaught-1")) +;; TODO: resume_throw (assert_exception (invoke "uncaught-2")) +;; TODO: resume_throw (assert_exception (invoke "uncaught-3")) + +(assert_trap (invoke "non-linear-1") "continuation already consumed") +(assert_trap (invoke "non-linear-2") "continuation already consumed") +(assert_trap (invoke "non-linear-3") "continuation already consumed") +;; TODO: cont.bind (assert_trap (invoke "non-linear-4") "continuation already consumed") + +(assert_invalid + (module + (type $ft (func)) + (func + (cont.new $ft (ref.null $ft)) + (drop))) + "non-continuation type 0") + +(assert_invalid + (module + (type $ft (func)) + (type $ct (cont $ft)) + (func + (resume $ft (ref.null $ct)) + (unreachable))) + "non-continuation type 0") + +(assert_invalid + (module + (type $ft (func)) + (type $ct (cont $ft)) + (tag $exn) + (func + (resume_throw $ft $exn (ref.null $ct)) + (unreachable))) + "non-continuation type 0") + +(assert_invalid + (module + (type $ft (func)) + (type $ct (cont $ft)) + (func + (cont.bind $ft $ct (ref.null $ct)) + (unreachable))) + "non-continuation type 0") + +(assert_invalid + (module + (type $ft (func)) + (type $ct (cont $ft)) + (func + (cont.bind $ct $ft (ref.null $ct)) + (unreachable))) + "non-continuation type 0") + +(assert_invalid + (module + (type $ft (func)) + (type $ct (cont $ft)) + (tag $foo) + (func + (block $on_foo (result (ref $ft)) + (resume $ct (on $foo $on_foo) (ref.null $ct)) + (unreachable) + ) + (drop))) + "non-continuation type 0") + +(assert_invalid + (module + (type $ft (func)) + (type $ct (cont $ft)) + (tag $foo) + (func + (block $on_foo (result (ref $ct) (ref $ft)) + (resume $ct (on $foo $on_foo) (ref.null $ct)) + (unreachable) + ) + (drop) + (drop))) + "non-continuation type 0") + +(assert_invalid + (module + (type $ct (cont $ct))) + "non-function type 0") + +(assert_invalid + (module + (rec + (type $s0 (struct (field (ref 0) (ref 1) (ref $s0) (ref $s1)))) + (type $s1 (struct (field (ref 0) (ref 1) (ref $s0) (ref $s1)))) + ) + (type $ct (cont $s0))) + "non-function type 0") + +(module + (rec + (type $f1 (func (param (ref $f2)))) + (type $f2 (func (param (ref $f1)))) + ) + (type $c1 (cont $f1)) + (type $c2 (cont $f2)) +) +