Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 27 additions & 16 deletions src/literal.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@

#include <array>
#include <iostream>
#include <variant>

#include "compiler-support.h"
#include "support/hash.h"
#include "support/name.h"
#include "support/small_vector.h"
Expand All @@ -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];
Expand All @@ -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> gcData;
// A reference to Exn data.
std::shared_ptr<ExnData> exnData;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps hasExternrefPayload / getExternrefPayload etc., to clarify this isn't a generic payload?

int64_t geti64() const {
assert(type == Type::i64);
return i64;
Expand Down Expand Up @@ -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
Expand Down
9 changes: 2 additions & 7 deletions src/tools/execution-results.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<GCData>(empty, Literals{}), empty);
Literals arguments = {inner.externalize()};
// JS exceptions contain an externref.
Literals arguments = {Literal::makeExtern(0, Unshared)};
auto payload = std::make_shared<ExnData>(&jsTag, arguments);
throwException(WasmException{Literal(payload)});
}
Expand Down
3 changes: 1 addition & 2 deletions src/wasm-interpreter.h
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,7 @@ class ExpressionRunner : public OverriddenVisitor<SubType, Flow> {
Literal makeGCData(Literals&& data,
Type type,
Literal desc = Literal::makeNull(HeapType::none)) {
auto allocation =
std::make_shared<GCData>(type.getHeapType(), std::move(data), desc);
auto allocation = std::make_shared<GCData>(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
Expand Down
124 changes: 73 additions & 51 deletions src/wasm/literal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
#include <cmath>

#include "emscripten-optimizer/simple_ast.h"
#include "fp16.h"
#include "ir/bits.h"
#include "literal.h"
#include "pretty_printing.h"
Expand Down Expand Up @@ -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");
}

Expand All @@ -90,14 +95,14 @@ Literal::Literal(std::shared_ptr<GCData> 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> exnData)
Expand All @@ -111,15 +116,15 @@ Literal::Literal(std::shared_ptr<ContData> 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);
for (size_t i = 0; i < string.size(); i += 2) {
int32_t u = uint8_t(string[i]) | (uint8_t(string[i + 1]) << 8);
contents.push_back(Literal(u));
}
gcData = std::make_shared<GCData>(HeapType::string, std::move(contents));
gcData = std::make_shared<GCData>(std::move(contents));
}

Literal::Literal(const Literal& other) : type(other.type) {
Expand Down Expand Up @@ -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<GCData>(other.gcData);
return;
}
Expand All @@ -169,20 +174,25 @@ Literal::Literal(const Literal& other) : type(other.type) {
case HeapType::exn:
new (&exnData) std::shared_ptr<ExnData>(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<GCData>(other.gcData);
}
return;
}
case HeapType::any:
// Internalized external reference or string.
new (&gcData) std::shared_ptr<GCData>(other.gcData);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep the code below that clarifies this is a string?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can expand the comment, but the gcData no longer has a type field for an assertion to look at. I could assert gcData.values[0].type.getHeapType().isMaybeShared(HeapType::ext), but I don't think it's worth it.

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<GCData>(other.gcData);
return;
case HeapType::eq:
case HeapType::func:
case HeapType::cont:
Expand All @@ -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();
Expand Down Expand Up @@ -363,7 +377,8 @@ std::shared_ptr<FuncData> Literal::getFuncData() const {
}

std::shared_ptr<GCData> Literal::getGCData() const {
assert(isNull() || isData());
assert(isNull() || isData() ||
(type.isRef() && type.getHeapType().isMaybeShared(HeapType::ext)));
return gcData;
}

Expand Down Expand Up @@ -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;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this change from printing a string?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyref literals are now either internalized strings or other internalized extern references. We don't always have a string to print.

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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<GCData>{}, HeapTypes::noext.getBasic(share));
}
auto heapType = type.getHeapType();
auto extType = HeapTypes::ext.getBasic(share);
if (heapType.isMaybeShared(HeapType::i31)) {
return Literal(std::make_shared<GCData>(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<GCData>(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<GCData>{}, 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<GCData>(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
4 changes: 2 additions & 2 deletions test/lit/exec/cont_export.wast
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion test/lit/exec/cont_export_throw.wast
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading
Loading