From 484732f698e1c6665209645cf5de5495012a2503 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 11 Mar 2026 15:14:29 -0700 Subject: [PATCH 1/2] Handle more types exposed to JS in GTO GTO already handled keeping the prototype configuration fields on descriptors of types that flowed out to JS via @binaryen.js.called functions, but it did not handle propagating the information that a type flows out to JS to that type's subtypes. It also did not consider that types may flow out to JS via imports and exports. Fix these issues. --- src/passes/GlobalTypeOptimization.cpp | 119 ++++++-- test/lit/passes/gto-jsinterop.wast | 388 ++++++++++++++++++++++++++ 2 files changed, 490 insertions(+), 17 deletions(-) diff --git a/src/passes/GlobalTypeOptimization.cpp b/src/passes/GlobalTypeOptimization.cpp index 5ecc9ebae0a..d5f4be3ec47 100644 --- a/src/passes/GlobalTypeOptimization.cpp +++ b/src/passes/GlobalTypeOptimization.cpp @@ -168,9 +168,11 @@ struct GlobalTypeOptimization : public Pass { // Combine the data from the functions. functionSetGetInfos.combineInto(combinedSetGetInfos); - // Analyze functions called by JS to find fields holding configured - // prototypes that cannot be removed. - analyzeJSCalledFunctions(*module); + SubTypes subTypes(*module); + + // Analyze the JS interface to find fields holding configured prototypes + // that cannot be removed. + analyzeJSInterface(*module, subTypes); // Propagate information to super and subtypes on set/get infos: // @@ -198,7 +200,6 @@ struct GlobalTypeOptimization : public Pass { // subtypes (as wasm only allows the type to differ if the fields are // immutable). Note that by making more things immutable we therefore // make it possible to apply more specific subtypes in subtype fields. - SubTypes subTypes(*module); StructUtils::TypeHierarchyPropagator propagator(subTypes); auto dataFromSubsAndSupersMap = combinedSetGetInfos; propagator.propagateToSuperAndSubTypes(dataFromSubsAndSupersMap); @@ -278,7 +279,6 @@ struct GlobalTypeOptimization : public Pass { // remove the field. That is so even if there are writes (it would be a // pointless "write-only field"). auto hasNoReadsAnywhere = !dataFromSubsAndSupers[i].hasRead; - // Check for reads or writes in ourselves and our supers. If there are // none, then operations only happen in our strict subtypes, and those // subtypes can define the field there, and we don't need it here. @@ -416,22 +416,107 @@ struct GlobalTypeOptimization : public Pass { } } - void analyzeJSCalledFunctions(Module& wasm) { + void analyzeJSInterface(Module& wasm, const SubTypes& subTypes) { if (!wasm.features.hasCustomDescriptors()) { return; } - for (auto func : Intrinsics(wasm).getJSCalledFunctions()) { - // Look at the result types being returned to JS and make sure we preserve - // any configured prototypes they might expose. - for (auto type : wasm.getFunction(func)->getResults()) { - if (!type.isRef()) { - continue; + + std::unordered_set subtypesExposed; + + // Mark the relevant prototype field as read and return true iff we newly + // know we have to propate the exposure to subtypes. + auto noteExposed = [&](HeapType type, Exactness exact = Inexact) -> bool { + if (auto desc = type.getDescriptorType(); + desc && StructUtils::hasPossibleJSPrototypeField(*desc)) { + // This field holds a JS-visible prototype. Do not remove it. + combinedSetGetInfos[std::make_pair(*desc, exact)][0].noteRead(); + } + if (exact == Inexact) { + return subtypesExposed.insert(type).second; + } + return false; + }; + + // Values flowing out to JS might have their prototype field on their + // descriptor read by JS. + auto flowOut = [&](Type type) { + if (type.isRef()) { + noteExposed(type.getHeapType(), type.getExactness()); + } + }; + + // @binaryen.js.called functions are called from JS. Their results flow back + // out to JS. + for (auto f : Intrinsics(wasm).getJSCalledFunctions()) { + auto* func = wasm.getFunction(f); + for (auto type : func->getResults()) { + flowOut(type); + } + } + + for (auto& ex : wasm.exports) { + switch (ex->kind) { + case ExternalKindImpl::Function: { + auto* func = wasm.getFunction(*ex->getInternalName()); + for (auto type : func->getResults()) { + flowOut(type); + } + break; + } + case ExternalKindImpl::Table: { + auto* table = wasm.getTable(*ex->getInternalName()); + flowOut(table->type); + break; + } + case ExternalKindImpl::Global: { + auto* global = wasm.getGlobal(*ex->getInternalName()); + flowOut(global->type); + break; } - if (auto desc = type.getHeapType().getDescriptorType(); - desc && StructUtils::hasPossibleJSPrototypeField(*desc)) { - // This field holds a JS-visible prototype. Do not remove it. - auto exact = type.getExactness(); - combinedSetGetInfos[std::make_pair(*desc, exact)][0].noteRead(); + case ExternalKindImpl::Memory: + case ExternalKindImpl::Tag: + case ExternalKindImpl::Invalid: + break; + } + } + + for (auto& func : wasm.functions) { + if (func->imported()) { + for (auto type : func->getParams()) { + flowOut(type); + } + } + } + for (auto& table : wasm.tables) { + if (table->imported()) { + flowOut(table->type); + } + } + for (auto& global : wasm.globals) { + if (global->imported() && global->mutable_) { + flowOut(global->type); + } + } + + // Any type that is a subtype of an exposed type is also exposed. Propagate + // from supertypes to subtypes. + std::vector work(subtypesExposed.begin(), subtypesExposed.end()); + while (!work.empty()) { + auto type = work.back(); + work.pop_back(); + if (type.isBasic()) { + // TODO: Unify this with the more incremental propagation below if + // SubTypes ever gets support for scanning basic types. + for (auto other : subTypes.types) { + if (HeapType::isSubType(other, type)) { + noteExposed(other); + } + } + } else { + for (auto sub : subTypes.getImmediateSubTypes(type)) { + if (noteExposed(sub)) { + work.push_back(sub); + } } } } diff --git a/test/lit/passes/gto-jsinterop.wast b/test/lit/passes/gto-jsinterop.wast index c6e520b229d..4c64f2201e9 100644 --- a/test/lit/passes/gto-jsinterop.wast +++ b/test/lit/passes/gto-jsinterop.wast @@ -14,6 +14,7 @@ ;; CHECK: (type $desc (describes $struct) (struct (field externref))) (type $desc (describes $struct) (struct (field externref))) ) + ;; CHECK: (type $2 (func)) ;; CHECK: (type $3 (func (result (ref $struct)))) @@ -236,3 +237,390 @@ ) ) ) + +(module + ;; Supertype on the boundary propagates exposure to subtypes. + ;; CHECK: (type $super (sub (struct (field i32)))) + (type $super (sub (struct (field i32)))) + + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub (sub $super (descriptor $sub-desc) (struct (field i32)))) + (type $sub (sub $super (descriptor $sub-desc) (struct (field i32 i64)))) + ;; CHECK: (type $sub-desc (describes $sub) (struct (field externref))) + (type $sub-desc (describes $sub) (struct (field externref i64))) + ) + + ;; $super flows out, so $sub is exposed, so $sub-desc field is kept. + ;; CHECK: (type $3 (func)) + + ;; CHECK: (type $4 (func (param (ref null $super)))) + + ;; CHECK: (import "" "" (func $import (type $4) (param (ref null $super)))) + (import "" "" (func $import (param (ref null $super)))) + + ;; CHECK: (func $use-sub (type $3) + ;; CHECK-NEXT: (local $0 (ref null $sub)) + ;; CHECK-NEXT: ) + (func $use-sub + (local (ref null $sub)) + ) +) + +(module + ;; Same, but now the type is exact on the boundary, so it does not propagate + ;; exposure to subtypes. We can optimize $sub-desc. + ;; CHECK: (type $super (sub (struct (field i32)))) + (type $super (sub (struct (field i32)))) + + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub (sub $super (descriptor $sub-desc) (struct (field i32)))) + (type $sub (sub $super (descriptor $sub-desc) (struct (field i32 i64)))) + ;; CHECK: (type $sub-desc (describes $sub) (struct)) + (type $sub-desc (describes $sub) (struct (field externref i64))) + ) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (type $4 (func (param (ref null (exact $super))))) + + ;; CHECK: (import "" "" (func $import (type $4) (param (ref null (exact $super))))) + (import "" "" (func $import (param (ref null (exact $super))))) + + ;; CHECK: (func $use-sub (type $3) + ;; CHECK-NEXT: (local $0 (ref null $sub)) + ;; CHECK-NEXT: ) + (func $use-sub + (local (ref null $sub)) + ) +) + +(module + ;; Same, but now there is an abstract supertype on the boundary. We still + ;; propagate expose and keep the externref field in $sub-desc. + ;; CHECK: (rec + ;; CHECK-NEXT: (type $super (sub (struct))) + (type $super (sub (struct (field i32)))) + + (rec + ;; CHECK: (type $sub (sub $super (descriptor $sub-desc) (struct))) + (type $sub (sub $super (descriptor $sub-desc) (struct (field i32 i64)))) + ;; CHECK: (type $sub-desc (describes $sub) (struct (field externref))) + (type $sub-desc (describes $sub) (struct (field externref i64))) + ) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (type $4 (func (param anyref))) + + ;; CHECK: (import "" "" (func $import (type $4) (param anyref))) + (import "" "" (func $import (param anyref))) + + ;; CHECK: (func $use-sub (type $3) + ;; CHECK-NEXT: (local $0 (ref null $sub)) + ;; CHECK-NEXT: ) + (func $use-sub + (local (ref null $sub)) + ) +) + +(module + ;; Now we have to propagate through multiple supertypes without descriptors. + ;; We cannot optimize the externref field in the subtype descriptor. + ;; CHECK: (type $super (sub (struct (field i32)))) + (type $super (sub (struct (field i32)))) + ;; CHECK: (rec + ;; CHECK-NEXT: (type $mid (sub $super (struct (field i32)))) + (type $mid (sub $super (struct (field i32)))) + (rec + ;; CHECK: (type $sub (sub $mid (descriptor $sub-desc) (struct (field i32)))) + (type $sub (sub $mid (descriptor $sub-desc) (struct (field i32 i64)))) + ;; CHECK: (type $sub-desc (describes $sub) (struct (field externref))) + (type $sub-desc (describes $sub) (struct (field externref i64))) + ) + + ;; CHECK: (type $4 (func)) + + ;; CHECK: (type $5 (func (param (ref null $super)))) + + ;; CHECK: (import "" "" (func $import (type $5) (param (ref null $super)))) + (import "" "" (func $import (param (ref null $super)))) + + ;; CHECK: (func $use-sub (type $4) + ;; CHECK-NEXT: (local $0 (ref null $sub)) + ;; CHECK-NEXT: ) + (func $use-sub + (local (ref null $sub)) + ) +) + +(module + ;; Now the supertype is a result of the imported function rather than a + ;; parameter. It does not flow out, so we can optimize the subtype's + ;; descriptor. + ;; CHECK: (type $super (sub (struct (field i32)))) + (type $super (sub (struct (field i32)))) + + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub (sub $super (descriptor $sub-desc) (struct (field i32)))) + (type $sub (sub $super (descriptor $sub-desc) (struct (field i32 i64)))) + ;; CHECK: (type $sub-desc (describes $sub) (struct)) + (type $sub-desc (describes $sub) (struct (field externref i64))) + ) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (type $4 (func (result (ref null $super)))) + + ;; CHECK: (import "" "" (func $import (type $4) (result (ref null $super)))) + (import "" "" (func $import (result (ref null $super)))) + + ;; CHECK: (func $use-sub (type $3) + ;; CHECK-NEXT: (local $0 (ref null $sub)) + ;; CHECK-NEXT: ) + (func $use-sub + (local (ref null $sub)) + ) +) + +(module + ;; An exported function instead of an imported function. The supertype flows + ;; out as a result, so we cannot optimize the externref in $sub-desc. + ;; CHECK: (type $super (sub (struct (field i32)))) + (type $super (sub (struct (field i32)))) + + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub (sub $super (descriptor $sub-desc) (struct (field i32)))) + (type $sub (sub $super (descriptor $sub-desc) (struct (field i32 i64)))) + ;; CHECK: (type $sub-desc (describes $sub) (struct (field externref))) + (type $sub-desc (describes $sub) (struct (field externref i64))) + ) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (type $4 (func (result (ref null $super)))) + + ;; CHECK: (export "export" (func $export)) + (export "export" (func $export)) + + ;; CHECK: (func $export (type $4) (result (ref null $super)) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $export (result (ref null $super)) + (unreachable) + ) + + ;; CHECK: (func $use-sub (type $3) + ;; CHECK-NEXT: (local $0 (ref null $sub)) + ;; CHECK-NEXT: ) + (func $use-sub + (local (ref null $sub)) + ) +) + +(module + ;; Now The supertype is a parameter of the exported function. It does not flow + ;; out and we can optimize. + ;; CHECK: (type $super (sub (struct (field i32)))) + (type $super (sub (struct (field i32)))) + + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub (sub $super (descriptor $sub-desc) (struct (field i32)))) + (type $sub (sub $super (descriptor $sub-desc) (struct (field i32 i64)))) + ;; CHECK: (type $sub-desc (describes $sub) (struct)) + (type $sub-desc (describes $sub) (struct (field externref i64))) + ) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (type $4 (func (param (ref null $super)))) + + ;; CHECK: (export "export" (func $export)) + (export "export" (func $export)) + + ;; CHECK: (func $export (type $4) (param $0 (ref null $super)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $export (param (ref null $super)) + (nop) + ) + + ;; CHECK: (func $use-sub (type $3) + ;; CHECK-NEXT: (local $0 (ref null $sub)) + ;; CHECK-NEXT: ) + (func $use-sub + (local (ref null $sub)) + ) +) + +(module + ;; The supertype flows out via an imported mutable global. We cannot optimize + ;; the externref field in $sub-desc. + ;; CHECK: (type $super (sub (struct (field i32)))) + (type $super (sub (struct (field i32)))) + + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub (sub $super (descriptor $sub-desc) (struct (field i32)))) + (type $sub (sub $super (descriptor $sub-desc) (struct (field i32 i64)))) + ;; CHECK: (type $sub-desc (describes $sub) (struct (field externref))) + (type $sub-desc (describes $sub) (struct (field externref i64))) + ) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (import "" "" (global $import (mut (ref null $super)))) + (import "" "" (global $import (mut (ref null $super)))) + + ;; CHECK: (func $use-sub (type $3) + ;; CHECK-NEXT: (local $0 (ref null $sub)) + ;; CHECK-NEXT: ) + (func $use-sub + (local (ref null $sub)) + ) +) + +(module + ;; Now the imported global is immutable. The value does not flow out and we + ;; can optimize $sub-desc. + ;; CHECK: (type $super (sub (struct (field i32)))) + (type $super (sub (struct (field i32)))) + + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub (sub $super (descriptor $sub-desc) (struct (field i32)))) + (type $sub (sub $super (descriptor $sub-desc) (struct (field i32 i64)))) + ;; CHECK: (type $sub-desc (describes $sub) (struct)) + (type $sub-desc (describes $sub) (struct (field externref i64))) + ) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (import "" "" (global $import (ref null $super))) + (import "" "" (global $import (ref null $super))) + + ;; CHECK: (func $use-sub (type $3) + ;; CHECK-NEXT: (local $0 (ref null $sub)) + ;; CHECK-NEXT: ) + (func $use-sub + (local (ref null $sub)) + ) +) + +(module + ;; The supertype flows out via an exported mutable global. We cannot optimize + ;; the externref field in $sub-desc. + ;; CHECK: (type $super (sub (struct (field i32)))) + (type $super (sub (struct (field i32)))) + + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub (sub $super (descriptor $sub-desc) (struct (field i32)))) + (type $sub (sub $super (descriptor $sub-desc) (struct (field i32 i64)))) + ;; CHECK: (type $sub-desc (describes $sub) (struct (field externref))) + (type $sub-desc (describes $sub) (struct (field externref i64))) + ) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (global $export (mut (ref null $super)) (ref.null none)) + (global $export (export "export") (mut (ref null $super)) (ref.null none)) + + ;; CHECK: (export "export" (global $export)) + + ;; CHECK: (func $use-sub (type $3) + ;; CHECK-NEXT: (local $0 (ref null $sub)) + ;; CHECK-NEXT: ) + (func $use-sub + (local (ref null $sub)) + ) +) + +(module + ;; Now the exported global is immutable. The supertype still flows out, so we + ;; cannot optimize the externref field in $sub-desc. + ;; CHECK: (type $super (sub (struct (field i32)))) + (type $super (sub (struct (field i32)))) + + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub (sub $super (descriptor $sub-desc) (struct (field i32)))) + (type $sub (sub $super (descriptor $sub-desc) (struct (field i32 i64)))) + ;; CHECK: (type $sub-desc (describes $sub) (struct (field externref))) + (type $sub-desc (describes $sub) (struct (field externref i64))) + ) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (global $export (ref null $super) (ref.null none)) + (global $export (export "export") (ref null $super) (ref.null none)) + + ;; CHECK: (export "export" (global $export)) + + ;; CHECK: (func $use-sub (type $3) + ;; CHECK-NEXT: (local $0 (ref null $sub)) + ;; CHECK-NEXT: ) + (func $use-sub + (local (ref null $sub)) + ) +) + +(module + ;; The supertype flows out via an imported table. We cannot optimize the + ;; externref field in $sub-desc. + ;; CHECK: (type $super (sub (struct (field i32)))) + (type $super (sub (struct (field i32)))) + + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub (sub $super (descriptor $sub-desc) (struct (field i32)))) + (type $sub (sub $super (descriptor $sub-desc) (struct (field i32 i64)))) + ;; CHECK: (type $sub-desc (describes $sub) (struct (field externref))) + (type $sub-desc (describes $sub) (struct (field externref i64))) + ) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (import "" "" (table $t 1 (ref null $super))) + (import "" "" (table $t 1 (ref null $super))) + + ;; CHECK: (func $use-sub (type $3) + ;; CHECK-NEXT: (local $0 (ref null $sub)) + ;; CHECK-NEXT: ) + (func $use-sub + (local (ref null $sub)) + ) +) + +(module + ;; The supertype flows out via an exported table. We cannot optimize the + ;; externref field in $sub-desc. + ;; CHECK: (type $super (sub (struct (field i32)))) + (type $super (sub (struct (field i32)))) + + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub (sub $super (descriptor $sub-desc) (struct (field i32)))) + (type $sub (sub $super (descriptor $sub-desc) (struct (field i32 i64)))) + ;; CHECK: (type $sub-desc (describes $sub) (struct (field externref))) + (type $sub-desc (describes $sub) (struct (field externref i64))) + ) + + ;; CHECK: (type $3 (func)) + + ;; CHECK: (table $t 1 (ref null $super)) + (table $t (export "export") 1 (ref null $super)) + + ;; CHECK: (export "export" (table $t)) + + ;; CHECK: (func $use-sub (type $3) + ;; CHECK-NEXT: (local $0 (ref null $sub)) + ;; CHECK-NEXT: ) + (func $use-sub + (local (ref null $sub)) + ) +) From 281ac11893a97a7bcc85d1bfd2988d07ed4244f5 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 5 Mar 2026 18:19:22 -0800 Subject: [PATCH 2/2] Print and compare prototypes in fuzzer interpreter When a struct flows out to JS, if it has a descriptor and that desriptor's first field is an externref, that field's value becomes the JS prototype of the struct. There is a class of bugs where we misoptimize something in this setup so that the JS-observable prototype changes. To catch those bugs in the fuzzer, update the fuzzer interpreter and fuzz_shell.js to print the prototypes of objects. This lets the fuzzer make sure that engines like V8 and interpreter agree on whether there is a prototype. Also update the fuzzer interpreter to compare configured prototypes when comparing two execution traces. This lets --fuzz-exec detect when optimizations have changed a prototype. --- scripts/fuzz_shell.js | 40 +++++-- src/literal.h | 8 ++ src/tools/execution-results.h | 143 ++++++++++++++++--------- src/tools/wasm-ctor-eval.cpp | 6 +- src/wasm-interpreter.h | 1 - src/wasm/CMakeLists.txt | 1 - src/wasm/literal.cpp | 49 ++++++++- src/wasm/wasm-interpreter.cpp | 10 -- test/lit/exec/cont_many_unhandled.wast | 2 +- test/lit/exec/eh-print.wast | 4 +- test/lit/exec/fuzzing-api.wast | 60 +++++------ test/lit/exec/tag-cross-module.wast | 4 +- 12 files changed, 220 insertions(+), 108 deletions(-) delete mode 100644 src/wasm/wasm-interpreter.cpp diff --git a/scripts/fuzz_shell.js b/scripts/fuzz_shell.js index 745cae7337c..44107a9050d 100644 --- a/scripts/fuzz_shell.js +++ b/scripts/fuzz_shell.js @@ -144,16 +144,29 @@ function printed(x, y) { // Print bigints in legalized form, which is two 32-bit numbers of the low // and high bits. return (Number(x & 0xffffffffn) | 0) + ' ' + (Number(x >> 32n) | 0) - } else if (typeof x !== 'number') { - // Something that is not a number or string, like a reference. We can't - // print a reference because it could look different after opts - imagine - // that a function gets renamed internally (that is, the problem is that - // JS printing will emit some info about the reference and not a stable - // external representation of it). In those cases just print the type, - // which will be 'object' or 'function'. - return typeof x; + } else if (typeof x === 'object') { + // This may be one of the externref imports, in which case we can print its + // payload. + if (Object.hasOwn(x, 'payload')) { + return 'externref(' + x.payload + ')'; + } + // Or maybe this is a JS error we caught. + if (x instanceof Error) { + return 'jserror'; + } + // If this is a Wasm object, we can't access its type or any of its + // internal structure, which might have been changed by optimizations + // anyway. It might have a configured prototype, though, and that + // prototype may be an imported externref global we can identify by the + // payload we gave it. + return 'object(' + printed(Object.getPrototypeOf(x)) + ')'; + } else if (typeof x === 'function') { + // We cannot print function names because they might have been changed by + // optimizations. + return 'function'; } else { // A number. Print the whole thing. + assert(typeof x === 'number'); return '' + x; } } @@ -468,8 +481,15 @@ function makeImports(module) { baseImports[module] = {}; } if (!baseImports[module][name]) { - // TODO: Use different payloads for different imports. - baseImports[module][name] = {}; + // Compute a payload from the import names. This must be kept in sync + // with execution-results.h. + var payload = 0; + for (var name of [module, name]) { + for (var c of name) { + payload = (payload + c.charCodeAt(0)) % 251; + } + } + baseImports[module][name] = { payload }; } } } diff --git a/src/literal.h b/src/literal.h index 9eb27177d75..21ff1c19688 100644 --- a/src/literal.h +++ b/src/literal.h @@ -737,6 +737,14 @@ class Literal { Literal externalize() const; Literal internalize() const; + // Internalize an externalized value or externalize an internalized value, + // otherwise return the literal unmodified. + Literal unwrap() const; + + // Get the JS prototype configured via this struct's descriptor, if it exists, + // or null. Assumes this is a reference value. + Literal getJSPrototype() const; + private: Literal addSatSI8(const Literal& other) const; Literal addSatUI8(const Literal& other) const; diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index f3b45c049d6..8b029b11d34 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -21,6 +21,7 @@ #include #include +#include "ir/import-names.h" #include "ir/import-utils.h" #include "shell-interface.h" #include "support/utilities.h" @@ -58,20 +59,9 @@ Tag& getJsTag() { return tag; } -void printValue(Literal value) { - // Unwrap an externalized GC value to get the actual value, but not strings, - // which are normally a subtype of ext. - if (Type::isSubType(value.type, Type(HeapType::ext, Nullable)) && - !value.type.isString()) { - value = value.internalize(); - } - - // An anyref literal is a string. - if (value.type.isRef() && - value.type.getHeapType().isMaybeShared(HeapType::any)) { - value = value.externalize(); - } +constexpr Index jsErrorPayload = 0xbad; +void printValue(Literal value) { // Don't print most reference values, as e.g. funcref(N) contains an index, // which is not guaranteed to remain identical after optimizations. Do not // print the type in detail (as even that may change due to closed-world @@ -81,22 +71,32 @@ void printValue(Literal value) { // // The only references we print in full are strings and i31s, which have // simple and stable internal structures that optimizations will not alter. - auto type = value.type; - if (type.isRef()) { - if (type.isString() || type.getHeapType().isMaybeShared(HeapType::i31)) { - std::cout << value; - } else if (value.isNull()) { - std::cout << "null"; - } else if (type.isFunction()) { - std::cout << "function"; - } else { - std::cout << "object"; - } + // + // Non-references can be printed in full. + if (!value.type.isRef()) { + std::cout << value; return; } - - // Non-references can be printed in full. - std::cout << value; + value = value.unwrap(); + auto heapType = value.type.getHeapType(); + if (heapType.isMaybeShared(HeapType::ext) && + value.getExternPayload() == jsErrorPayload) { + std::cout << "jserror"; + return; + } + if (heapType.isString() || heapType.isMaybeShared(HeapType::ext) || + heapType.isMaybeShared(HeapType::i31)) { + std::cout << value; + } else if (value.isNull()) { + std::cout << "null"; + } else if (heapType.isFunction()) { + std::cout << "function"; + } else { + // Print 'object' and its JS-visible prototype, which may be null. + std::cout << "object("; + printValue(value.getJSPrototype()); + std::cout << ')'; + } } } // namespace @@ -275,7 +275,7 @@ struct LoggingExternalInterface : public ShellExternalInterface { void throwJSException() { // JS exceptions contain an externref. - Literals arguments = {Literal::makeExtern(0, Unshared)}; + Literals arguments = {Literal::makeExtern(jsErrorPayload, Unshared)}; auto payload = std::make_shared(&jsTag, arguments); throwException(WasmException{Literal(payload)}); } @@ -395,8 +395,18 @@ class FuzzerImportResolver if (mut || !type.isRef() || type.getHeapType() != HeapType::ext) { return nullptr; } - // TODO: Generate a distinct payload for each global. - synthesizedGlobals.emplace_back(Literals{Literal::makeExtern(0, Unshared)}); + // Optimizations may reorder or remove imports, so we need a distinct + // payload that is independent of the import order. Just compute a simple + // payload integer from the import names. This must be kept in sync with + // fuzz_shell.js. + Index payload = 0; + for (auto name : {name.module, name.name}) { + for (auto c : name.str) { + payload = (payload + static_cast(c)) % 251; + } + } + synthesizedGlobals.emplace_back( + Literals{Literal::makeExtern(payload, Unshared)}); return &synthesizedGlobals.back(); } @@ -510,28 +520,56 @@ struct ExecutionResults { } bool areEqual(Literal a, Literal b) { - // Don't compare references. There are several issues here that we can't - // fully handle, see https://github.com/WebAssembly/binaryen/issues/3378, - // but the core issue is that since we optimize assuming a closed world, the - // types and structure of GC data can arbitrarily change after - // optimizations, even in ways that are externally visible from outside - // the module. - // - // We can, however, compare strings as they refer to simple data that has a - // consistent representation (the same reasons as why we can print them in - // printValue(), above). + // Only compare some references. In general the optimizer may change + // identities and structures of functions, types, and GC values in ways that + // are not externally observable. We must therefore limit ourselves to + // comparing information that _is_ externally observable. // - // TODO: Once we support optimizing under some form of open-world - // assumption, we should be able to check that the types and/or structure of - // GC data passed out of the module does not change. - if (a.type.isRef() && !a.type.isString() && - !a.type.getHeapType().isMaybeShared(HeapType::i31)) { - return true; + // TODO: We could compare more information when we know it will be + // externally visible, for example when the type of the value is public. + if (!a.type.isRef() || !b.type.isRef()) { + return a == b; + } + // The environment always sees externalized references and is able to + // observe the difference between external references and externalized + // internal references. Make sure this is accounted for below by unrapping + // the references. + a = a.unwrap(); + b = b.unwrap(); + auto htA = a.type.getHeapType(); + auto htB = b.type.getHeapType(); + // What type hierarchy a heap type is in is generally observable. + if (htA.getTop() != htB.getTop()) { + return false; } - if (a != b) { - std::cout << "values not identical! " << a << " != " << b << '\n'; + // Null values are observable. + if (htA.isBottom() || htB.isBottom()) { + return a == b; + } + // String values are observable. + if (htA.isString() || htB.isString()) { + return a == b; + } + // i31 values are observable. + if (htA.isMaybeShared(HeapType::i31) || htB.isMaybeShared(HeapType::i31)) { + return a == b; + } + // External references are observable. (These cannot be externalized + // internal references because they've already been unwrapped.) + if (htA.isMaybeShared(HeapType::ext) || htB.isMaybeShared(HeapType::ext)) { + return a == b; + } + // Configured prototypes are observable. Even if they are also opaque Wasm + // references, their having different pointer identities is observable. + // However, we have no way of comparing pointer identities across + // executions, so just recursively look for externally observable + // differences in the prototypes. + if (!areEqual(a.getJSPrototype(), b.getJSPrototype())) { return false; } + + // Other differences are not observable, so conservatively consider the + // values equal. return true; } @@ -542,6 +580,7 @@ struct ExecutionResults { } for (Index i = 0; i < a.size(); i++) { if (!areEqual(a[i], b[i])) { + std::cout << "values not identical! " << a[i] << " != " << b[i] << '\n'; return false; } } @@ -611,7 +650,13 @@ struct ExecutionResults { } catch (const TrapException&) { return Trap{}; } catch (const WasmException& e) { - std::cout << "[exception thrown: " << e << "]" << std::endl; + auto& exn = *e.exn.getExnData(); + std::cout << "[exception thrown: " << exn.tag->name; + for (auto val : exn.payload) { + std::cout << ' '; + printValue(val); + } + std::cout << "]" << std::endl; return Exception{}; } catch (const HostLimitException&) { // This should be ignored and not compared with, as optimizations can diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index 9b2800a2d84..749b703bc1a 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -467,7 +467,11 @@ struct CtorEvalExternalInterface : EvallingModuleRunner::ExternalInterface { void throwException(const WasmException& exn) override { std::stringstream ss; - ss << "exception thrown: " << exn; + auto& data = *exn.exn.getExnData(); + ss << "exception thrown: " << data.tag->name; + if (!data.payload.empty()) { + ss << ' ' << data.payload; + } throw FailToEvalException(ss.str()); } diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index b0281636bb6..42cc6b494ae 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -61,7 +61,6 @@ namespace wasm { struct WasmException { Literal exn; }; -std::ostream& operator<<(std::ostream& o, const WasmException& exn); // An exception thrown when we try to execute non-constant code, that is, code // that we cannot properly evaluate at compile time (e.g. if it refers to an diff --git a/src/wasm/CMakeLists.txt b/src/wasm/CMakeLists.txt index 07e067c49b8..0f4002cd6e4 100644 --- a/src/wasm/CMakeLists.txt +++ b/src/wasm/CMakeLists.txt @@ -7,7 +7,6 @@ set(wasm_SOURCES wasm-binary.cpp wasm-debug.cpp wasm-emscripten.cpp - wasm-interpreter.cpp wasm-io.cpp wasm-ir-builder.cpp wasm-stack.cpp diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index 1bf14432c90..5b3fcb7a177 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -19,6 +19,7 @@ #include "emscripten-optimizer/simple_ast.h" #include "ir/bits.h" +#include "ir/struct-utils.h" #include "literal.h" #include "pretty_printing.h" #include "support/bits.h" @@ -514,6 +515,12 @@ bool Literal::operator==(const Literal& other) const { return i32 == other.i32; } if (heapType.isMaybeShared(HeapType::ext)) { + if (hasExternPayload()) { + if (!other.hasExternPayload()) { + return false; + } + return getExternPayload() == other.getExternPayload(); + } return internalize() == other.internalize(); } if (heapType.isMaybeShared(HeapType::any)) { @@ -782,7 +789,14 @@ std::ostream& operator<<(std::ostream& o, Literal literal) { assert(literal.isData()); auto data = literal.getGCData(); assert(data); - o << "[ref " << literal.type.getHeapType() << ' ' << data->values << ']'; + o << "[ref " << literal.type.getHeapType() << ' ' << data->values; + if (!data->desc.isNull()) { + if (!data->values.empty()) { + o << ", "; + } + o << "desc=" << data->desc; + } + o << ']'; } } restoreNormalColor(o); @@ -3009,4 +3023,37 @@ Literal Literal::internalize() const { return gcData->values[0]; } +Literal Literal::unwrap() const { + if (!type.isRef()) { + return *this; + } + if (type.getHeapType().isMaybeShared(HeapType::any)) { + // An internalized external reference (possibly a string). + return externalize(); + } + if (type.getHeapType().isMaybeShared(HeapType::ext) && !hasExternPayload()) { + // An externalized internal reference. + return internalize(); + } + // Something other reference that is not wrapped. + return *this; +} + +Literal Literal::getJSPrototype() const { + assert(type.isRef()); + if (auto desc = type.getHeapType().getDescriptorType(); + desc && StructUtils::hasPossibleJSPrototypeField(*desc)) { + auto proto = gcData->desc.getGCData()->values[0].unwrap(); + // Strings and numbers are not valid prototypes, so they appear as null. + // Externref nulls are also converted to nullref. + auto protoType = proto.type.getHeapType(); + if (protoType.isMaybeShared(HeapType::i31) || + protoType.isMaybeShared(HeapType::string) || protoType.isBottom()) { + return Literal::makeNull(HeapType::none); + } + return proto; + } + return Literal::makeNull(HeapType::none); +} + } // namespace wasm diff --git a/src/wasm/wasm-interpreter.cpp b/src/wasm/wasm-interpreter.cpp deleted file mode 100644 index 3af29d2c773..00000000000 --- a/src/wasm/wasm-interpreter.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include "wasm-interpreter.h" - -namespace wasm { - -std::ostream& operator<<(std::ostream& o, const WasmException& exn) { - auto exnData = exn.exn.getExnData(); - return o << exnData->tag->name << " " << exnData->payload; -} - -} // namespace wasm diff --git a/test/lit/exec/cont_many_unhandled.wast b/test/lit/exec/cont_many_unhandled.wast index 6bb71c6eb16..bc221315ef0 100644 --- a/test/lit/exec/cont_many_unhandled.wast +++ b/test/lit/exec/cont_many_unhandled.wast @@ -16,7 +16,7 @@ ) ;; CHECK: [fuzz-exec] calling a - ;; CHECK-NEXT: [exception thrown: tag ()] + ;; CHECK-NEXT: [exception thrown: tag] (func $a (export "a") (resume_throw $cont $tag (cont.new $cont diff --git a/test/lit/exec/eh-print.wast b/test/lit/exec/eh-print.wast index f501646313f..24edab11946 100644 --- a/test/lit/exec/eh-print.wast +++ b/test/lit/exec/eh-print.wast @@ -11,7 +11,7 @@ (type $B (struct (field (mut anyref)))) ;; CHECK: [fuzz-exec] calling array - ;; CHECK-NEXT: [exception thrown: A [ref (type $array.0 (array (mut i32))) (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0[..])]] + ;; CHECK-NEXT: [exception thrown: A object(null)] (func $array (export "array") (result (ref $A)) ;; Throw a very large array. We should not print all 12K items in it, as that ;; would be very verbose. Instead we stop after a reasonable amount and @@ -24,7 +24,7 @@ ) ;; CHECK: [fuzz-exec] calling struct - ;; CHECK-NEXT: [exception thrown: B [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [ref (type $struct.0 (struct (field (mut anyref)))) [..]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] + ;; CHECK-NEXT: [exception thrown: B object(null)] (func $struct (export "struct") (result (ref $B)) (local $x (ref $B)) ;; As above, but now with a recursive struct. diff --git a/test/lit/exec/fuzzing-api.wast b/test/lit/exec/fuzzing-api.wast index e91231229eb..2e08b4e10a9 100644 --- a/test/lit/exec/fuzzing-api.wast +++ b/test/lit/exec/fuzzing-api.wast @@ -40,7 +40,7 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [LoggingExternalInterface logging object] + ;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] @@ -73,7 +73,7 @@ ) ;; CHECK: [fuzz-exec] calling throwing - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] + ;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] (func $throwing (export "throwing") ;; Throwing 0 throws a JS ("private") exception. (call $throw @@ -91,7 +91,7 @@ ) ;; CHECK: [fuzz-exec] calling table.setting - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] + ;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] (func $table.setting (export "table.setting") (call $table.set (i32.const 5) @@ -107,7 +107,7 @@ ;; CHECK: [fuzz-exec] calling table.getting ;; CHECK-NEXT: [LoggingExternalInterface logging 0] ;; CHECK-NEXT: [LoggingExternalInterface logging 1] - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] + ;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] (func $table.getting (export "table.getting") ;; There is a non-null value at 5, and a null at 6. (call $log-i32 @@ -136,11 +136,11 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [LoggingExternalInterface logging object] + ;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] + ;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] (func $export.calling (export "export.calling") ;; At index 0 in the exports we have $logging, so we will do those loggings. (call $call.export @@ -159,11 +159,11 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [LoggingExternalInterface logging object] + ;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] + ;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] (func $export.calling.rethrow (export "export.calling.rethrow") ;; As above, but the second param is different. (call $call.export @@ -183,7 +183,7 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [LoggingExternalInterface logging object] + ;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] @@ -209,11 +209,11 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [LoggingExternalInterface logging object] + ;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] + ;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] (func $ref.calling (export "ref.calling") ;; This will emit some logging. (call $call.ref @@ -232,11 +232,11 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [LoggingExternalInterface logging object] + ;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] + ;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] (func $ref.calling.rethrow (export "ref.calling.rethrow") ;; As with calling an export, when we set the flags to 1 exceptions are ;; caught and rethrown, but there is no noticeable difference here. @@ -255,7 +255,7 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [LoggingExternalInterface logging object] + ;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] @@ -492,7 +492,7 @@ ) ;; CHECK: [fuzz-exec] calling return-externref-exception - ;; CHECK-NEXT: [fuzz-exec] note result: return-externref-exception => object + ;; CHECK-NEXT: [fuzz-exec] note result: return-externref-exception => jserror ;; CHECK-NEXT: warning: no passes specified, not doing any work (func $return-externref-exception (export "return-externref-exception") (result externref) ;; Call JS table.set in a way that throws (on out of bounds). The JS exception @@ -513,50 +513,50 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [LoggingExternalInterface logging object] +;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK: [fuzz-exec] calling throwing -;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] +;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] ;; CHECK: [fuzz-exec] calling throwing-tag ;; CHECK-NEXT: [exception thrown: imported-wasm-tag 42] ;; CHECK: [fuzz-exec] calling table.setting -;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] +;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] ;; CHECK: [fuzz-exec] calling table.getting ;; CHECK-NEXT: [LoggingExternalInterface logging 0] ;; CHECK-NEXT: [LoggingExternalInterface logging 1] -;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] +;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] ;; CHECK: [fuzz-exec] calling export.calling ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [LoggingExternalInterface logging object] +;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] +;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] ;; CHECK: [fuzz-exec] calling export.calling.rethrow ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [LoggingExternalInterface logging object] +;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] +;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] ;; CHECK: [fuzz-exec] calling export.calling.catching ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [LoggingExternalInterface logging object] +;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] @@ -567,27 +567,27 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [LoggingExternalInterface logging object] +;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] +;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] ;; CHECK: [fuzz-exec] calling ref.calling.rethrow ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [LoggingExternalInterface logging object] +;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] +;; CHECK-NEXT: [exception thrown: imported-js-tag jserror] ;; CHECK: [fuzz-exec] calling ref.calling.catching ;; CHECK-NEXT: [LoggingExternalInterface logging 42] ;; CHECK-NEXT: [LoggingExternalInterface logging 3.14159] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [LoggingExternalInterface logging object] +;; CHECK-NEXT: [LoggingExternalInterface logging object(null)] ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] @@ -630,7 +630,7 @@ ;; CHECK-NEXT: [fuzz-exec] note result: do-sleep => 42 ;; CHECK: [fuzz-exec] calling return-externref-exception -;; CHECK-NEXT: [fuzz-exec] note result: return-externref-exception => object +;; CHECK-NEXT: [fuzz-exec] note result: return-externref-exception => jserror ;; CHECK-NEXT: [fuzz-exec] comparing catch-js-tag ;; CHECK-NEXT: [fuzz-exec] comparing do-sleep ;; CHECK-NEXT: [fuzz-exec] comparing export.calling diff --git a/test/lit/exec/tag-cross-module.wast b/test/lit/exec/tag-cross-module.wast index 0bb17c97fee..e0c31b848bf 100644 --- a/test/lit/exec/tag-cross-module.wast +++ b/test/lit/exec/tag-cross-module.wast @@ -17,10 +17,10 @@ ) ;; CHECK: [fuzz-exec] calling func -;; CHECK-NEXT: [exception thrown: tag nullref] +;; CHECK-NEXT: [exception thrown: tag null] ;; CHECK-NEXT: [fuzz-exec] running second module ;; CHECK-NEXT: [fuzz-exec] calling func2-internal -;; CHECK-NEXT: [exception thrown: tag nullref] +;; CHECK-NEXT: [exception thrown: tag null] ;; CHECK-NEXT: [fuzz-exec] calling func2-imported ;; CHECK-NEXT: func2-imported => null