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
349 changes: 170 additions & 179 deletions src/wasm/literal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include <cassert>
#include <cmath>
#include <unordered_map>

#include "emscripten-optimizer/simple_ast.h"
#include "ir/bits.h"
Expand Down Expand Up @@ -699,208 +700,198 @@ void Literal::printVec128(std::ostream& o, const std::array<uint8_t, 16>& v) {
o << std::dec;
}

// Printing literals is mainly for debugging purposes, and they can be of
// massive size or even infinitely recursive, so abbreviate past some point.
// We do so by tracking how much we've printed so far, and whether we are the
// "toplevel" call (the entry point from somewhere else), and we reset the
// count when we leave the toplevel call.
namespace {
struct PrintLimiter {
static const size_t PRINT_LIMIT = 100;
static thread_local size_t printed;

bool isTopLevel;

PrintLimiter() : isTopLevel(printed == 0) { printed++; }

~PrintLimiter() {
if (isTopLevel) {
printed = 0;
}
}

bool stop() { return printed >= PRINT_LIMIT; }
};

thread_local size_t PrintLimiter::printed = 0;

} // namespace

std::ostream& operator<<(std::ostream& o, Literal literal) {
PrintLimiter limiter;

prepareMinorColor(o);
assert(literal.type.isSingle());
if (literal.type.isBasic()) {
switch (literal.type.getBasic()) {
case Type::none:
o << "?";
break;
case Type::i32:
o << literal.geti32();
break;
case Type::i64:
o << literal.geti64();
break;
case Type::f32:
literal.printFloat(o, literal.getf32());
break;
case Type::f64:
literal.printDouble(o, literal.getf64());
break;
case Type::v128:
o << "i32x4 ";
literal.printVec128(o, literal.getv128());
break;
case Type::unreachable:
WASM_UNREACHABLE("unexpected type");
}
} else {
assert(literal.type.isRef());
auto heapType = literal.type.getHeapType();
if (heapType.isShared()) {
o << "shared ";
}
if (heapType.isBasic()) {
switch (heapType.getBasic(Unshared)) {
case HeapType::i31:
o << "i31ref(" << literal.geti31() << ")";
break;
case HeapType::none:
o << "nullref";
struct LiteralPrinter {
std::ostream& o;
// Track the depths of active GCData objects to detect cycles and print them
// using relative depths.
std::unordered_map<GCData*, size_t> depths;
Comment on lines +705 to +709

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using std::unordered_map here introduces unnecessary heap allocation and hashing overhead for tracking active ancestors during printing. Since the recursion depth is typically very small, a simple std::vector with linear search is much more efficient and avoids dynamic memory allocation overhead.

struct LiteralPrinter {
  std::ostream& o;
  // Track the depths of active GCData objects to detect cycles and print them
  // using relative depths.
  std::vector<GCData*> depths;


std::ostream& print(const Literal& literal) {
prepareMinorColor(o);
assert(literal.type.isSingle());
if (literal.type.isBasic()) {
switch (literal.type.getBasic()) {
case Type::none:
o << "?";
break;
case HeapType::noext:
o << "nullexternref";
case Type::i32:
o << literal.geti32();
break;
case HeapType::nofunc:
o << "nullfuncref";
case Type::i64:
o << literal.geti64();
break;
case HeapType::noexn:
o << "nullexnref";
case Type::f32:
literal.printFloat(o, literal.getf32());
break;
case HeapType::nocont:
o << "nullcontref";
case Type::f64:
literal.printDouble(o, literal.getf64());
break;
case HeapType::nowaitqueue:
o << "nullwaitqueue";
case Type::v128:
o << "i32x4 ";
literal.printVec128(o, literal.getv128());
break;
case HeapType::any: {
auto data = literal.getGCData();
assert(data->getLiterals().size() == 1);
o << "internalized " << data->getLiterals()[0];
break;
}
case HeapType::ext: {
if (literal.hasExternPayload()) {
// Externref payload
o << "externref(" << literal.getExternPayload() << ")";
} else {
// Externalized internal reference.
o << "externalized " << literal.internalize();
case Type::unreachable:
WASM_UNREACHABLE("unexpected type");
}
} else {
assert(literal.type.isRef());
auto heapType = literal.type.getHeapType();
if (heapType.isShared()) {
o << "shared ";
}
if (heapType.isBasic()) {
switch (heapType.getBasic(Unshared)) {
case HeapType::i31:
o << "i31ref(" << literal.geti31() << ")";
break;
case HeapType::none:
o << "nullref";
break;
case HeapType::noext:
o << "nullexternref";
break;
case HeapType::nofunc:
o << "nullfuncref";
break;
case HeapType::noexn:
o << "nullexnref";
break;
case HeapType::nocont:
o << "nullcontref";
break;
case HeapType::nowaitqueue:
o << "nullwaitqueue";
break;
case HeapType::any: {
auto data = literal.getGCData();
assert(data->getLiterals().size() == 1);
o << "internalized ";
print(data->getLiterals()[0]);
break;
}
break;
}
case HeapType::exn:
o << "exnref";
break;
case HeapType::eq:
case HeapType::func:
case HeapType::cont:
case HeapType::struct_:
case HeapType::array:
WASM_UNREACHABLE("invalid type");
case HeapType::string: {
auto data = literal.getGCData();
if (!data) {
o << "nullstring";
} else {
o << "string(";
// Convert WTF-16 literals to WTF-16 string.
std::stringstream wtf16;
for (auto c : data->getLiterals()) {
auto u = c.getInteger();
assert(u < 0x10000);
wtf16 << uint8_t(u & 0xFF);
wtf16 << uint8_t(u >> 8);
case HeapType::ext: {
if (literal.hasExternPayload()) {
// Externref payload
o << "externref(" << literal.getExternPayload() << ")";
} else {
// Externalized internal reference.
o << "externalized ";
print(literal.internalize());
}
// Escape to ensure we have valid unicode output and to make
// unprintable characters visible.
// TODO: Use wtf16.view() once we have C++20.
String::printEscapedJSON(o, wtf16.str());
o << ")";
break;
}
case HeapType::exn:
o << "exnref";
break;
case HeapType::eq:
case HeapType::func:
case HeapType::cont:
case HeapType::struct_:
case HeapType::array:
WASM_UNREACHABLE("invalid type");
case HeapType::string: {
auto data = literal.getGCData();
if (!data) {
o << "nullstring";
} else {
o << "string(";
// Convert WTF-16 literals to WTF-16 string.
std::stringstream wtf16;
for (auto c : data->getLiterals()) {
auto u = c.getInteger();
assert(u < 0x10000);
wtf16 << uint8_t(u & 0xFF);
wtf16 << uint8_t(u >> 8);
}
// Escape to ensure we have valid unicode output and to make
// unprintable characters visible.
// TODO: Use wtf16.view() once we have C++20.
String::printEscapedJSON(o, wtf16.str());
Comment on lines +810 to +811

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we use .view() now?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes. Let's address that separately since this is existing code. (Just the indentation changed.)

o << ")";
}
break;
}
case HeapType::waitqueue: {
o << "waitqueue";
break;
}
break;
}
case HeapType::waitqueue: {
o << "waitqueue";
break;
} else if (heapType.isSignature()) {
o << "funcref(" << literal.getFunc() << ")";
} else if (heapType.isContinuation()) {
auto data = literal.getContData();
o << "cont(" << data->func << ' ' << data->type;
if (data->resumeExpr) {
o << " resumeExpr=" << reinterpret_cast<size_t>(data->resumeExpr);
}
}
} else if (heapType.isSignature()) {
o << "funcref(" << literal.getFunc() << ")";
} else if (heapType.isContinuation()) {
auto data = literal.getContData();
o << "cont(" << data->func << ' ' << data->type;
if (data->resumeExpr) {
o << " resumeExpr=" << reinterpret_cast<size_t>(data->resumeExpr);
}
if (!data->resumeInfo.empty()) {
o << " |resumeInfo|=" << data->resumeInfo.size();
}
if (!data->resumeArguments.empty()) {
o << " resumeArguments=" << data->resumeArguments;
}
o << " executed=" << data->executed << ')';
} else {
assert(literal.isData());
auto data = literal.getGCData();
assert(data);
o << "[ref " << literal.type.getHeapType() << ' ';
for (size_t i = 0; i < literal.getNumElements(); i++) {
if (i > 0) {
o << ' ';
if (!data->resumeInfo.empty()) {
o << " |resumeInfo|=" << data->resumeInfo.size();
}
o << literal.getElement(i);
}
if (!data->desc.isNull()) {
if (literal.getNumElements() > 0) {
o << ", ";
if (!data->resumeArguments.empty()) {
o << " resumeArguments=";
print(data->resumeArguments);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[HIGH | Confidence: 5/5 | BUG]

Continuations (ContData) can form reference cycles, for instance when a continuation is resumed and passes itself or another continuation as part of its resumeArguments. Because depths only tracks GCData* (std::unordered_map<GCData*, size_t>), ContData allocations are never recorded in depths during traversal.

With PrintLimiter removed, traversing a continuation whose resumeArguments references a continuation in a cycle causes unbounded recursive calls to print(data->resumeArguments) and leads to a stack overflow crash. LiteralPrinter should either track active ContData* instances as well (or track generic heap pointers) to detect continuation cycles, or retain a recursion/print limit.

}
o << " executed=" << data->executed << ')';
} else {
assert(literal.isData());
auto data = literal.getGCData();
assert(data);
auto [it, inserted] = depths.insert({data.get(), depths.size()});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: We can use try_emplace and avoid a pair move

if (!inserted) {
o << ".." << (depths.size() - 1 - it->second);
} else {
o << "[ref " << literal.type.getHeapType() << ' ';
for (size_t i = 0; i < literal.getNumElements(); i++) {
if (i > 0) {
o << ' ';
}
print(literal.getElement(i));
}
if (!data->desc.isNull()) {
if (literal.getNumElements() > 0) {
o << ", ";
}
o << "desc=";
print(data->desc);
}
o << ']';
depths.erase(data.get());
}
Comment on lines +841 to 861

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Update the cycle detection logic to use the std::vector instead of std::unordered_map. This avoids map insertion/erasure overhead and simplifies the relative depth calculation.

        size_t index = -1;
        for (size_t i = 0; i < depths.size(); ++i) {
          if (depths[i] == data.get()) {
            index = i;
            break;
          }
        }
        if (index != -1) {
          o << ".." << (depths.size() - 1 - index);
        } else {
          depths.push_back(data.get());
          o << "[ref " << literal.type.getHeapType() << ' ';
          for (size_t i = 0; i < literal.getNumElements(); i++) {
            if (i > 0) {
              o << ' ';
            }
            print(literal.getElement(i));
          }
          if (!data->desc.isNull()) {
            if (literal.getNumElements() > 0) {
              o << ", ";
            }
            o << "desc=";
            print(data->desc);
          }
          o << ']';
          depths.pop_back();
        }

o << "desc=" << data->desc;
}
o << ']';
}
restoreNormalColor(o);
return o;
}
restoreNormalColor(o);
return o;
}

std::ostream& operator<<(std::ostream& o, wasm::Literals literals) {
PrintLimiter limiter;
std::ostream& print(const Literals& literals) {
if (literals.size() == 1) {
return print(literals[0]);
}

if (limiter.stop()) {
return o << "[..]";
o << '(';
bool first = true;
for (auto& literal : literals) {
if (first) {
first = false;
} else {
o << ", ";
}
print(literal);
}
return o << ')';
}
};

if (literals.size() == 1) {
return o << literals[0];
}
} // namespace

o << '(';
bool first = true;
for (auto& literal : literals) {
if (limiter.stop()) {
o << "[..]";
break;
}
if (first) {
first = false;
} else {
o << ", ";
}
o << literal;
}
return o << ')';
std::ostream& operator<<(std::ostream& o, Literal literal) {
return LiteralPrinter{o, {}}.print(literal);
}

std::ostream& operator<<(std::ostream& o, wasm::Literals literals) {
return LiteralPrinter{o, {}}.print(literals);
}

Literal Literal::countLeadingZeroes() const {
Expand Down
Loading
Loading