From da7829ef86274dd91ec751d162888add1ce72ae7 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 24 Feb 2026 20:57:39 -0800 Subject: [PATCH 1/5] Represent externref payloads in Literals Literals could previously represent externalized internal references and strings, but they could not represent externref values that actually represent external references. Fix this by using the `i32` field to hold externref payloads in the case where they are not externalized references or strings. Strings literals are already differentiated because they have type `(ref string)`, but that still leaves external references and externalized internal references to be differentiated. They were previously differentiated using the `type` field in GCData, but that field was otherwise redundant wit the `type` field in the outer Literal. Remove `type` from GCData and instead use the low bit of the i32/shared_ptr union to mark external references. This depends on `shared_ptr` not using that low bit. --- src/literal.h | 40 +++++---- src/tools/execution-results.h | 9 +- src/wasm-interpreter.h | 3 +- src/wasm/literal.cpp | 122 ++++++++++++++++----------- test/lit/exec/cont_export.wast | 4 +- test/lit/exec/cont_export_throw.wast | 2 +- test/lit/exec/fuzzing-api.wast | 28 +++--- 7 files changed, 115 insertions(+), 93 deletions(-) diff --git a/src/literal.h b/src/literal.h index 8b5a8052676..1f53a9c00ed 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,10 @@ 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 are also stored here, with their low bit set to + // differentiate an externref with a payload from an externalized internal + // reference, which uses the gcData field instead. int32_t i32; int64_t i64; uint8_t v128[16]; @@ -54,14 +56,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 +260,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 +302,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 hasPayload() const { + assert(type.getHeapType().isMaybeShared(HeapType::ext)); + return (i32 & 1) == 1; + } + int32_t getPayload() const { + assert(hasPayload()); + return int32_t(uint32_t(i32) >> 1); + } int64_t geti64() const { assert(type == Type::i64); return i64; @@ -775,19 +785,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 @@ -834,7 +840,7 @@ template<> struct hash { return digest; } if (type.isMaybeShared(wasm::HeapType::any)) { - // This may be an extern string that was internalized to |any|. Undo + // This may be an extern string that was 9d to |any|. Undo // that to get the actual value. (Rehash here with the existing digest, // which contains the |any| type, so that the final hash takes into // account the fact that it was internalized.) 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 484afb33294..ca65378d27d 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..886bbab9442 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) @@ -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.hasPayload()) { + i32 = other.i32; + } else { + // Externalized internal reference. + new (&gcData) std::shared_ptr(other.gcData); + } + return; + } + case HeapType::any: + // Internalized external reference. + 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) && !hasPayload()) { + // 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.hasPayload()) { + // Externref payload + o << "externref(" << literal.getPayload() << ")"; + } 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; 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() || hasPayload()) { + // 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] From ff9555a5081295cf9b6b79514c88cbf52f904cc9 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 26 Feb 2026 11:50:42 -0800 Subject: [PATCH 2/5] correct keyboard flailing --- src/literal.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/literal.h b/src/literal.h index 1f53a9c00ed..77465267c6a 100644 --- a/src/literal.h +++ b/src/literal.h @@ -840,7 +840,7 @@ template<> struct hash { return digest; } if (type.isMaybeShared(wasm::HeapType::any)) { - // This may be an extern string that was 9d to |any|. Undo + // This may be an extern string that was internalized to |any|. Undo // that to get the actual value. (Rehash here with the existing digest, // which contains the |any| type, so that the final hash takes into // account the fact that it was internalized.) From a0da4373babd08f4a52c7ef47c9a375e3f3fcda8 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 27 Feb 2026 12:36:50 -0800 Subject: [PATCH 3/5] docs in comment --- src/literal.h | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/literal.h b/src/literal.h index 77465267c6a..c70f1a0d8d1 100644 --- a/src/literal.h +++ b/src/literal.h @@ -43,9 +43,14 @@ class Literal { // the value if there is one, and the highest bit containing whether there // is a value. Thus, a null is |i32 === 0|. // - // Externref payloads are also stored here, with their low bit set to - // differentiate an externref with a payload from an externalized internal - // reference, which uses the gcData field instead. + // 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]; From b2d67fb3c8321621eda21cdaade4704a6322195a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 27 Feb 2026 14:15:28 -0800 Subject: [PATCH 4/5] ExternPayload methods --- src/literal.h | 6 +++--- src/wasm/literal.cpp | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/literal.h b/src/literal.h index c70f1a0d8d1..80e58a061bd 100644 --- a/src/literal.h +++ b/src/literal.h @@ -307,12 +307,12 @@ 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 hasPayload() const { + bool hasExternPayload() const { assert(type.getHeapType().isMaybeShared(HeapType::ext)); return (i32 & 1) == 1; } - int32_t getPayload() const { - assert(hasPayload()); + int32_t getExternPayload() const { + assert(hasExternPayload()); return int32_t(uint32_t(i32) >> 1); } int64_t geti64() const { diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index 886bbab9442..b1d335ac406 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -175,7 +175,7 @@ Literal::Literal(const Literal& other) : type(other.type) { new (&exnData) std::shared_ptr(other.exnData); return; case HeapType::ext: { - if (other.hasPayload()) { + if (other.hasExternPayload()) { i32 = other.i32; } else { // Externalized internal reference. @@ -209,7 +209,7 @@ Literal::~Literal() { if (type.isBasic()) { return; } - if (type.getHeapType().isMaybeShared(HeapType::ext) && !hasPayload()) { + if (type.getHeapType().isMaybeShared(HeapType::ext) && !hasExternPayload()) { // Externalized internal reference. gcData.~shared_ptr(); return; @@ -722,9 +722,9 @@ std::ostream& operator<<(std::ostream& o, Literal literal) { break; } case HeapType::ext: { - if (literal.hasPayload()) { + if (literal.hasExternPayload()) { // Externref payload - o << "externref(" << literal.getPayload() << ")"; + o << "externref(" << literal.getExternPayload() << ")"; } else { // Externalized internal reference. o << "externalized " << literal.internalize(); @@ -3001,7 +3001,7 @@ Literal Literal::internalize() const { auto none = HeapTypes::none.getBasic(heapType.getShared()); return Literal(nullptr, none); } - if (isString() || hasPayload()) { + 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); From 8e9ecf12bde6e6aedd4a06965e3bb862b7de482e Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 27 Feb 2026 14:18:57 -0800 Subject: [PATCH 5/5] comments --- src/wasm/literal.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index b1d335ac406..1b35ac605d7 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -116,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); @@ -184,7 +184,7 @@ Literal::Literal(const Literal& other) : type(other.type) { return; } case HeapType::any: - // Internalized external reference. + // Internalized external reference or string. new (&gcData) std::shared_ptr(other.gcData); return; case HeapType::none: @@ -2984,7 +2984,7 @@ Literal Literal::externalize() const { return Literal(nullptr, noext); } if (heapType.isMaybeShared(HeapType::any)) { - // This is an internalized externref; just unwrap it. + // This is an internalized externref or string; just unwrap it. assert(gcData->values.size() == 1); return gcData->values[0]; }