diff --git a/CHANGELOG.md b/CHANGELOG.md index 5637bb014d7..a5b616d6080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ Current Trunk - Reject non-natural alignment for atomic memory operations at parse time (#8962) - Note that fast-math mode can ignore the difference between negative zero and zero (like clang and gcc). (#9056) +- Add a new `--jspi-hooks` pass which wraps promising exports and suspending + imports with calls to a module-provided fiber lifecycle hook. v132 ---- diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index 5924e3b7e76..969fa0570ea 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -59,6 +59,7 @@ set(passes_SOURCES Intrinsics.cpp J2CLItableMerging.cpp J2CLOpts.cpp + JSPIHooks.cpp LegalizeJSInterface.cpp LimitSegments.cpp LLVMMemoryCopyFillLowering.cpp diff --git a/src/passes/JSPIHooks.cpp b/src/passes/JSPIHooks.cpp new file mode 100644 index 00000000000..32455399aa5 --- /dev/null +++ b/src/passes/JSPIHooks.cpp @@ -0,0 +1,471 @@ +/* + * Copyright 2026 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// Instruments the JSPI (JavaScript Promise Integration) boundary of a module +// with lifecycle hooks, so that the runtime linked into the module can observe +// when a stack-switching fiber is entered, exited, suspended and resumed. +// +// The hooks must run inside the fiber's own wasm frames, at the instruction +// immediately before/after the boundary call: a JS wrapper around a promising +// export or a suspending import only observes the transition a microtask +// later, during which another fiber may already have run. Hence this pass, +// which wraps: +// +// * every exported function matching the jspi-exports patterns (these are +// the exports the host wraps with WebAssembly.promising), with the ENTER +// event before and the EXIT event after the call, and +// * every imported function matching the jspi-imports patterns (those the +// host wraps with WebAssembly.Suspending), with the SUSPEND event before +// and the RESUME event after the call. +// +// The module must export the four hooks the events are delivered to: +// +// __jspi_enter: [] -> [i64 token] +// __jspi_exit: [i64 token, i32 error] -> [] +// __jspi_suspend: [] -> [i64 token] +// __jspi_resume: [i64 token, i32 error] -> [] +// +// token: opaque to the pass. The value returned by the "before" hook (enter, +// suspend) is kept in a wasm local, which JSPI preserves across the +// suspension, and passed to the matching "after" hook (exit, resume). +// The runtime decides what it means (Emscripten uses a pointer to its +// fiber record). +// error: 1 when the wrapped call threw, else 0. +// +// All policy lives in those functions; the wrappers only hold their state in +// locals. On an exceptional exit the "after" event is delivered with error=1 +// and the exception is then rethrown unchanged, whatever its tag (a JS +// exception or a wasm one). Traps are not exceptions and bypass the hook. +// +// The pass enables the exception handling and reference types features +// (try_table/exnref). Engines reject modules mixing the legacy and +// standardized exception handling instructions, so if the module already uses +// legacy try/catch the wrappers are emitted in that form too. +// +// Arguments (asyncify-imports style: comma or newline separated, '*' +// wildcards, @file response files): +// +// --pass-arg=jspi-imports@module.base,... patterns over import module.base +// --pass-arg=jspi-exports@name,... patterns over export names +// --pass-arg=jspi-dyncalls also export a wrapped trampoline +// __jspi_dyncall_(fptr, ...) +// for every function signature in +// the table, so that the host can +// make function pointers promising +// without bypassing the hooks +// --pass-arg=jspi-dyncall-sigs@sig,... additional trampoline signatures +// +// uses the Emscripten signature alphabet of asm_v_wasm.h getSig() +// (result then params; v i j f d), which the host uses to look the +// trampolines up at runtime. +// + +#include "asm_v_wasm.h" +#include "ir/element-utils.h" +#include "ir/find_all.h" +#include "ir/module-utils.h" +#include "ir/names.h" +#include "pass.h" +#include "support/file.h" +#include "support/insert_ordered.h" +#include "support/string.h" +#include "wasm-builder.h" +#include "wasm-traversal.h" +#include "wasm.h" + +namespace wasm { + +namespace { + +const Name ENTER("__jspi_enter"); +const Name EXIT("__jspi_exit"); +const Name SUSPEND("__jspi_suspend"); +const Name RESUME("__jspi_resume"); +const std::string PREFIX = "byn$jspi-hooks$"; +const Name ANY_LABEL("byn$jspi-hooks$any"); +const Name TRY_LABEL("byn$jspi-hooks$try"); + +struct JSPIHooks : public Pass { + // Imports become defined functions that call hooks and throw. + bool addsEffects() override { return true; } + // All added locals are numeric or nullable. + bool requiresNonNullableLocalFixups() override { return false; } + // New function bodies shift code offsets. + bool invalidatesDWARF() override { return true; } + + Module* module = nullptr; + Name enter, exit, suspend, resume; + bool legacyEH = false; + + void run(Module* module_) override { + module = module_; + + String::Split importPatterns(String::trim(read_possible_response_file( + getArgumentOrDefault("jspi-imports", ""))), + String::Split::NewLineOr(",")); + String::Split exportPatterns(String::trim(read_possible_response_file( + getArgumentOrDefault("jspi-exports", ""))), + String::Split::NewLineOr(",")); + + std::vector imports; + for (auto& func : module->functions) { + if (func->imported() && !isGenerated(func->name) && + matches(importPatterns, + func->module.toString() + '.' + func->base.toString())) { + imports.push_back(func.get()); + } + } + std::vector exports; + for (auto& ex : module->exports) { + if (ex->kind == ExternalKind::Function && + !isGenerated(*ex->getInternalName()) && + matches(exportPatterns, ex->name.toString())) { + exports.push_back(ex.get()); + } + } + Table* dynCallTable = nullptr; + String::Split dynCallSigs; + if (hasArgument("jspi-dyncalls")) { + for (auto& table : module->tables) { + if (table->type.isFunction()) { + dynCallTable = table.get(); + break; + } + } + dynCallSigs = String::Split( + String::trim(getArgumentOrDefault("jspi-dyncall-sigs", "")), + String::Split::NewLineOr(",")); + } + if (imports.empty() && exports.empty() && !dynCallTable) { + return; + } + + Signature beforeSig({}, Type::i64); + Signature afterSig({Type::i64, Type::i32}, Type::none); + enter = getHook(ENTER, beforeSig); + exit = getHook(EXIT, afterSig); + suspend = getHook(SUSPEND, beforeSig); + resume = getHook(RESUME, afterSig); + // The hook implementations are never wrapped, even when imported or + // exported under a matching name. + auto isHook = [&](Name name) { + return name == enter || name == exit || name == suspend || name == resume; + }; + std::erase_if(imports, [&](Function* f) { return isHook(f->name); }); + std::erase_if(exports, + [&](Export* ex) { return isHook(*ex->getInternalName()); }); + if (imports.empty() && exports.empty() && !dynCallTable) { + return; + } + + legacyEH = usesLegacyEH(); + module->features.enable(FeatureSet::ExceptionHandling | + FeatureSet::ReferenceTypes); + + if (!imports.empty()) { + std::unordered_set wrapped; + for (auto* import : imports) { + wrapImport(import); + wrapped.insert(import->name); + } + // The wrapped functions are now defined, so their exact types differ + // from the imports they replaced; refinalize references to them. + struct Refinalizer : public WalkerPass> { + bool isFunctionParallel() override { return true; } + std::unordered_set& wrapped; + Refinalizer(std::unordered_set& wrapped) : wrapped(wrapped) {} + std::unique_ptr create() override { + return std::make_unique(wrapped); + } + void visitRefFunc(RefFunc* curr) { + if (wrapped.count(curr->func)) { + curr->finalize(*getModule()); + } + } + }; + Refinalizer refinalizer(wrapped); + refinalizer.run(getPassRunner(), module); + refinalizer.runOnModuleCode(getPassRunner(), module); + } + std::unordered_map wrappers; + for (auto* ex : exports) { + auto* name = ex->getInternalName(); + auto [iter, inserted] = wrappers.insert({*name, Name()}); + if (inserted) { + iter->second = wrapExport(module->getFunction(*name)); + } + *name = iter->second; + } + if (dynCallTable) { + makeDynCalls(dynCallTable, dynCallSigs); + } + } + +private: + const Type exnref = Type(HeapType::exn, Nullable); + + static bool isGenerated(Name name) { + return name.startsWith(std::string_view(PREFIX)); + } + + static bool matches(const String::Split& patterns, const std::string& name) { + for (auto& pattern : patterns) { + if (String::wildcardMatch(pattern, name)) { + return true; + } + } + return false; + } + + Name getHook(Name name, Signature sig) { + auto* ex = module->getExportOrNull(name); + if (!ex || ex->kind != ExternalKind::Function) { + Fatal() << "jspi-hooks: module must export function " << name; + } + auto* func = module->getFunction(*ex->getInternalName()); + if (func->getSig() != sig) { + Fatal() << "jspi-hooks: export " << name << " has type " << func->getSig() + << " but " << sig << " is required"; + } + return func->name; + } + + bool usesLegacyEH() { + // {legacy, standardized} + ModuleUtils::ParallelFunctionAnalysis> analysis( + *module, [](Function* func, std::pair& found) { + if (!func->imported()) { + found = {!FindAll(func->body).list.empty(), + !FindAll(func->body).list.empty()}; + } + }); + bool legacy = false; + bool standard = false; + for (auto& [_, found] : analysis.map) { + legacy |= found.first; + standard |= found.second; + } + if (legacy && standard) { + Fatal() << "jspi-hooks: module mixes legacy and standardized exception " + "handling; run --translate-to-exnref first"; + } + return legacy; + } + + // Moves the import to a new function and turns the original function object + // into the wrapper, so every existing use (calls, ref.func, element + // segments, exports) reaches the wrapper without any reference rewriting. + void wrapImport(Function* import) { + auto raw = Builder::makeFunction( + Names::getValidFunctionName(*module, + PREFIX + "import$" + import->name.toString()), + import->type, + {}); + raw->module = import->module; + raw->base = import->base; + raw->hasExplicitName = true; + Name rawName = module->addFunction(std::move(raw))->name; + import->module = Name(); + import->base = Name(); + import->type = import->type.with(Exact); + makeWrapperBody(import, makeCall(import, rawName), suspend, resume); + } + + static Type sigType(char c, Type addressType) { + switch (c) { + case 'v': + return Type::none; + case 'i': + return Type::i32; + case 'j': + return Type::i64; + case 'f': + return Type::f32; + case 'd': + return Type::f64; + case 'p': + return addressType; + default: + Fatal() << "jspi-hooks: invalid signature character '" << c << "'"; + } + } + + void makeDynCalls(Table* table, const String::Split& sigs) { + InsertOrderedSet types; + for (auto& segment : module->elementSegments) { + if (segment->table != table->name) { + continue; + } + ElementUtils::iterElementSegmentFunctionNames( + segment.get(), [&](Name name, Index) { + types.insert(module->getFunction(name)->type.getHeapType()); + }); + } + for (auto& sigStr : sigs) { + if (sigStr.empty()) { + Fatal() << "jspi-hooks: empty signature in jspi-dyncall-sigs"; + } + std::vector params; + for (size_t i = 1; i < sigStr.size(); i++) { + params.push_back(sigType(sigStr[i], table->addressType)); + } + types.insert(HeapType( + Signature(Type(params), sigType(sigStr[0], table->addressType)))); + } + for (auto type : types) { + auto sig = type.getSignature(); + if (sig.results.isTuple() || !isJSSig(sig)) { + continue; + } + std::string sigStr = getSig(sig.results, sig.params); + Name name = std::string("__jspi_dyncall_") + sigStr; + if (module->getExportOrNull(name)) { + continue; + } + std::vector params{table->addressType}; + for (auto param : sig.params) { + params.push_back(param); + } + auto func = Builder::makeFunction( + Names::getValidFunctionName(*module, PREFIX + "dyncall$" + sigStr), + Signature(Type(params), sig.results), + {}); + func->hasExplicitName = true; + Builder builder(*module); + std::vector args; + for (Index i = 0; i < sig.params.size(); i++) { + args.push_back(builder.makeLocalGet(i + 1, sig.params[i])); + } + auto* call = builder.makeCallIndirect( + table->name, builder.makeLocalGet(0, table->addressType), args, type); + auto* added = module->addFunction(std::move(func)); + makeWrapperBody(added, call, enter, exit); + module->addExport( + Builder::makeExport(name, added->name, ExternalKind::Function)); + } + } + + static bool isJSSig(Signature sig) { + for (auto type : sig.results) { + if (!type.isNumber() || type == Type::v128) { + return false; + } + } + for (auto type : sig.params) { + if (!type.isNumber() || type == Type::v128) { + return false; + } + } + return true; + } + + Name wrapExport(Function* target) { + auto wrapper = Builder::makeFunction( + Names::getValidFunctionName(*module, + PREFIX + "export$" + target->name.toString()), + target->type.with(Exact), + {}); + wrapper->hasExplicitName = true; + auto* func = module->addFunction(std::move(wrapper)); + makeWrapperBody(func, makeCall(func, target->name), enter, exit); + return func->name; + } + + Expression* makeCall(Function* func, Name target) { + Builder builder(*module); + auto params = func->getParams(); + std::vector args; + for (Index i = 0; i < params.size(); i++) { + args.push_back(builder.makeLocalGet(i, params[i])); + } + return builder.makeCall(target, args, func->getResults()); + } + + // Standardized form ($before/$after are enter/exit or suspend/resume): + // + // (local.set $tok (call $before)) + // (local.set $exn + // (block $any (result exnref) + // (try_table (catch_all_ref $any) + // (local.set $r (call $target params...))) + // (call $after (local.get $tok) (i32.const 0)) + // (return (local.get $r)))) + // (call $after (local.get $tok) (i32.const 1)) + // (throw_ref (local.get $exn)) + // + // Legacy form: + // + // (local.set $tok (call $before)) + // (try $try + // (do (local.set $r (call $target params...))) + // (catch_all + // (call $after (local.get $tok) (i32.const 1)) + // (rethrow $try))) + // (call $after (local.get $tok) (i32.const 0)) + // (return (local.get $r)) + void + makeWrapperBody(Function* func, Expression* call, Name before, Name after) { + Builder builder(*module); + auto results = func->getResults(); + bool hasResult = results != Type::none; + Index tok = Builder::addVar(func, Type::i64); + Index result = hasResult ? Builder::addVar(func, results) : 0; + + if (hasResult) { + call = builder.makeLocalSet(result, call); + } + auto afterCall = [&](int error) { + return builder.makeCall(after, + {builder.makeLocalGet(tok, Type::i64), + builder.makeConst(int32_t(error))}, + Type::none); + }; + auto makeReturn = [&]() { + return builder.makeReturn( + hasResult ? builder.makeLocalGet(result, results) : nullptr); + }; + auto* beforeCall = + builder.makeLocalSet(tok, builder.makeCall(before, {}, Type::i64)); + + if (legacyEH) { + auto* catchAll = + builder.makeBlock({afterCall(1), builder.makeRethrow(TRY_LABEL)}); + auto* tryExpr = + builder.makeTry(TRY_LABEL, call, {}, {catchAll}, Type::none); + func->body = builder.makeBlock( + {beforeCall, tryExpr, afterCall(0), makeReturn()}, Type::unreachable); + return; + } + + Index exn = Builder::addVar(func, exnref); + auto* tryTable = builder.makeTryTable(call, {Name()}, {ANY_LABEL}, {true}); + auto* anyBlock = builder.makeBlock( + ANY_LABEL, {tryTable, afterCall(0), makeReturn()}, exnref); + func->body = builder.makeBlock( + {beforeCall, + builder.makeLocalSet(exn, anyBlock), + afterCall(1), + builder.makeThrowRef(builder.makeLocalGet(exn, exnref))}, + Type::unreachable); + } +}; + +} // anonymous namespace + +Pass* createJSPIHooksPass() { return new JSPIHooks(); } + +} // namespace wasm diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index d93bb0876cb..26d14ea0d3f 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -242,6 +242,9 @@ void PassRegistry::registerPasses() { registerPass("intrinsic-lowering", "lower away binaryen intrinsics", createIntrinsicLoweringPass); + registerPass("jspi-hooks", + "wrap the JSPI import/export boundary with lifecycle hooks", + createJSPIHooksPass); registerPass("legalize-js-interface", "legalizes i64 types on the import/export boundary", createLegalizeJSInterfacePass); diff --git a/src/passes/passes.h b/src/passes/passes.h index b6242cf5259..4ef29d782b8 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -76,6 +76,7 @@ Pass* createInliningOptimizingPass(); Pass* createJ2CLItableMergingPass(); Pass* createJ2CLOptsPass(); Pass* createLegalizeAndPruneJSInterfacePass(); +Pass* createJSPIHooksPass(); Pass* createLegalizeJSInterfacePass(); Pass* createLimitSegmentsPass(); Pass* createLocalCSEPass(); diff --git a/test/lit/d8/jspi-hooks-legacy.wast b/test/lit/d8/jspi-hooks-legacy.wast new file mode 100644 index 00000000000..d0277bb1f69 --- /dev/null +++ b/test/lit/d8/jspi-hooks-legacy.wast @@ -0,0 +1,105 @@ +;; Same as jspi-hooks.wast, but the module already uses legacy exception +;; handling, so the pass emits the legacy try/catch wrapper form; the engine +;; would reject a module mixing the two. The trace must be identical. + +;; REQUIRES: linux + +;; RUN: wasm-opt %s --enable-exception-handling --enable-reference-types --jspi-hooks --pass-arg=jspi-imports@env.susp --pass-arg=jspi-exports@main --pass-arg=jspi-dyncalls -o %t.wasm -q +;; RUN: v8 --wasm-staging %S/jspi-hooks.js -- %t.wasm | filecheck %s + +;; CHECK: async success: result=10 +;; CHECK-NEXT: ENTER#1 sid=1 | SUSPEND#1 sid=1 | RESUME#1 sid=1 | EXIT#1 sid=1 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: rejected import: threw Error (same object) "boom" +;; CHECK-NEXT: ENTER#2 sid=2 | SUSPEND#2 sid=2 | RESUME#2 err sid=2 | EXIT#2 err sid=2 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: throwing inner (wasm tag): threw WebAssembly.Exception +;; CHECK-NEXT: ENTER#3 sid=3 | EXIT#3 err sid=3 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: sync completion, no import: result=77 +;; CHECK-NEXT: ENTER#4 sid=4 | EXIT#4 sid=4 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: nested promising from plain import: result=99 +;; CHECK-NEXT: ENTER#5 sid=5 | nested-start sid=5 | ENTER#6 sid=6 | SUSPEND#6 sid=6 | nested-after sid=5 promise=true | EXIT#5 sid=5 | RESUME#6 sid=6 | EXIT#6 sid=6 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: import via call_indirect: result=10 +;; CHECK-NEXT: ENTER#7 sid=7 | SUSPEND#7 sid=7 | RESUME#7 sid=7 | EXIT#7 sid=7 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: plain export calling main internally: no events: result=77 +;; CHECK-NEXT: {{^ *$}} +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: suspending import outside any fiber (id 0): threw SuspendError "trying to suspend without WebAssembly.promising" +;; CHECK-NEXT: SUSPEND#0 sid=0 | RESUME#0 err sid=0 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: function pointer via dyncall trampoline: result=10 +;; CHECK-NEXT: ENTER#8 sid=8 | SUSPEND#8 sid=8 | RESUME#8 sid=8 | EXIT#8 sid=8 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: concurrent fibers: result=20 +;; CHECK-NEXT: ENTER#9 sid=9 | SUSPEND#9 sid=9 | ENTER#10 sid=10 | SUSPEND#10 sid=10 | RESUME#9 sid=9 | EXIT#9 sid=9 | RESUME#10 sid=10 | EXIT#10 sid=10 +;; CHECK-NEXT: sid after: 0 + +(module + (import "env" "susp" (func $susp (param i32) (result i32))) + (import "env" "log" (func $log (param i32 i32 i32))) + (import "env" "nested" (func $nested (param i32) (result i32))) + (tag $cpp (param i32)) + (global $cur (mut i32) (i32.const 0)) + (global $next (mut i32) (i32.const 0)) + (table $indirect funcref (elem $susp)) + + ;; host_ids[id]: the id of whoever entered or last resumed fiber id, restored + ;; when the fiber leaves at SUSPEND and EXIT (ids stay below 64 here). The + ;; token the wrappers carry from ENTER to EXIT and SUSPEND to RESUME is the + ;; fiber id. + (memory 1) + (func $host_id (param $id i32) (result i32) + (i32.load (i32.mul (local.get $id) (i32.const 4)))) + (func (export "__jspi_enter") (result i64) + ;; new id, remember who entered us + (global.set $next (i32.add (global.get $next) (i32.const 1))) + (i32.store (i32.mul (global.get $next) (i32.const 4)) (global.get $cur)) + (global.set $cur (global.get $next)) + (call $log (i32.const 0) (global.get $cur) (i32.const 0)) + (i64.extend_i32_u (global.get $cur))) + (func (export "__jspi_exit") (param $tok64 i64) (param $error i32) + ;; back to whoever entered or last resumed us + (local $tok i32) + (local.set $tok (i32.wrap_i64 (local.get $tok64))) + (call $log (i32.const 1) (local.get $tok) (local.get $error)) + (global.set $cur (call $host_id (local.get $tok)))) + (func (export "__jspi_suspend") (result i64) + ;; the current fiber leaves; its id is the token + (local $id i32) + (local.set $id (global.get $cur)) + (call $log (i32.const 2) (local.get $id) (i32.const 0)) + (if (local.get $id) + (then (global.set $cur (call $host_id (local.get $id))))) + (i64.extend_i32_u (local.get $id))) + (func (export "__jspi_resume") (param $tok64 i64) (param $error i32) + ;; remember who resumed us, become current + (local $tok i32) + (local.set $tok (i32.wrap_i64 (local.get $tok64))) + (if (local.get $tok) + (then (i32.store (i32.mul (local.get $tok) (i32.const 4)) (global.get $cur)))) + (global.set $cur (local.get $tok)) + (call $log (i32.const 3) (local.get $tok) (local.get $error))) + + (func (export "stack_id") (result i32) (global.get $cur)) + + (func $legacy_user (param $x i32) (result i32) + (try (result i32) + (do (call $inner (local.get $x))) + (catch $cpp (drop (pop i32)) (i32.const -1)))) + (func $inner (param $x i32) (result i32) + (if (i32.eq (local.get $x) (i32.const 7)) (then (return (i32.const 77)))) + (if (i32.eq (local.get $x) (i32.const 3)) (then (throw $cpp (i32.const 42)))) + (if (i32.eq (local.get $x) (i32.const 5)) (then (return (call $nested (local.get $x))))) + (if (i32.eq (local.get $x) (i32.const 6)) + (then (return (call_indirect $indirect (type $sig) (i32.const 1) (i32.const 0))))) + (call $susp (local.get $x))) + (type $sig (func (param i32) (result i32))) + + ;; promising export; also called internally by "plain" (must not get events there) + (func $main (export "main") (param $x i32) (result i32) (call $inner (local.get $x))) + (func (export "plain") (param $x i32) (result i32) (call $main (local.get $x))) +) diff --git a/test/lit/d8/jspi-hooks.js b/test/lit/d8/jspi-hooks.js new file mode 100644 index 00000000000..f312cac66d5 --- /dev/null +++ b/test/lit/d8/jspi-hooks.js @@ -0,0 +1,71 @@ +// Engine-level test harness for the jspi-hooks pass. Instantiates the wasm +// given on the command line with a JSPI host and logs the hook events the +// module reports through its "log" import as (event, id, error). +// +// The module is expected to implement the __jspi_enter/exit/suspend/resume +// exports itself (see jspi-hooks.wast) and to export stack_id. + +const binary = readbuffer(arguments[0]); +const names = ['ENTER', 'EXIT', 'SUSPEND', 'RESUME']; +const events = []; +const rejectWith = new Error('boom'); +let raw; +let exports; + +const imports = { + env: { + log: (ev, id, error) => { + events.push(`${names[ev]}#${id}${error ? ' err' : ''} sid=${raw.stack_id()}`); + }, + susp: new WebAssembly.Suspending(async (x) => { + if (x === 1) return 10; + if (x === 2) throw rejectWith; + return x; + }), + // A plain (non-suspending) import that synchronously re-enters a + // promising export from inside a fiber. + nested: (x) => { + events.push(`nested-start sid=${raw.stack_id()}`); + const p = exports.main(1); + events.push(`nested-after sid=${raw.stack_id()} promise=${p instanceof Promise}`); + return 99; + }, + }, +}; + +async function run(label, fn) { + events.length = 0; + let out; + try { + out = `result=${await fn()}`; + } catch (e) { + const kind = e instanceof WebAssembly.Exception ? 'WebAssembly.Exception' : e?.constructor?.name; + out = `threw ${kind}${e === rejectWith ? ' (same object)' : ''}${e?.message ? ` "${e.message}"` : ''}`; + } + print(`${label}: ${out}`); + print(` ${events.join(' | ')}`); + print(` sid after: ${raw.stack_id()}`); +} + +async function main() { + const { instance } = await WebAssembly.instantiate(binary, imports); + raw = instance.exports; + exports = { main: WebAssembly.promising(raw.main) }; + + await run('async success', () => exports.main(1)); + await run('rejected import', () => exports.main(2)); + await run('throwing inner (wasm tag)', () => exports.main(3)); + await run('sync completion, no import', () => exports.main(7)); + await run('nested promising from plain import', () => exports.main(5)); + await run('import via call_indirect', () => exports.main(6)); + await run('plain export calling main internally: no events', () => raw.plain(7)); + await run('suspending import outside any fiber (id 0)', () => raw.plain(4)); + await run('function pointer via dyncall trampoline', () => WebAssembly.promising(raw.__jspi_dyncall_ii)(0, 1)); + await run('concurrent fibers', async () => { + const a = exports.main(1); + const b = exports.main(1); + return (await a) + (await b); + }); +} + +main().catch((e) => { print(`harness error: ${e}\n${e.stack}`); }); diff --git a/test/lit/d8/jspi-hooks.wast b/test/lit/d8/jspi-hooks.wast new file mode 100644 index 00000000000..84e791f0bc5 --- /dev/null +++ b/test/lit/d8/jspi-hooks.wast @@ -0,0 +1,103 @@ +;; Engine-level test of the jspi-hooks pass: the wrapped module runs under +;; JSPI with the harness in jspi-hooks.js, which prints the event trace the +;; module's own hook implementations log. This module gets the +;; standardized try_table wrappers; jspi-hooks-legacy.wast is the same module +;; with a legacy try, so it gets the legacy wrappers. + +;; REQUIRES: linux + +;; RUN: wasm-opt %s --enable-exception-handling --enable-reference-types --jspi-hooks --pass-arg=jspi-imports@env.susp --pass-arg=jspi-exports@main --pass-arg=jspi-dyncalls -o %t.wasm -q +;; RUN: v8 --wasm-staging %S/jspi-hooks.js -- %t.wasm | filecheck %s + +;; CHECK: async success: result=10 +;; CHECK-NEXT: ENTER#1 sid=1 | SUSPEND#1 sid=1 | RESUME#1 sid=1 | EXIT#1 sid=1 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: rejected import: threw Error (same object) "boom" +;; CHECK-NEXT: ENTER#2 sid=2 | SUSPEND#2 sid=2 | RESUME#2 err sid=2 | EXIT#2 err sid=2 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: throwing inner (wasm tag): threw WebAssembly.Exception +;; CHECK-NEXT: ENTER#3 sid=3 | EXIT#3 err sid=3 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: sync completion, no import: result=77 +;; CHECK-NEXT: ENTER#4 sid=4 | EXIT#4 sid=4 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: nested promising from plain import: result=99 +;; CHECK-NEXT: ENTER#5 sid=5 | nested-start sid=5 | ENTER#6 sid=6 | SUSPEND#6 sid=6 | nested-after sid=5 promise=true | EXIT#5 sid=5 | RESUME#6 sid=6 | EXIT#6 sid=6 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: import via call_indirect: result=10 +;; CHECK-NEXT: ENTER#7 sid=7 | SUSPEND#7 sid=7 | RESUME#7 sid=7 | EXIT#7 sid=7 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: plain export calling main internally: no events: result=77 +;; CHECK-NEXT: {{^ *$}} +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: suspending import outside any fiber (id 0): threw SuspendError "trying to suspend without WebAssembly.promising" +;; CHECK-NEXT: SUSPEND#0 sid=0 | RESUME#0 err sid=0 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: function pointer via dyncall trampoline: result=10 +;; CHECK-NEXT: ENTER#8 sid=8 | SUSPEND#8 sid=8 | RESUME#8 sid=8 | EXIT#8 sid=8 +;; CHECK-NEXT: sid after: 0 +;; CHECK-NEXT: concurrent fibers: result=20 +;; CHECK-NEXT: ENTER#9 sid=9 | SUSPEND#9 sid=9 | ENTER#10 sid=10 | SUSPEND#10 sid=10 | RESUME#9 sid=9 | EXIT#9 sid=9 | RESUME#10 sid=10 | EXIT#10 sid=10 +;; CHECK-NEXT: sid after: 0 + +(module + (import "env" "susp" (func $susp (param i32) (result i32))) + (import "env" "log" (func $log (param i32 i32 i32))) + (import "env" "nested" (func $nested (param i32) (result i32))) + (tag $cpp (param i32)) + (global $cur (mut i32) (i32.const 0)) + (global $next (mut i32) (i32.const 0)) + (table $indirect funcref (elem $susp)) + + ;; host_ids[id]: the id of whoever entered or last resumed fiber id, restored + ;; when the fiber leaves at SUSPEND and EXIT (ids stay below 64 here). The + ;; token the wrappers carry from ENTER to EXIT and SUSPEND to RESUME is the + ;; fiber id. + (memory 1) + (func $host_id (param $id i32) (result i32) + (i32.load (i32.mul (local.get $id) (i32.const 4)))) + (func (export "__jspi_enter") (result i64) + ;; new id, remember who entered us + (global.set $next (i32.add (global.get $next) (i32.const 1))) + (i32.store (i32.mul (global.get $next) (i32.const 4)) (global.get $cur)) + (global.set $cur (global.get $next)) + (call $log (i32.const 0) (global.get $cur) (i32.const 0)) + (i64.extend_i32_u (global.get $cur))) + (func (export "__jspi_exit") (param $tok64 i64) (param $error i32) + ;; back to whoever entered or last resumed us + (local $tok i32) + (local.set $tok (i32.wrap_i64 (local.get $tok64))) + (call $log (i32.const 1) (local.get $tok) (local.get $error)) + (global.set $cur (call $host_id (local.get $tok)))) + (func (export "__jspi_suspend") (result i64) + ;; the current fiber leaves; its id is the token + (local $id i32) + (local.set $id (global.get $cur)) + (call $log (i32.const 2) (local.get $id) (i32.const 0)) + (if (local.get $id) + (then (global.set $cur (call $host_id (local.get $id))))) + (i64.extend_i32_u (local.get $id))) + (func (export "__jspi_resume") (param $tok64 i64) (param $error i32) + ;; remember who resumed us, become current + (local $tok i32) + (local.set $tok (i32.wrap_i64 (local.get $tok64))) + (if (local.get $tok) + (then (i32.store (i32.mul (local.get $tok) (i32.const 4)) (global.get $cur)))) + (global.set $cur (local.get $tok)) + (call $log (i32.const 3) (local.get $tok) (local.get $error))) + + (func (export "stack_id") (result i32) (global.get $cur)) + + (func $inner (param $x i32) (result i32) + (if (i32.eq (local.get $x) (i32.const 7)) (then (return (i32.const 77)))) + (if (i32.eq (local.get $x) (i32.const 3)) (then (throw $cpp (i32.const 42)))) + (if (i32.eq (local.get $x) (i32.const 5)) (then (return (call $nested (local.get $x))))) + (if (i32.eq (local.get $x) (i32.const 6)) + (then (return (call_indirect $indirect (type $sig) (i32.const 1) (i32.const 0))))) + (call $susp (local.get $x))) + (type $sig (func (param i32) (result i32))) + + ;; promising export; also called internally by "plain" (must not get events there) + (func $main (export "main") (param $x i32) (result i32) (call $inner (local.get $x))) + (func (export "plain") (param $x i32) (result i32) (call $main (local.get $x))) +) diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index d6b80d5bf89..891a2ff7a24 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test @@ -230,6 +230,9 @@ ;; CHECK-EMPTY: ;; CHECK-NEXT: --intrinsic-lowering lower away binaryen intrinsics ;; CHECK-EMPTY: +;; CHECK-NEXT: --jspi-hooks wrap the JSPI import/export +;; CHECK-NEXT: boundary with lifecycle hooks +;; CHECK-EMPTY: ;; CHECK-NEXT: --legalize-and-prune-js-interface legalizes the import/export ;; CHECK-NEXT: boundary and prunes when needed ;; CHECK-EMPTY: diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index ff37a52d159..9c259603afb 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -266,6 +266,9 @@ ;; CHECK-EMPTY: ;; CHECK-NEXT: --intrinsic-lowering lower away binaryen intrinsics ;; CHECK-EMPTY: +;; CHECK-NEXT: --jspi-hooks wrap the JSPI import/export +;; CHECK-NEXT: boundary with lifecycle hooks +;; CHECK-EMPTY: ;; CHECK-NEXT: --legalize-and-prune-js-interface legalizes the import/export ;; CHECK-NEXT: boundary and prunes when needed ;; CHECK-EMPTY: diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index 33c963860e1..0b62c06f47e 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -194,6 +194,9 @@ ;; CHECK-EMPTY: ;; CHECK-NEXT: --intrinsic-lowering lower away binaryen intrinsics ;; CHECK-EMPTY: +;; CHECK-NEXT: --jspi-hooks wrap the JSPI import/export +;; CHECK-NEXT: boundary with lifecycle hooks +;; CHECK-EMPTY: ;; CHECK-NEXT: --legalize-and-prune-js-interface legalizes the import/export ;; CHECK-NEXT: boundary and prunes when needed ;; CHECK-EMPTY: diff --git a/test/lit/passes/jspi-hooks-bad-dyncall-sig.wast b/test/lit/passes/jspi-hooks-bad-dyncall-sig.wast new file mode 100644 index 00000000000..113ded05e39 --- /dev/null +++ b/test/lit/passes/jspi-hooks-bad-dyncall-sig.wast @@ -0,0 +1,11 @@ +;; RUN: not wasm-opt %s --enable-reference-types --jspi-hooks --pass-arg=jspi-dyncalls --pass-arg=jspi-dyncall-sigs@ix 2>&1 | filecheck %s + +;; CHECK: jspi-hooks: invalid signature character 'x' + +(module + (table $t 1 funcref) + (func $enter (export "__jspi_enter") (result i64) (i64.const 0)) + (func $exit (export "__jspi_exit") (param i64 i32)) + (func $suspend (export "__jspi_suspend") (result i64) (i64.const 0)) + (func $resume (export "__jspi_resume") (param i64 i32)) +) diff --git a/test/lit/passes/jspi-hooks-bad-hook-type.wast b/test/lit/passes/jspi-hooks-bad-hook-type.wast new file mode 100644 index 00000000000..a81c552eb89 --- /dev/null +++ b/test/lit/passes/jspi-hooks-bad-hook-type.wast @@ -0,0 +1,9 @@ +;; RUN: not wasm-opt %s --enable-reference-types --jspi-hooks --pass-arg=jspi-exports@main 2>&1 | filecheck %s + +;; CHECK: jspi-hooks: export __jspi_exit has type (func (param i32 i32)) but (func (param i64 i32)) is required + +(module + (func (export "__jspi_enter") (result i64) (i64.const 0)) + (func (export "__jspi_exit") (param i32 i32)) + (func (export "main")) +) diff --git a/test/lit/passes/jspi-hooks-dyncalls.wast b/test/lit/passes/jspi-hooks-dyncalls.wast new file mode 100644 index 00000000000..2b0a58ff164 --- /dev/null +++ b/test/lit/passes/jspi-hooks-dyncalls.wast @@ -0,0 +1,180 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: wasm-opt %s --enable-reference-types --enable-exception-handling --jspi-hooks --pass-arg=jspi-dyncalls --pass-arg=jspi-dyncall-sigs@vf,ii -S -o - | filecheck %s +;; RUN: wasm-opt %s --enable-reference-types --enable-exception-handling --jspi-hooks --pass-arg=jspi-dyncalls --pass-arg=jspi-dyncall-sigs@vf,ii --roundtrip -O2 -o /dev/null + +;; With jspi-dyncalls a wrapped __jspi_dyncall_ trampoline is exported for +;; each function signature in the (first funcref) table plus the explicitly +;; listed ones (vf here; ii is listed and also in the table), skipping +;; signatures that cannot be expressed in the JS sig alphabet (reference +;; types), and deduplicating by signature. The externref table is ignored. +(module + ;; CHECK: (type $ii (func (param i32) (result i32))) + (type $ii (func (param i32) (result i32))) + + ;; CHECK: (type $1 (func (result i64))) + + ;; CHECK: (type $2 (func (param i64 i32))) + + ;; CHECK: (type $3 (func (param i64 f64))) + + ;; CHECK: (type $4 (func (param externref))) + + ;; CHECK: (type $5 (func (param i32 i32) (result i32))) + + ;; CHECK: (type $6 (func (param i32 i64 f64))) + + ;; CHECK: (type $7 (func (param i32 f32))) + + ;; CHECK: (type $8 (func (param f32))) + + ;; CHECK: (table $refs 1 externref) + (table $refs 1 externref) + ;; CHECK: (table $t 4 funcref) + (table $t 4 funcref) + (elem (table $t) (i32.const 0) func $a $b $c $d) + + ;; CHECK: (elem $0 (table $t) (i32.const 0) func $a $b $c $d) + + ;; CHECK: (export "__jspi_enter" (func $enter)) + + ;; CHECK: (export "__jspi_exit" (func $exit)) + + ;; CHECK: (export "__jspi_suspend" (func $suspend)) + + ;; CHECK: (export "__jspi_resume" (func $resume)) + + ;; CHECK: (export "__jspi_dyncall_ii" (func $byn$jspi-hooks$dyncall$ii)) + + ;; CHECK: (export "__jspi_dyncall_vjd" (func $byn$jspi-hooks$dyncall$vjd)) + + ;; CHECK: (export "__jspi_dyncall_vf" (func $byn$jspi-hooks$dyncall$vf)) + + ;; CHECK: (func $enter (result i64) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $enter (export "__jspi_enter") (result i64) (i64.const 0)) + ;; CHECK: (func $exit (param $0 i64) (param $1 i32) + ;; CHECK-NEXT: ) + (func $exit (export "__jspi_exit") (param i64 i32)) + ;; CHECK: (func $suspend (result i64) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $suspend (export "__jspi_suspend") (result i64) (i64.const 0)) + ;; CHECK: (func $resume (param $0 i64) (param $1 i32) + ;; CHECK-NEXT: ) + (func $resume (export "__jspi_resume") (param i64 i32)) + ;; CHECK: (func $a (param $0 i32) (result i32) + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + (func $a (type $ii) (local.get 0)) + ;; CHECK: (func $b (param $0 i32) (result i32) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $b (type $ii) (i32.add (local.get 0) (i32.const 1))) + ;; CHECK: (func $c (param $0 i64) (param $1 f64) + ;; CHECK-NEXT: ) + (func $c (param i64 f64)) + ;; CHECK: (func $d (param $0 externref) + ;; CHECK-NEXT: ) + (func $d (param externref)) +) + +;; CHECK: (func $byn$jspi-hooks$dyncall$ii (param $0 i32) (param $1 i32) (result i32) +;; CHECK-NEXT: (local $2 i64) +;; CHECK-NEXT: (local $3 i32) +;; CHECK-NEXT: (local $4 exnref) +;; CHECK-NEXT: (local.set $2 +;; CHECK-NEXT: (call $enter) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.set $4 +;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) +;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) +;; CHECK-NEXT: (local.set $3 +;; CHECK-NEXT: (call_indirect $t (type $ii) +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $2) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (return +;; CHECK-NEXT: (local.get $3) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $2) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (throw_ref +;; CHECK-NEXT: (local.get $4) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $byn$jspi-hooks$dyncall$vjd (param $0 i32) (param $1 i64) (param $2 f64) +;; CHECK-NEXT: (local $3 i64) +;; CHECK-NEXT: (local $4 exnref) +;; CHECK-NEXT: (local.set $3 +;; CHECK-NEXT: (call $enter) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.set $4 +;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) +;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) +;; CHECK-NEXT: (call_indirect $t (type $3) +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: (local.get $2) +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $3) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (return) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $3) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (throw_ref +;; CHECK-NEXT: (local.get $4) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $byn$jspi-hooks$dyncall$vf (param $0 i32) (param $1 f32) +;; CHECK-NEXT: (local $2 i64) +;; CHECK-NEXT: (local $3 exnref) +;; CHECK-NEXT: (local.set $2 +;; CHECK-NEXT: (call $enter) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.set $3 +;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) +;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) +;; CHECK-NEXT: (call_indirect $t (type $8) +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $2) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (return) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $2) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (throw_ref +;; CHECK-NEXT: (local.get $3) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) diff --git a/test/lit/passes/jspi-hooks-features.wast b/test/lit/passes/jspi-hooks-features.wast new file mode 100644 index 00000000000..f565f051f81 --- /dev/null +++ b/test/lit/passes/jspi-hooks-features.wast @@ -0,0 +1,18 @@ +;; RUN: wasm-opt %s --enable-reference-types --print-features --print --jspi-hooks --pass-arg=jspi-exports@main --print-features | filecheck %s + +;; The pass enables exception handling and reference types (for the exnref +;; local in the wrappers). + +;; CHECK: --enable-reference-types +;; CHECK-NOT: --enable-exception-handling +;; CHECK: (module +;; CHECK: --enable-exception-handling +;; CHECK: --enable-reference-types + +(module + (func (export "__jspi_enter") (result i64) (i64.const 0)) + (func (export "__jspi_exit") (param i64 i32)) + (func (export "__jspi_suspend") (result i64) (i64.const 0)) + (func (export "__jspi_resume") (param i64 i32)) + (func (export "main")) +) diff --git a/test/lit/passes/jspi-hooks-legacy-eh.wast b/test/lit/passes/jspi-hooks-legacy-eh.wast new file mode 100644 index 00000000000..73cd42e587c --- /dev/null +++ b/test/lit/passes/jspi-hooks-legacy-eh.wast @@ -0,0 +1,132 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: wasm-opt %s --enable-reference-types --enable-exception-handling --jspi-hooks --pass-arg=jspi-imports@env.sleep --pass-arg=jspi-exports@main -S -o - | filecheck %s +;; RUN: wasm-opt %s --enable-reference-types --enable-exception-handling --jspi-hooks --pass-arg=jspi-imports@env.sleep --pass-arg=jspi-exports@main --roundtrip -O2 -o /dev/null +;; RUN: wasm-opt %s --enable-reference-types --enable-exception-handling --jspi-hooks --pass-arg=jspi-imports@env.sleep --pass-arg=jspi-exports@main -g --roundtrip -o /dev/null + +;; A module that already uses legacy exception handling gets legacy-form +;; wrappers (try/catch/catch_all/rethrow, no exnref), since engines reject +;; modules mixing legacy and standardized EH instructions. +(module + (import "env" "sleep" (func $sleep (param i32) (result i32))) + + ;; CHECK: (type $0 (func (param i32) (result i32))) + + ;; CHECK: (type $1 (func (result i64))) + + ;; CHECK: (type $2 (func (param i64 i32))) + + ;; CHECK: (type $3 (func (param i32))) + + ;; CHECK: (import "env" "sleep" (func $byn$jspi-hooks$import$sleep (param i32) (result i32))) + + ;; CHECK: (tag $cpp (type $3) (param i32)) + (tag $cpp (param i32)) + + ;; CHECK: (export "__jspi_enter" (func $enter)) + + ;; CHECK: (export "__jspi_exit" (func $exit)) + + ;; CHECK: (export "__jspi_suspend" (func $suspend)) + + ;; CHECK: (export "__jspi_resume" (func $resume)) + + ;; CHECK: (export "main" (func $byn$jspi-hooks$export$main)) + + ;; CHECK: (func $sleep (param $0 i32) (result i32) + ;; CHECK-NEXT: (local $1 i64) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (call $suspend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (try $byn$jspi-hooks$try + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (call $byn$jspi-hooks$import$sleep + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch_all + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (rethrow $byn$jspi-hooks$try) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + + ;; CHECK: (func $enter (result i64) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $enter (export "__jspi_enter") (result i64) (i64.const 0)) + ;; CHECK: (func $exit (param $0 i64) (param $1 i32) + ;; CHECK-NEXT: ) + (func $exit (export "__jspi_exit") (param i64 i32)) + ;; CHECK: (func $suspend (result i64) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $suspend (export "__jspi_suspend") (result i64) (i64.const 0)) + ;; CHECK: (func $resume (param $0 i64) (param $1 i32) + ;; CHECK-NEXT: ) + (func $resume (export "__jspi_resume") (param i64 i32)) + ;; CHECK: (func $main (param $x i32) (result i32) + ;; CHECK-NEXT: (try (result i32) + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (call $sleep + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $cpp + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (pop i32) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const -1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $main (export "main") (param $x i32) (result i32) + (try (result i32) + (do (call $sleep (local.get $x))) + (catch $cpp (drop (pop i32)) (i32.const -1)) + ) + ) +) +;; CHECK: (func $byn$jspi-hooks$export$main (param $0 i32) (result i32) +;; CHECK-NEXT: (local $1 i64) +;; CHECK-NEXT: (local $2 i32) +;; CHECK-NEXT: (local.set $1 +;; CHECK-NEXT: (call $enter) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (try $byn$jspi-hooks$try +;; CHECK-NEXT: (do +;; CHECK-NEXT: (local.set $2 +;; CHECK-NEXT: (call $main +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (catch_all +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (rethrow $byn$jspi-hooks$try) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (return +;; CHECK-NEXT: (local.get $2) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) diff --git a/test/lit/passes/jspi-hooks-missing-hook.wast b/test/lit/passes/jspi-hooks-missing-hook.wast new file mode 100644 index 00000000000..ea0dd7c7e1d --- /dev/null +++ b/test/lit/passes/jspi-hooks-missing-hook.wast @@ -0,0 +1,7 @@ +;; RUN: not wasm-opt %s --enable-reference-types --jspi-hooks --pass-arg=jspi-exports@main 2>&1 | filecheck %s + +;; CHECK: jspi-hooks: module must export function __jspi_enter + +(module + (func (export "main")) +) diff --git a/test/lit/passes/jspi-hooks-mixed-eh.wast b/test/lit/passes/jspi-hooks-mixed-eh.wast new file mode 100644 index 00000000000..19af4731316 --- /dev/null +++ b/test/lit/passes/jspi-hooks-mixed-eh.wast @@ -0,0 +1,14 @@ +;; RUN: not wasm-opt %s --enable-reference-types --enable-exception-handling --jspi-hooks --pass-arg=jspi-exports@main 2>&1 | filecheck %s + +;; CHECK: jspi-hooks: module mixes legacy and standardized exception handling; run --translate-to-exnref first + +(module + (tag $t) + (func $enter (export "__jspi_enter") (result i64) (i64.const 0)) + (func $exit (export "__jspi_exit") (param i64 i32)) + (func $suspend (export "__jspi_suspend") (result i64) (i64.const 0)) + (func $resume (export "__jspi_resume") (param i64 i32)) + (func $legacy (try (do (nop)) (catch_all (nop)))) + (func $standard (block $b (try_table (catch_all $b) (nop)))) + (func $main (export "main")) +) diff --git a/test/lit/passes/jspi-hooks-multivalue.wast b/test/lit/passes/jspi-hooks-multivalue.wast new file mode 100644 index 00000000000..cf0f2e76bdd --- /dev/null +++ b/test/lit/passes/jspi-hooks-multivalue.wast @@ -0,0 +1,117 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: wasm-opt %s --enable-multivalue --enable-reference-types --enable-exception-handling --jspi-hooks --pass-arg=jspi-imports@env.pair --pass-arg=jspi-exports@main -S -o - | filecheck %s +;; RUN: wasm-opt %s --enable-multivalue --enable-reference-types --enable-exception-handling --jspi-hooks --pass-arg=jspi-imports@env.pair --pass-arg=jspi-exports@main --roundtrip -O2 -o /dev/null + +;; Multivalue results are held in a tuple local. +(module + (import "env" "pair" (func $pair (param i32) (result i32 i64))) + + ;; CHECK: (type $0 (func (param i32) (result i32 i64))) + + ;; CHECK: (type $1 (func (result i64))) + + ;; CHECK: (type $2 (func (param i64 i32))) + + ;; CHECK: (import "env" "pair" (func $byn$jspi-hooks$import$pair (param i32) (result i32 i64))) + + ;; CHECK: (export "__jspi_enter" (func $enter)) + + ;; CHECK: (export "__jspi_exit" (func $exit)) + + ;; CHECK: (export "__jspi_suspend" (func $suspend)) + + ;; CHECK: (export "__jspi_resume" (func $resume)) + + ;; CHECK: (export "main" (func $byn$jspi-hooks$export$main)) + + ;; CHECK: (func $pair (param $0 i32) (result i32 i64) + ;; CHECK-NEXT: (local $1 i64) + ;; CHECK-NEXT: (local $2 (tuple i32 i64)) + ;; CHECK-NEXT: (local $3 exnref) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (call $suspend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) + ;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (call $byn$jspi-hooks$import$pair + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (throw_ref + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + + ;; CHECK: (func $enter (result i64) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $enter (export "__jspi_enter") (result i64) (i64.const 0)) + ;; CHECK: (func $exit (param $0 i64) (param $1 i32) + ;; CHECK-NEXT: ) + (func $exit (export "__jspi_exit") (param i64 i32)) + ;; CHECK: (func $suspend (result i64) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $suspend (export "__jspi_suspend") (result i64) (i64.const 0)) + ;; CHECK: (func $resume (param $0 i64) (param $1 i32) + ;; CHECK-NEXT: ) + (func $resume (export "__jspi_resume") (param i64 i32)) + ;; CHECK: (func $main (param $x i32) (result i32 i64) + ;; CHECK-NEXT: (call $pair + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $main (export "main") (param $x i32) (result i32 i64) + (call $pair (local.get $x)) + ) +) +;; CHECK: (func $byn$jspi-hooks$export$main (param $0 i32) (result i32 i64) +;; CHECK-NEXT: (local $1 i64) +;; CHECK-NEXT: (local $2 (tuple i32 i64)) +;; CHECK-NEXT: (local $3 exnref) +;; CHECK-NEXT: (local.set $1 +;; CHECK-NEXT: (call $enter) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.set $3 +;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) +;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) +;; CHECK-NEXT: (local.set $2 +;; CHECK-NEXT: (call $main +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (return +;; CHECK-NEXT: (local.get $2) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (throw_ref +;; CHECK-NEXT: (local.get $3) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) diff --git a/test/lit/passes/jspi-hooks-response-file.txt b/test/lit/passes/jspi-hooks-response-file.txt new file mode 100644 index 00000000000..498d5e4c797 --- /dev/null +++ b/test/lit/passes/jspi-hooks-response-file.txt @@ -0,0 +1,2 @@ +env.a +env.b diff --git a/test/lit/passes/jspi-hooks-response-file.wast b/test/lit/passes/jspi-hooks-response-file.wast new file mode 100644 index 00000000000..f007ddeb1af --- /dev/null +++ b/test/lit/passes/jspi-hooks-response-file.wast @@ -0,0 +1,21 @@ +;; RUN: wasm-opt %s --jspi-hooks --pass-arg=jspi-imports@@%S/jspi-hooks-response-file.txt --enable-reference-types --enable-exception-handling -S -o - | filecheck %s + +;; Newline-separated list read from a response file. + +;; CHECK: (import "env" "c" (func $c)) +;; CHECK: (import "env" "a" (func $byn$jspi-hooks$import$a)) +;; CHECK: (import "env" "b" (func $byn$jspi-hooks$import$b)) +;; CHECK: (func $a +;; CHECK: (func $b +;; CHECK-NOT: (func $c + +(module + (import "env" "a" (func $a)) + (import "env" "b" (func $b)) + (import "env" "c" (func $c)) + (func (export "__jspi_enter") (result i64) (i64.const 0)) + (func (export "__jspi_exit") (param i64 i32)) + (func (export "__jspi_suspend") (result i64) (i64.const 0)) + (func (export "__jspi_resume") (param i64 i32)) + (func (export "use") (call $a) (call $b) (call $c)) +) diff --git a/test/lit/passes/jspi-hooks.wast b/test/lit/passes/jspi-hooks.wast new file mode 100644 index 00000000000..a4596d55b8c --- /dev/null +++ b/test/lit/passes/jspi-hooks.wast @@ -0,0 +1,609 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: foreach %s %t wasm-opt --jspi-hooks --pass-arg=jspi-imports@env.sleep,env.fetch_*,other.io --pass-arg=jspi-exports@main,run_*,__jspi_* --enable-reference-types --enable-exception-handling -S -o - | filecheck %s + +;; Also check the wrapped module is stable through a binary round trip and +;; validates after optimization. +;; RUN: foreach %s %t wasm-opt --jspi-hooks --pass-arg=jspi-imports@env.sleep,env.fetch_*,other.io --pass-arg=jspi-exports@main,run_*,__jspi_* --enable-reference-types --enable-exception-handling --roundtrip -O2 -o /dev/null + +(module + ;; Imports: direct call, ref.func, element segment, export of an import, an + ;; import with a mixed-type signature and no result, and non-matching imports. + + ;; CHECK: (type $sig (func (param i32) (result i32))) + (type $sig (func (param i32) (result i32))) + (import "env" "sleep" (func $sleep (param i32) (result i32))) + (import "env" "fetch_data" (func $fetch_data (param i32 i64 f32 f64 externref))) + (import "other" "io" (func $io (result i64))) + + ;; CHECK: (type $1 (func (result i64))) + + ;; CHECK: (type $2 (func (param i32 i64 f32 f64 externref))) + + ;; CHECK: (type $3 (func (param i64 i32))) + + ;; CHECK: (type $4 (func (param externref))) + + ;; CHECK: (type $5 (func (result funcref))) + + ;; CHECK: (import "env" "not_async" (func $not_async (param i32) (result i32))) + (import "env" "not_async" (func $not_async (param i32) (result i32))) + (import "env" "sleep" (func $sleep_alias (param i32) (result i32))) + + ;; CHECK: (import "env" "sleep" (func $byn$jspi-hooks$import$sleep (param i32) (result i32))) + + ;; CHECK: (import "env" "fetch_data" (func $byn$jspi-hooks$import$fetch_data (param i32 i64 f32 f64 externref))) + + ;; CHECK: (import "other" "io" (func $byn$jspi-hooks$import$io (result i64))) + + ;; CHECK: (import "env" "sleep" (func $byn$jspi-hooks$import$sleep_alias (param i32) (result i32))) + + ;; CHECK: (table $t 2 funcref) + (table $t 2 funcref) + (elem (table $t) (i32.const 0) func $sleep $not_async) + + ;; CHECK: (elem $0 (i32.const 0) $sleep $not_async) + + ;; CHECK: (export "__jspi_enter" (func $enter)) + + ;; CHECK: (export "__jspi_exit" (func $exit)) + + ;; CHECK: (export "__jspi_suspend" (func $suspend)) + + ;; CHECK: (export "__jspi_resume" (func $resume)) + + ;; CHECK: (export "call_sleep" (func $call_sleep)) + + ;; CHECK: (export "call_fetch" (func $call_fetch)) + + ;; CHECK: (export "call_io" (func $call_io)) + + ;; CHECK: (export "call_indirect" (func $call_indirect)) + + ;; CHECK: (export "get_ref" (func $get_ref)) + + ;; CHECK: (export "sleep_export" (func $sleep)) + + ;; CHECK: (func $sleep (param $0 i32) (result i32) + ;; CHECK-NEXT: (local $1 i64) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local $3 exnref) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (call $suspend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) + ;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (call $byn$jspi-hooks$import$sleep + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (throw_ref + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + + ;; CHECK: (func $fetch_data (param $0 i32) (param $1 i64) (param $2 f32) (param $3 f64) (param $4 externref) + ;; CHECK-NEXT: (local $5 i64) + ;; CHECK-NEXT: (local $6 exnref) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (call $suspend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) + ;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) + ;; CHECK-NEXT: (call $byn$jspi-hooks$import$fetch_data + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (throw_ref + ;; CHECK-NEXT: (local.get $6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + + ;; CHECK: (func $io (result i64) + ;; CHECK-NEXT: (local $0 i64) + ;; CHECK-NEXT: (local $1 i64) + ;; CHECK-NEXT: (local $2 exnref) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (call $suspend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) + ;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (call $byn$jspi-hooks$import$io) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (throw_ref + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + + ;; CHECK: (func $sleep_alias (param $0 i32) (result i32) + ;; CHECK-NEXT: (local $1 i64) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local $3 exnref) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (call $suspend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) + ;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (call $byn$jspi-hooks$import$sleep_alias + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (throw_ref + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + + ;; CHECK: (func $enter (result i64) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $enter (export "__jspi_enter") (result i64) (i64.const 0)) + ;; CHECK: (func $exit (param $0 i64) (param $1 i32) + ;; CHECK-NEXT: ) + (func $exit (export "__jspi_exit") (param i64 i32)) + ;; CHECK: (func $suspend (result i64) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $suspend (export "__jspi_suspend") (result i64) (i64.const 0)) + ;; CHECK: (func $resume (param $0 i64) (param $1 i32) + ;; CHECK-NEXT: ) + (func $resume (export "__jspi_resume") (param i64 i32)) + + ;; CHECK: (func $call_sleep (param $x i32) (result i32) + ;; CHECK-NEXT: (call $sleep + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call_sleep (export "call_sleep") (param $x i32) (result i32) + (call $sleep (local.get $x)) + ) + ;; CHECK: (func $call_fetch (param $r externref) + ;; CHECK-NEXT: (call $fetch_data + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i64.const 2) + ;; CHECK-NEXT: (f32.const 3) + ;; CHECK-NEXT: (f64.const 4) + ;; CHECK-NEXT: (local.get $r) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call_fetch (export "call_fetch") (param $r externref) + (call $fetch_data (i32.const 1) (i64.const 2) (f32.const 3) (f64.const 4) (local.get $r)) + ) + ;; CHECK: (func $call_io (result i64) + ;; CHECK-NEXT: (call $io) + ;; CHECK-NEXT: ) + (func $call_io (export "call_io") (result i64) (call $io)) + ;; CHECK: (func $call_indirect (param $x i32) (result i32) + ;; CHECK-NEXT: (call_indirect $t (type $sig) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $call_indirect (export "call_indirect") (param $x i32) (result i32) + (call_indirect $t (type $sig) (local.get $x) (i32.const 0)) + ) + ;; CHECK: (func $get_ref (result funcref) + ;; CHECK-NEXT: (ref.func $sleep) + ;; CHECK-NEXT: ) + (func $get_ref (export "get_ref") (result funcref) (ref.func $sleep)) + + (export "sleep_export" (func $sleep)) +) + +;; Exports: retargeting, internal callers untouched, one function exported +;; twice, wildcard, non-function exports, exported import, no-result export. + +(module + + (import "env" "sleep" (func $sleep (param i32) (result i32))) + + ;; CHECK: (type $0 (func (param i32) (result i32))) + + ;; CHECK: (type $1 (func (result i64))) + + ;; CHECK: (type $2 (func (param i64 i32))) + + ;; CHECK: (type $3 (func (param i32 i32) (result i32))) + + ;; CHECK: (type $4 (func)) + + ;; CHECK: (type $5 (func (result i32))) + + ;; CHECK: (import "env" "imp" (func $imp (param i32) (result i32))) + (import "env" "imp" (func $imp (param i32) (result i32))) + + ;; CHECK: (import "env" "sleep" (func $byn$jspi-hooks$import$sleep (param i32) (result i32))) + + ;; CHECK: (global $g i32 (i32.const 0)) + (global $g i32 (i32.const 0)) + ;; CHECK: (memory $m 1) + (memory $m 1) + + ;; CHECK: (export "__jspi_enter" (func $enter)) + + ;; CHECK: (export "__jspi_exit" (func $exit)) + + ;; CHECK: (export "__jspi_suspend" (func $suspend)) + + ;; CHECK: (export "__jspi_resume" (func $resume)) + + ;; CHECK: (export "main" (func $byn$jspi-hooks$export$main)) + + ;; CHECK: (export "run_thing" (func $byn$jspi-hooks$export$run_thing)) + + ;; CHECK: (export "unrelated" (func $unrelated)) + + ;; CHECK: (export "__main_argc_argv" (func $main)) + + ;; CHECK: (export "run_import" (func $byn$jspi-hooks$export$imp)) + + ;; CHECK: (export "run_sleep" (func $byn$jspi-hooks$export$sleep)) + + ;; CHECK: (export "main_global" (global $g)) + + ;; CHECK: (export "run_memory" (memory $m)) + + ;; CHECK: (func $sleep (param $0 i32) (result i32) + ;; CHECK-NEXT: (local $1 i64) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local $3 exnref) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (call $suspend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) + ;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (call $byn$jspi-hooks$import$sleep + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $resume + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (throw_ref + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + + ;; CHECK: (func $enter (result i64) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $enter (export "__jspi_enter") (result i64) (i64.const 0)) + ;; CHECK: (func $exit (param $0 i64) (param $1 i32) + ;; CHECK-NEXT: ) + (func $exit (export "__jspi_exit") (param i64 i32)) + ;; CHECK: (func $suspend (result i64) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $suspend (export "__jspi_suspend") (result i64) (i64.const 0)) + ;; CHECK: (func $resume (param $0 i64) (param $1 i32) + ;; CHECK-NEXT: ) + (func $resume (export "__jspi_resume") (param i64 i32)) + + ;; CHECK: (func $main (param $argc i32) (param $argv i32) (result i32) + ;; CHECK-NEXT: (local.get $argc) + ;; CHECK-NEXT: ) + (func $main (export "main") (param $argc i32) (param $argv i32) (result i32) + (local.get $argc) + ) + (export "__main_argc_argv" (func $main)) + ;; CHECK: (func $run_thing + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $main + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $run_thing (export "run_thing") + (drop (call $main (i32.const 0) (i32.const 0))) + ) + ;; CHECK: (func $unrelated (result i32) + ;; CHECK-NEXT: (call $main + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unrelated (export "unrelated") (result i32) + (call $main (i32.const 1) (i32.const 2)) + ) + (export "run_import" (func $imp)) + ;; matched both as import and as export: the export wrapper calls the + ;; import wrapper + (export "run_sleep" (func $sleep)) + (export "main_global" (global $g)) + (export "run_memory" (memory $m)) +) + +;; The hook export itself is never wrapped even under a matching +;; pattern. + +;; CHECK: (func $byn$jspi-hooks$export$main (param $0 i32) (param $1 i32) (result i32) +;; CHECK-NEXT: (local $2 i64) +;; CHECK-NEXT: (local $3 i32) +;; CHECK-NEXT: (local $4 exnref) +;; CHECK-NEXT: (local.set $2 +;; CHECK-NEXT: (call $enter) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.set $4 +;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) +;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) +;; CHECK-NEXT: (local.set $3 +;; CHECK-NEXT: (call $main +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $2) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (return +;; CHECK-NEXT: (local.get $3) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $2) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (throw_ref +;; CHECK-NEXT: (local.get $4) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $byn$jspi-hooks$export$run_thing +;; CHECK-NEXT: (local $0 i64) +;; CHECK-NEXT: (local $1 exnref) +;; CHECK-NEXT: (local.set $0 +;; CHECK-NEXT: (call $enter) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.set $1 +;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) +;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) +;; CHECK-NEXT: (call $run_thing) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (return) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (throw_ref +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $byn$jspi-hooks$export$imp (param $0 i32) (result i32) +;; CHECK-NEXT: (local $1 i64) +;; CHECK-NEXT: (local $2 i32) +;; CHECK-NEXT: (local $3 exnref) +;; CHECK-NEXT: (local.set $1 +;; CHECK-NEXT: (call $enter) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.set $3 +;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) +;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) +;; CHECK-NEXT: (local.set $2 +;; CHECK-NEXT: (call $imp +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (return +;; CHECK-NEXT: (local.get $2) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (throw_ref +;; CHECK-NEXT: (local.get $3) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $byn$jspi-hooks$export$sleep (param $0 i32) (result i32) +;; CHECK-NEXT: (local $1 i64) +;; CHECK-NEXT: (local $2 i32) +;; CHECK-NEXT: (local $3 exnref) +;; CHECK-NEXT: (local.set $1 +;; CHECK-NEXT: (call $enter) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.set $3 +;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) +;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) +;; CHECK-NEXT: (local.set $2 +;; CHECK-NEXT: (call $sleep +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (return +;; CHECK-NEXT: (local.get $2) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (throw_ref +;; CHECK-NEXT: (local.get $3) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +(module + + ;; CHECK: (type $0 (func (result i64))) + + ;; CHECK: (type $1 (func (param i64 i32))) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (type $3 (func (param externref))) + + ;; CHECK: (import "env" "__jspi_jstag" (tag $tag (type $3) (param externref))) + (import "env" "__jspi_jstag" (tag $tag (param externref))) + + ;; CHECK: (export "__jspi_enter" (func $enter)) + + ;; CHECK: (export "__jspi_exit" (func $exit)) + + ;; CHECK: (export "__jspi_suspend" (func $suspend)) + + ;; CHECK: (export "__jspi_resume" (func $resume)) + + ;; CHECK: (export "main" (func $byn$jspi-hooks$export$main)) + + ;; CHECK: (func $enter (result i64) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $enter (export "__jspi_enter") (result i64) (i64.const 0)) + ;; CHECK: (func $exit (param $0 i64) (param $1 i32) + ;; CHECK-NEXT: ) + (func $exit (export "__jspi_exit") (param i64 i32)) + ;; CHECK: (func $suspend (result i64) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $suspend (export "__jspi_suspend") (result i64) (i64.const 0)) + ;; CHECK: (func $resume (param $0 i64) (param $1 i32) + ;; CHECK-NEXT: ) + (func $resume (export "__jspi_resume") (param i64 i32)) + ;; CHECK: (func $main + ;; CHECK-NEXT: ) + (func $main (export "main")) +) + +;; Nothing matches: the module is untouched and no hook is required. + +;; CHECK: (func $byn$jspi-hooks$export$main +;; CHECK-NEXT: (local $0 i64) +;; CHECK-NEXT: (local $1 exnref) +;; CHECK-NEXT: (local.set $0 +;; CHECK-NEXT: (call $enter) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.set $1 +;; CHECK-NEXT: (block $byn$jspi-hooks$any (result exnref) +;; CHECK-NEXT: (try_table (catch_all_ref $byn$jspi-hooks$any) +;; CHECK-NEXT: (call $main) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (return) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (call $exit +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (throw_ref +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +(module + + ;; CHECK: (type $0 (func)) + + ;; CHECK: (import "env" "other" (func $other)) + (import "env" "other" (func $other)) + + ;; CHECK: (export "foo" (func $foo)) + + ;; CHECK: (func $foo + ;; CHECK-NEXT: (call $other) + ;; CHECK-NEXT: ) + (func $foo (export "foo") (call $other)) +) +