diff --git a/src/literal.h b/src/literal.h index 8b5a8052676..80e58a061bd 100644 --- a/src/literal.h +++ b/src/literal.h @@ -19,9 +19,7 @@ #include #include -#include -#include "compiler-support.h" #include "support/hash.h" #include "support/name.h" #include "support/small_vector.h" @@ -44,6 +42,15 @@ class Literal { // Note: i31 is stored in the |i32| field, with the lower 31 bits containing // the value if there is one, and the highest bit containing whether there // is a value. Thus, a null is |i32 === 0|. + // + // Externref payloads, which serve to differentiate different external + // references but are otherwise meaningless, are also stored in the i32 + // field, with their low bit set to differentiate an externref with a + // payload from an externalized internal reference, which uses the gcData + // field instead. This scheme supports 31 bits of payload for externrefs, + // which should be sufficient for spec test and fuzzing purposes, but if we + // need more bits we can use the i64 field instead. This scheme also depends + // on the low bit of a shared_ptr not being used. int32_t i32; int64_t i64; uint8_t v128[16]; @@ -54,14 +61,9 @@ class Literal { // Array, and for a Struct, is just the fields in order). The type is used // to indicate whether this is a Struct or an Array, and of what type. We // also use this to store String data, as it is similarly stored on the - // heap. For externrefs, the gcData is the same as for the corresponding - // internal references and the values are only differentiated by the type. - // Externalized i31 references have a gcData containing the internal i31 - // reference as its sole value even though internal i31 references do not - // have a gcData. - // - // Note that strings can be internalized, in which case they keep the same - // gcData, but their type becomes anyref. + // heap. For externalized or internalized references (including strings), + // gcData holds a single value, which is the wrapped internal or external + // reference. std::shared_ptr gcData; // A reference to Exn data. std::shared_ptr exnData; @@ -263,6 +265,11 @@ class Literal { lit.i32 = value | 0x80000000; return lit; } + static Literal makeExtern(int32_t payload, Shareability share) { + auto lit = Literal(Type(HeapTypes::ext.getBasic(share), NonNullable)); + lit.i32 = (payload << 1) | 1; + return lit; + } // Wasm has nondeterministic rules for NaN propagation in some operations. For // example. f32.neg is deterministic and just flips the sign, even of a NaN, // but f32.add is nondeterministic, and if one or more of the inputs is a NaN, @@ -300,6 +307,14 @@ class Literal { // Cast to unsigned for the left shift to avoid undefined behavior. return signed_ ? int32_t((uint32_t(i32) << 1)) >> 1 : (i32 & 0x7fffffff); } + bool hasExternPayload() const { + assert(type.getHeapType().isMaybeShared(HeapType::ext)); + return (i32 & 1) == 1; + } + int32_t getExternPayload() const { + assert(hasExternPayload()); + return int32_t(uint32_t(i32) >> 1); + } int64_t geti64() const { assert(type == Type::i64); return i64; @@ -775,19 +790,15 @@ std::ostream& operator<<(std::ostream& o, wasm::Literals literals); // A GC Struct, Array, or String is a set of values with a type saying how it // should be interpreted. struct GCData { - // The type of this struct, array, or string. - HeapType type; - // The element or field values. Literals values; // The descriptor, if it exists, or null. Literal desc; - GCData(HeapType type, - Literals&& values, + GCData(Literals&& values, const Literal& desc = Literal::makeNull(HeapType::none)) - : type(type), values(std::move(values)), desc(desc) {} + : values(std::move(values)), desc(desc) {} }; } // namespace wasm diff --git a/src/tools/execution-results.h b/src/tools/execution-results.h index f5f55bdaaee..4c377f21969 100644 --- a/src/tools/execution-results.h +++ b/src/tools/execution-results.h @@ -268,13 +268,8 @@ struct LoggingExternalInterface : public ShellExternalInterface { } void throwJSException() { - // JS exceptions contain an externref. Use the same type of value as a JS - // exception would have, which is a reference to an object, and which will - // print out "object" in the logging from JS. A trivial struct is enough for - // us to log the same thing here. - auto empty = HeapType(Struct{}); - auto inner = Literal(std::make_shared(empty, Literals{}), empty); - Literals arguments = {inner.externalize()}; + // JS exceptions contain an externref. + Literals arguments = {Literal::makeExtern(0, Unshared)}; auto payload = std::make_shared(&jsTag, arguments); throwException(WasmException{Literal(payload)}); } diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index 48404207a60..f0058310b95 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -362,8 +362,7 @@ class ExpressionRunner : public OverriddenVisitor { Literal makeGCData(Literals&& data, Type type, Literal desc = Literal::makeNull(HeapType::none)) { - auto allocation = - std::make_shared(type.getHeapType(), std::move(data), desc); + auto allocation = std::make_shared(std::move(data), desc); #if __has_feature(leak_sanitizer) || __has_feature(address_sanitizer) // GC data with cycles will leak, since shared_ptrs do not handle cycles. // Binaryen is generally not used in long-running programs so we just ignore diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index 3e2dcab19b0..1b35ac605d7 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -18,7 +18,6 @@ #include #include "emscripten-optimizer/simple_ast.h" -#include "fp16.h" #include "ir/bits.h" #include "literal.h" #include "pretty_printing.h" @@ -64,6 +63,12 @@ Literal::Literal(Type type) : type(type) { return; } + if (type.isRef() && type.getHeapType().isMaybeShared(HeapType::ext)) { + assert(type.isNonNullable()); + i32 = 1; + return; + } + WASM_UNREACHABLE("Unexpected literal type"); } @@ -90,14 +95,14 @@ Literal::Literal(std::shared_ptr gcData, HeapType type) : gcData(gcData), type(type, gcData ? NonNullable : Nullable, gcData && !type.isBasic() ? Exact : Inexact) { - // The type must be a proper type for GC data: either a struct, array, or - // string; or an externalized version of the same; or a null; or an - // internalized string (which appears as an anyref). + // The type must be a proper type for GC data: either a struct, array, or i31 + // or an externalized version of the same; or a null; or a string or extern, + // or an internalized version of the same. assert((isData() && gcData) || (type.isMaybeShared(HeapType::ext) && gcData) || - (type.isBottom() && !gcData) || - (type.isMaybeShared(HeapType::any) && gcData && - gcData->type.isMaybeShared(HeapType::string))); + (type.isMaybeShared(HeapType::string) && gcData) || + (type.isMaybeShared(HeapType::any) && gcData) || + (type.isBottom() && !gcData)); } Literal::Literal(std::shared_ptr exnData) @@ -111,7 +116,7 @@ Literal::Literal(std::shared_ptr contData) Literal::Literal(std::string_view string) : gcData(nullptr), type(Type(HeapType::string, NonNullable)) { - // TODO: we could in theory internalize strings + // TODO: we could in theory intern strings // Extract individual WTF-16LE code units. Literals contents; assert(string.size() % 2 == 0); @@ -119,7 +124,7 @@ Literal::Literal(std::string_view string) int32_t u = uint8_t(string[i]) | (uint8_t(string[i + 1]) << 8); contents.push_back(Literal(u)); } - gcData = std::make_shared(HeapType::string, std::move(contents)); + gcData = std::make_shared(std::move(contents)); } Literal::Literal(const Literal& other) : type(other.type) { @@ -150,7 +155,7 @@ Literal::Literal(const Literal& other) : type(other.type) { assert(!type.isNullable()); auto heapType = type.getHeapType(); - if (other.isData() || heapType.isMaybeShared(HeapType::ext)) { + if (other.isData()) { new (&gcData) std::shared_ptr(other.gcData); return; } @@ -169,20 +174,25 @@ Literal::Literal(const Literal& other) : type(other.type) { case HeapType::exn: new (&exnData) std::shared_ptr(other.exnData); return; - case HeapType::ext: - WASM_UNREACHABLE("handled above with isData()"); + case HeapType::ext: { + if (other.hasExternPayload()) { + i32 = other.i32; + } else { + // Externalized internal reference. + new (&gcData) std::shared_ptr(other.gcData); + } + return; + } + case HeapType::any: + // Internalized external reference or string. + new (&gcData) std::shared_ptr(other.gcData); + return; case HeapType::none: case HeapType::noext: case HeapType::nofunc: case HeapType::noexn: case HeapType::nocont: WASM_UNREACHABLE("null literals should already have been handled"); - case HeapType::any: - // This must be an anyref literal, which is an internalized string. - assert(other.gcData && - other.gcData->type.isMaybeShared(HeapType::string)); - new (&gcData) std::shared_ptr(other.gcData); - return; case HeapType::eq: case HeapType::func: case HeapType::cont: @@ -199,8 +209,12 @@ Literal::~Literal() { if (type.isBasic()) { return; } - if (isNull() || isData() || type.getHeapType().isMaybeShared(HeapType::ext) || - type.getHeapType().isMaybeShared(HeapType::any)) { + if (type.getHeapType().isMaybeShared(HeapType::ext) && !hasExternPayload()) { + // Externalized internal reference. + gcData.~shared_ptr(); + return; + } + if (isNull() || isData() || type.getHeapType().isMaybeShared(HeapType::any)) { gcData.~shared_ptr(); } else if (isFunction()) { funcData.~shared_ptr(); @@ -363,7 +377,8 @@ std::shared_ptr Literal::getFuncData() const { } std::shared_ptr Literal::getGCData() const { - assert(isNull() || isData()); + assert(isNull() || isData() || + (type.isRef() && type.getHeapType().isMaybeShared(HeapType::ext))); return gcData; } @@ -700,9 +715,22 @@ std::ostream& operator<<(std::ostream& o, Literal literal) { case HeapType::nocont: o << "nullcontref"; break; - case HeapType::ext: - o << "externref"; + case HeapType::any: { + auto data = literal.getGCData(); + assert(data->values.size() == 1); + o << "internalized " << literal.getGCData()->values[0]; + break; + } + case HeapType::ext: { + if (literal.hasExternPayload()) { + // Externref payload + o << "externref(" << literal.getExternPayload() << ")"; + } else { + // Externalized internal reference. + o << "externalized " << literal.internalize(); + } break; + } case HeapType::exn: o << "exnref"; break; @@ -712,8 +740,6 @@ std::ostream& operator<<(std::ostream& o, Literal literal) { case HeapType::struct_: case HeapType::array: WASM_UNREACHABLE("invalid type"); - case HeapType::any: - // Anyref literals contain strings. case HeapType::string: { auto data = literal.getGCData(); if (!data) { @@ -756,7 +782,7 @@ std::ostream& operator<<(std::ostream& o, Literal literal) { assert(literal.isData()); auto data = literal.getGCData(); assert(data); - o << "[ref " << data->type << ' ' << data->values << ']'; + o << "[ref " << literal.type.getHeapType() << ' ' << data->values << ']'; } } restoreNormalColor(o); @@ -2952,41 +2978,37 @@ Literal Literal::relaxedNmaddF64x2(const Literal& left, Literal Literal::externalize() const { assert(type.isRef() && type.getHeapType().getUnsharedTop() == HeapType::any && "can only externalize internal references"); - auto share = type.getHeapType().getShared(); - if (isNull()) { - return Literal(std::shared_ptr{}, HeapTypes::noext.getBasic(share)); - } auto heapType = type.getHeapType(); - auto extType = HeapTypes::ext.getBasic(share); - if (heapType.isMaybeShared(HeapType::i31)) { - return Literal(std::make_shared(heapType, Literals{*this}), - extType); + if (isNull()) { + auto noext = HeapTypes::noext.getBasic(heapType.getShared()); + return Literal(nullptr, noext); } if (heapType.isMaybeShared(HeapType::any)) { - // Anyref literals turn into strings (if we add any other anyref literals, - // we will need to be more careful here). - return Literal(gcData, HeapTypes::string.getBasic(share)); + // This is an internalized externref or string; just unwrap it. + assert(gcData->values.size() == 1); + return gcData->values[0]; } - return Literal(gcData, extType); + // This is an internal reference. Wrap it. + auto ext = HeapTypes::ext.getBasic(heapType.getShared()); + return Literal(std::make_shared(Literals{*this}), ext); } Literal Literal::internalize() const { - auto share = type.getHeapType().getShared(); - assert( - Type::isSubType(type, Type(HeapTypes::ext.getBasic(share), Nullable)) && - "can only internalize external references"); + assert(type.isRef() && type.getHeapType().getUnsharedTop() == HeapType::ext && + "can only internalize external references"); + auto heapType = type.getHeapType(); if (isNull()) { - return Literal(std::shared_ptr{}, HeapTypes::none.getBasic(share)); - } - if (gcData->type.isMaybeShared(HeapType::i31)) { - assert(gcData->values[0].type.getHeapType().isMaybeShared(HeapType::i31)); - return gcData->values[0]; + auto none = HeapTypes::none.getBasic(heapType.getShared()); + return Literal(nullptr, none); } - if (gcData->type.isMaybeShared(HeapType::string)) { - // Strings turn into anyref literals. - return Literal(gcData, HeapTypes::any.getBasic(share)); + if (isString() || hasExternPayload()) { + // This is an external reference. Wrap it. + auto any = HeapTypes::any.getBasic(heapType.getShared()); + return Literal(std::make_shared(Literals{*this}), any); } - return Literal(gcData, gcData->type); + // This is an externalized internal reference; just unwrap it. + assert(gcData->values.size() == 1); + return gcData->values[0]; } } // namespace wasm diff --git a/test/lit/exec/cont_export.wast b/test/lit/exec/cont_export.wast index 8576e74bf0b..7775fa485a1 100644 --- a/test/lit/exec/cont_export.wast +++ b/test/lit/exec/cont_export.wast @@ -24,7 +24,7 @@ ;; CHECK: [fuzz-exec] calling call-call-export ;; CHECK-NEXT: [LoggingExternalInterface logging 10] - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref] + ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] (func $call-call-export (export "call-call-export") ;; Call suspend as an export. We cannot suspend through JS, so we throw. (call $call-export @@ -35,7 +35,7 @@ ;; CHECK: [fuzz-exec] calling handled ;; CHECK-NEXT: [LoggingExternalInterface logging 10] - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref] + ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] (func $handled (export "handled") ;; As above, but inside a continuation, so it would be handled - if we could ;; suspend though JS. But we can't, so we throw. diff --git a/test/lit/exec/cont_export_throw.wast b/test/lit/exec/cont_export_throw.wast index 8946caecb04..3b11bfb91ee 100644 --- a/test/lit/exec/cont_export_throw.wast +++ b/test/lit/exec/cont_export_throw.wast @@ -22,7 +22,7 @@ ) ;; CHECK: [fuzz-exec] calling handled - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref] + ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] (func $handled (export "handled") (drop (block $block (result (ref $cont)) diff --git a/test/lit/exec/fuzzing-api.wast b/test/lit/exec/fuzzing-api.wast index ef34fc61df8..00db75a19be 100644 --- a/test/lit/exec/fuzzing-api.wast +++ b/test/lit/exec/fuzzing-api.wast @@ -73,7 +73,7 @@ ) ;; CHECK: [fuzz-exec] calling throwing - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref] + ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] (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] + ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] (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] + ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] (func $table.getting (export "table.getting") ;; There is a non-null value at 5, and a null at 6. (call $log-i32 @@ -140,7 +140,7 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref] + ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] (func $export.calling (export "export.calling") ;; At index 0 in the exports we have $logging, so we will do those loggings. (call $call.export @@ -163,7 +163,7 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref] + ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] (func $export.calling.rethrow (export "export.calling.rethrow") ;; As above, but the second param is different. (call $call.export @@ -213,7 +213,7 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref] + ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] (func $ref.calling (export "ref.calling") ;; This will emit some logging. (call $call.ref @@ -236,7 +236,7 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] - ;; CHECK-NEXT: [exception thrown: imported-js-tag externref] + ;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] (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. @@ -476,18 +476,18 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK: [fuzz-exec] calling throwing -;; CHECK-NEXT: [exception thrown: imported-js-tag externref] +;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] ;; 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] +;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] ;; CHECK: [fuzz-exec] calling table.getting ;; CHECK-NEXT: [LoggingExternalInterface logging 0] ;; CHECK-NEXT: [LoggingExternalInterface logging 1] -;; CHECK-NEXT: [exception thrown: imported-js-tag externref] +;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] ;; CHECK: [fuzz-exec] calling export.calling ;; CHECK-NEXT: [LoggingExternalInterface logging 42] @@ -497,7 +497,7 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [exception thrown: imported-js-tag externref] +;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] ;; CHECK: [fuzz-exec] calling export.calling.rethrow ;; CHECK-NEXT: [LoggingExternalInterface logging 42] @@ -507,7 +507,7 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [exception thrown: imported-js-tag externref] +;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] ;; CHECK: [fuzz-exec] calling export.calling.catching ;; CHECK-NEXT: [LoggingExternalInterface logging 42] @@ -528,7 +528,7 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [exception thrown: imported-js-tag externref] +;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] ;; CHECK: [fuzz-exec] calling ref.calling.rethrow ;; CHECK-NEXT: [LoggingExternalInterface logging 42] @@ -538,7 +538,7 @@ ;; CHECK-NEXT: [LoggingExternalInterface logging function] ;; CHECK-NEXT: [LoggingExternalInterface logging null] ;; CHECK-NEXT: [LoggingExternalInterface logging null] -;; CHECK-NEXT: [exception thrown: imported-js-tag externref] +;; CHECK-NEXT: [exception thrown: imported-js-tag externref(0)] ;; CHECK: [fuzz-exec] calling ref.calling.catching ;; CHECK-NEXT: [LoggingExternalInterface logging 42]