diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index 5f20800ad3f..00fdd12f757 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -16,6 +16,7 @@ #include #include +#include #include "emscripten-optimizer/simple_ast.h" #include "ir/bits.h" @@ -699,208 +700,198 @@ void Literal::printVec128(std::ostream& o, const std::array& 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 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()); + 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(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(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); + } + o << " executed=" << data->executed << ')'; + } else { + assert(literal.isData()); + auto data = literal.getGCData(); + assert(data); + auto [it, inserted] = depths.insert({data.get(), depths.size()}); + 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()); } - 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 { diff --git a/test/lit/ctor-eval/gc-cycle.wast b/test/lit/ctor-eval/gc-cycle.wast index 6d4f0726730..7eea1555c77 100644 --- a/test/lit/ctor-eval/gc-cycle.wast +++ b/test/lit/ctor-eval/gc-cycle.wast @@ -1347,4 +1347,51 @@ ) ) ) +(module + ;; CHECK: (type $A (struct (field i32) (field (mut (ref null $A))))) + (type $A (struct (field i32) (field (mut (ref null $A))))) + + ;; CHECK: (type $1 (func)) + + ;; CHECK: (type $2 (func (param (ref $A)))) + + ;; CHECK: (global $ctor-eval$global_1 (ref (exact $A)) (struct.new $A + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: )) + + ;; CHECK: (tag $tag (type $2) (param (ref $A))) + (tag $tag (param (ref $A))) + (func $test (export "test") + ;; Throwing a cyclic struct should not cause infinite recursion when + ;; formatting the exception message. + (local $a (ref $A)) + (local.set $a + (struct.new_default $A) + ) + (struct.set $A 1 + (local.get $a) + (local.get $a) + ) + (throw $tag + (local.get $a) + ) + ) +) +;; CHECK: (export "test" (func $test_2)) + +;; CHECK: (start $start) + +;; CHECK: (func $start (type $1) +;; CHECK-NEXT: (struct.set $A 1 +;; CHECK-NEXT: (global.get $ctor-eval$global_1) +;; CHECK-NEXT: (global.get $ctor-eval$global_1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $test_2 (type $1) +;; CHECK-NEXT: (throw $tag +;; CHECK-NEXT: (global.get $ctor-eval$global_1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) diff --git a/test/lit/exec/gc-cycle-leak.wast b/test/lit/exec/gc-cycle-leak.wast index f477f24010a..f01bcba4096 100644 --- a/test/lit/exec/gc-cycle-leak.wast +++ b/test/lit/exec/gc-cycle-leak.wast @@ -5,24 +5,61 @@ (module (type $A (struct (field (mut (ref null $A))))) + (import "env" "log_execution" (func $log (param (ref $A)))) + ;; CHECK: [fuzz-exec] export test + ;; CHECK-NEXT: [LoggingExternalInterface log-execution [ref (type $struct.0 (struct (field (mut (ref null $struct.0))))) ..0]] (func $test (export "test") (local $a (ref $A)) ;; This function makes a self-cycle where the local $a's ref field points to ;; itself. This test checks that we do not error, even in sanitizers, when ;; such cycles are created (at the time of creating this test, we represent ;; GC data using std::shared_ptr, which does not handle cycles, and so by - ;; default leak checks will error if not suppressed). + ;; default leak checks will error if not suppressed). Also log $a to test + ;; printing of recursive GC structures. + (local.set $a + (struct.new $A + (ref.null $A) + ) + ) + (struct.set $A 0 + (local.get $a) + (local.get $a) + ) + (call $log + (local.get $a) + ) + ) + + ;; CHECK: [fuzz-exec] export cycle2 + ;; CHECK-NEXT: [LoggingExternalInterface log-execution [ref (type $struct.0 (struct (field (mut (ref null $struct.0))))) [ref (type $struct.0 (struct (field (mut (ref null $struct.0))))) ..1]]] + (func $cycle2 (export "cycle2") + (local $a (ref $A)) + (local $b (ref $A)) + ;; A cycle of length 2 between $a and $b. (local.set $a (struct.new $A (ref.null $A) ) ) + (local.set $b + (struct.new $A + (local.get $a) + ) + ) (struct.set $A 0 (local.get $a) + (local.get $b) + ) + (call $log (local.get $a) ) ) ) ;; CHECK: [fuzz-exec] export test +;; CHECK-NEXT: [LoggingExternalInterface log-execution [ref (type $struct.0 (struct (field (mut (ref null $struct.0))))) ..0]] + +;; CHECK: [fuzz-exec] export cycle2 +;; CHECK-NEXT: [LoggingExternalInterface log-execution [ref (type $struct.0 (struct (field (mut (ref null $struct.0))))) [ref (type $struct.0 (struct (field (mut (ref null $struct.0))))) ..1]]] +;; CHECK-NEXT: [fuzz-exec] comparing cycle2 ;; CHECK-NEXT: [fuzz-exec] comparing test