diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index 2ac4ceeddde..27b3447ac02 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -798,8 +798,14 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { // If |possibleDefiningGlobal| is provided, it is the name of a global that we // are in the init expression of, and which can be reused as defining global, // if the other conditions are suitable. + // + // Returns nullptr if we cannot serialize. Expression* getSerialization(Literal value, Name possibleDefiningGlobal = Name()) { + if (value.isContinuation()) { + return nullptr; + } + Builder builder(*wasm); // If this is externalized then we want to inspect the inner data, handle @@ -861,12 +867,19 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { } for (auto& value : values) { - args.push_back(getSerialization(value)); + auto* serialized = getSerialization(value); + if (!serialized) { + return nullptr; + } + args.push_back(serialized); } Expression* desc = nullptr; if (data->desc.getGCData()) { desc = getSerialization(data->desc); + if (!desc) { + return nullptr; + } } Expression* init; @@ -913,7 +926,11 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { assert(possibleDefiningGlobal.isNull()); std::vector children; for (const auto& value : values) { - children.push_back(getSerialization(value)); + auto* serialized = getSerialization(value); + if (!serialized) { + return nullptr; + } + children.push_back(serialized); } return Builder(*wasm).makeTupleMake(children); } @@ -1132,7 +1149,17 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, // state in case we fail to eval the new function. localExprs.clear(); for (auto& param : params) { - localExprs.push_back(interface.getSerialization(param)); + auto* serialized = interface.getSerialization(param); + if (!serialized) { + break; + } + localExprs.push_back(serialized); + } + if (localExprs.size() < params.size()) { + if (!quiet) { + std::cout << " ...stopping due to non-serializable param\n"; + } + break; } interface.applyToModule(); goto start_eval; @@ -1152,7 +1179,17 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, // serialization sets from scratch each time here, for all locals. localExprs.clear(); for (Index i = 0; i < func->getNumLocals(); i++) { - localExprs.push_back(interface.getSerialization(scope.locals[i])); + auto* serialized = interface.getSerialization(scope.locals[i]); + if (!serialized) { + break; + } + localExprs.push_back(serialized); + } + if (localExprs.size() < func->getNumLocals()) { + if (!quiet) { + std::cout << " ...stopping due to non-serializable local\n"; + } + break; } interface.applyToModule(); successes++; @@ -1341,12 +1378,20 @@ void evalCtors(Module& wasm, ? *exp->getInternalName() : Name()); auto copyName = Names::getValidFunctionName(wasm, func->name); - auto* copyFunc = ModuleUtils::copyFunction(func, wasm, copyName); + auto copyFunc = + ModuleUtils::copyFunctionWithoutAdd(func, wasm, copyName); if (func->getResults() == Type::none) { copyFunc->body = Builder(wasm).makeNop(); } else { copyFunc->body = interface.getSerialization(*outcome); + if (!copyFunc->body) { + if (!quiet) { + std::cout << " ...stopping due to non-serializable body\n"; + } + return; + } } + wasm.addFunction(std::move(copyFunc)); *wasm.getExport(exp->name)->getInternalName() = copyName; } } diff --git a/test/lit/ctor-eval/cont-noserial.wast b/test/lit/ctor-eval/cont-noserial.wast new file mode 100644 index 00000000000..68eeb2cafc9 --- /dev/null +++ b/test/lit/ctor-eval/cont-noserial.wast @@ -0,0 +1,338 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; Test that we do not error on trying to serialize continuations (which cannot +;; be serialized). When we do not pass --kept-exports, we can at least remove +;; that "test" export, since we don't need to serialize anything when it doesn't +;; stick around. + +;; RUN: foreach %s %t wasm-ctor-eval --ctors=test --kept-exports=test -all --ignore-external-input -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-ctor-eval --ctors=test -all --ignore-external-input -S -o - | filecheck %s --check-prefix=NOKEEP + +(module + ;; CHECK: (type $func (func)) + ;; NOKEEP: (type $func (func)) + (type $func (func)) + ;; CHECK: (type $cont (cont $func)) + (type $cont (cont $func)) + + ;; CHECK: (type $2 (func (result (ref $cont)))) + + ;; CHECK: (elem declare func $func) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (export "test" (func $test)) + (export "test" (func $test)) + + ;; CHECK: (func $func (type $func) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $func + ) + + ;; CHECK: (func $test (type $2) (result (ref $cont)) + ;; CHECK-NEXT: (cont.new $cont + ;; CHECK-NEXT: (ref.func $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (result (ref $cont)) + (cont.new $cont + (ref.func $func) + ) + ) + + ;; CHECK: (func $export (type $func) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; NOKEEP: (export "export" (func $export)) + + ;; NOKEEP: (func $export (type $func) + ;; NOKEEP-NEXT: (nop) + ;; NOKEEP-NEXT: ) + (func $export (export "export") + ;; A dummy export, just to avoid the NOKEEP case ending up with a blank + ;; module and no CHECKs. + ) +) + +;; As above, but there is another use of $test. +(module + ;; CHECK: (type $func (func)) + ;; NOKEEP: (type $func (func)) + (type $func (func)) + ;; CHECK: (type $cont (cont $func)) + ;; NOKEEP: (type $cont (cont $func)) + (type $cont (cont $func)) + + ;; CHECK: (type $2 (func (result (ref $cont)))) + + ;; CHECK: (elem declare func $func) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (export "test" (func $test)) + (export "test" (func $test)) + + ;; CHECK: (func $func (type $func) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; NOKEEP: (type $2 (func (result (ref $cont)))) + + ;; NOKEEP: (elem declare func $func) + + ;; NOKEEP: (export "export" (func $export)) + + ;; NOKEEP: (func $func (type $func) + ;; NOKEEP-NEXT: (nop) + ;; NOKEEP-NEXT: ) + (func $func + ) + + ;; CHECK: (func $test (type $2) (result (ref $cont)) + ;; CHECK-NEXT: (cont.new $cont + ;; CHECK-NEXT: (ref.func $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; NOKEEP: (func $test (type $2) (result (ref $cont)) + ;; NOKEEP-NEXT: (cont.new $cont + ;; NOKEEP-NEXT: (ref.func $func) + ;; NOKEEP-NEXT: ) + ;; NOKEEP-NEXT: ) + (func $test (result (ref $cont)) + (cont.new $cont + (ref.func $func) + ) + ) + + ;; CHECK: (func $export (type $func) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $test) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; NOKEEP: (func $export (type $func) + ;; NOKEEP-NEXT: (drop + ;; NOKEEP-NEXT: (call $test) + ;; NOKEEP-NEXT: ) + ;; NOKEEP-NEXT: ) + (func $export (export "export") + ;; Call $test internally. This keeps $test alive in NOKEEP as well (but not + ;; the export "test"). + (drop + (call $test) + ) + ) +) + +;; As the original testcase, but the serialization problem happens in a local. +(module + ;; CHECK: (type $func (func)) + ;; NOKEEP: (type $func (func)) + (type $func (func)) + ;; CHECK: (type $cont (cont $func)) + ;; NOKEEP: (type $cont (cont $func)) + (type $cont (cont $func)) + + ;; CHECK: (elem declare func $func) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (export "test" (func $test)) + ;; NOKEEP: (elem declare func $func) + + ;; NOKEEP: (export "export" (func $export)) + + ;; NOKEEP: (export "test" (func $test)) + (export "test" (func $test)) + + ;; CHECK: (func $func (type $func) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; NOKEEP: (func $func (type $func) + ;; NOKEEP-NEXT: (nop) + ;; NOKEEP-NEXT: ) + (func $func + ) + + ;; CHECK: (func $test (type $func) + ;; CHECK-NEXT: (local $cont (ref $cont)) + ;; CHECK-NEXT: (local.set $cont + ;; CHECK-NEXT: (cont.new $cont + ;; CHECK-NEXT: (ref.func $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; NOKEEP: (func $test (type $func) + ;; NOKEEP-NEXT: (local $cont (ref $cont)) + ;; NOKEEP-NEXT: (local.set $cont + ;; NOKEEP-NEXT: (cont.new $cont + ;; NOKEEP-NEXT: (ref.func $func) + ;; NOKEEP-NEXT: ) + ;; NOKEEP-NEXT: ) + ;; NOKEEP-NEXT: (unreachable) + ;; NOKEEP-NEXT: ) + (func $test + (local $cont (ref $cont)) + (local.set $cont + (cont.new $cont + (ref.func $func) + ) + ) + ;; An effect, so the function cannot be fully precomputed - we precompute up + ;; to here, and try to stash in locals, but fail. + (unreachable) + ) + + ;; CHECK: (func $export (type $func) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; NOKEEP: (func $export (type $func) + ;; NOKEEP-NEXT: (nop) + ;; NOKEEP-NEXT: ) + (func $export (export "export") + ) +) + +;; As the original testcase, but the serialization problem happens in a +;; return_call param. +(module + ;; CHECK: (type $func (func)) + ;; NOKEEP: (type $func (func)) + (type $func (func)) + ;; CHECK: (type $1 (func (param contref))) + + ;; CHECK: (type $cont (cont $func)) + ;; NOKEEP: (type $1 (func (param contref))) + + ;; NOKEEP: (type $cont (cont $func)) + (type $cont (cont $func)) + + ;; CHECK: (elem declare func $func) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (export "test" (func $test)) + ;; NOKEEP: (elem declare func $func) + + ;; NOKEEP: (export "export" (func $export)) + + ;; NOKEEP: (export "test" (func $test)) + (export "test" (func $test)) + + ;; CHECK: (func $func (type $func) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; NOKEEP: (func $func (type $func) + ;; NOKEEP-NEXT: (nop) + ;; NOKEEP-NEXT: ) + (func $func + ) + + ;; CHECK: (func $test (type $1) (param $cont contref) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (ref.is_null + ;; CHECK-NEXT: (local.get $cont) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return_call $test + ;; CHECK-NEXT: (cont.new $cont + ;; CHECK-NEXT: (ref.func $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; NOKEEP: (func $test (type $1) (param $cont contref) + ;; NOKEEP-NEXT: (if + ;; NOKEEP-NEXT: (ref.is_null + ;; NOKEEP-NEXT: (local.get $cont) + ;; NOKEEP-NEXT: ) + ;; NOKEEP-NEXT: (then + ;; NOKEEP-NEXT: (return_call $test + ;; NOKEEP-NEXT: (cont.new $cont + ;; NOKEEP-NEXT: (ref.func $func) + ;; NOKEEP-NEXT: ) + ;; NOKEEP-NEXT: ) + ;; NOKEEP-NEXT: ) + ;; NOKEEP-NEXT: ) + ;; NOKEEP-NEXT: (unreachable) + ;; NOKEEP-NEXT: ) + (func $test (param $cont (ref null cont)) + (if + (ref.is_null + (local.get $cont) + ) + (then + (return_call $test + (cont.new $cont + (ref.func $func) + ) + ) + ) + ) + (unreachable) + ) + + ;; CHECK: (func $export (type $func) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; NOKEEP: (func $export (type $func) + ;; NOKEEP-NEXT: (nop) + ;; NOKEEP-NEXT: ) + (func $export (export "export") + ) +) + +;; As the original testcase, but the problem happens in a nested GC value. +(module + ;; CHECK: (type $func (func)) + ;; NOKEEP: (type $func (func)) + (type $func (func)) + ;; CHECK: (type $cont (cont $func)) + (type $cont (cont $func)) + + ;; CHECK: (type $2 (func (result anyref))) + + ;; CHECK: (type $struct (struct (field $cont (ref $cont)))) + (type $struct (struct (field $cont (ref $cont)))) + + ;; CHECK: (elem declare func $func) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (export "test" (func $test)) + (export "test" (func $test)) + + ;; CHECK: (func $func (type $func) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $func + ) + + ;; CHECK: (func $test (type $2) (result anyref) + ;; CHECK-NEXT: (struct.new $struct + ;; CHECK-NEXT: (cont.new $cont + ;; CHECK-NEXT: (ref.func $func) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (result anyref) + (struct.new $struct + (cont.new $cont + (ref.func $func) + ) + ) + ) + + ;; CHECK: (func $export (type $func) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; NOKEEP: (export "export" (func $export)) + + ;; NOKEEP: (func $export (type $func) + ;; NOKEEP-NEXT: (nop) + ;; NOKEEP-NEXT: ) + (func $export (export "export") + ) +)