diff options
-rw-r--r-- | src/literal.h | 24 | ||||
-rw-r--r-- | src/wasm-builder.h | 2 | ||||
-rw-r--r-- | src/wasm-interpreter.h | 250 | ||||
-rw-r--r-- | src/wasm-type.h | 4 | ||||
-rw-r--r-- | src/wasm/literal.cpp | 21 | ||||
-rw-r--r-- | src/wasm/wasm-type.cpp | 9 | ||||
-rw-r--r-- | src/wasm/wasm-validator.cpp | 5 | ||||
-rw-r--r-- | test/lit/exec/rtts.wast | 88 | ||||
-rw-r--r-- | test/passes/Oz_fuzz-exec_all-features.txt | 2 | ||||
-rw-r--r-- | test/passes/Oz_fuzz-exec_all-features.wast | 4 |
10 files changed, 214 insertions, 195 deletions
diff --git a/src/literal.h b/src/literal.h index ce3f80902..ae0d4253f 100644 --- a/src/literal.h +++ b/src/literal.h @@ -62,9 +62,6 @@ class Literal { // as the Literal class itself. // To support the experimental RttFreshSub instruction, we not only store // the type, but also a reference to an allocation. - // The above describes dynamic data, that is with an actual RTT. The static - // case just has a static type in its GCData. - // See struct RttSuper below for more details. std::unique_ptr<RttSupers> rttSupers; // TODO: Literals of type `externref` can only be `null` currently but we // will need to represent extern values eventually, to @@ -264,6 +261,10 @@ public: return lit; } + // Get the canonical RTT value for a given HeapType. For nominal types, the + // canonical RTT reflects the static supertype chain. + static Literal makeCanonicalRtt(HeapType type); + Literal castToF32(); Literal castToF64(); Literal castToI32(); @@ -700,23 +701,18 @@ std::ostream& operator<<(std::ostream& o, wasm::Literals literals); // is. In the case of static (rtt-free) typing, the rtt is not present and // instead we have a static type. struct GCData { - // Either the RTT or the type must be present, but not both. - std::variant<Literal, HeapType> typeInfo; + // The runtime type info for this struct or array. + Literal rtt; + // The element or field values. Literals values; - GCData(HeapType type, Literals values) : typeInfo(type), values(values) {} - GCData(Literal rtt, Literals values) : typeInfo(rtt), values(values) {} - - bool hasRtt() { return std::get_if<Literal>(&typeInfo); } - bool hasHeapType() { return std::get_if<HeapType>(&typeInfo); } - Literal getRtt() { return std::get<Literal>(typeInfo); } - HeapType getHeapType() { return std::get<HeapType>(typeInfo); } + GCData(Literal rtt, Literals values) : rtt(rtt), values(values) {} }; struct RttSuper { // The type of the super. - Type type; + HeapType type; // A shared allocation, used to implement rtt.fresh_sub. This is null for a // normal sub, and for a fresh one we allocate a value here, which can then be // used to differentiate rtts. (The allocation is shared so that when copying @@ -724,7 +720,7 @@ struct RttSuper { // TODO: Remove or optimize this when the spec stabilizes. std::shared_ptr<size_t> freshPtr; - RttSuper(Type type) : type(type) {} + RttSuper(HeapType type) : type(type) {} void makeFresh() { freshPtr = std::make_shared<size_t>(); } diff --git a/src/wasm-builder.h b/src/wasm-builder.h index b192afbac..5914a8afd 100644 --- a/src/wasm-builder.h +++ b/src/wasm-builder.h @@ -837,7 +837,7 @@ public: } RttCanon* makeRttCanon(HeapType heapType) { auto* ret = wasm.allocator.alloc<RttCanon>(); - ret->type = Type(Rtt(0, heapType)); + ret->type = Type(Rtt(heapType.getDepth(), heapType)); ret->finalize(); return ret; } diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index 61a08c14f..c67ac6768 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -26,6 +26,7 @@ #include <cmath> #include <limits.h> #include <sstream> +#include <variant> #include "ir/module-utils.h" #include "support/bits.h" @@ -1404,154 +1405,139 @@ public: // Helper for ref.test, ref.cast, and br_on_cast, which share almost all their // logic except for what they return. struct Cast { - enum Outcome { - // We took a break before doing anything. - Break, - // The input was null. - Null, - // The cast succeeded. - Success, - // The cast failed. - Failure - } outcome; - - Flow breaking; - Literal originalRef; - Literal castRef; + // The control flow that preempts the cast. + struct Breaking : Flow { + Breaking(Flow breaking) : Flow(breaking) {} + }; + // The null input to the cast. + struct Null : Literal { + Null(Literal original) : Literal(original) {} + }; + // The result of the successful cast. + struct Success : Literal { + Success(Literal result) : Literal(result) {} + }; + // The input to a failed cast. + struct Failure : Literal { + Failure(Literal original) : Literal(original) {} + }; + + std::variant<Breaking, Null, Success, Failure> state; + + template<class T> Cast(T state) : state(state) {} + Flow* getBreaking() { return std::get_if<Breaking>(&state); } + Literal* getNull() { return std::get_if<Null>(&state); } + Literal* getSuccess() { return std::get_if<Success>(&state); } + Literal* getFailure() { return std::get_if<Failure>(&state); } + Literal* getNullOrFailure() { + if (auto* original = getNull()) { + return original; + } else { + return getFailure(); + } + } }; template<typename T> Cast doCast(T* curr) { - Cast cast; Flow ref = this->visit(curr->ref); if (ref.breaking()) { - cast.outcome = cast.Break; - cast.breaking = ref; - return cast; + return typename Cast::Breaking{ref}; } + // The RTT value for the type we are trying to cast to. Literal intendedRtt; if (curr->rtt) { - // This is a dynamic check with an rtt. + // This is a dynamic check with an RTT. Flow rtt = this->visit(curr->rtt); if (rtt.breaking()) { - cast.outcome = cast.Break; - cast.breaking = rtt; - return cast; + return typename Cast::Breaking{ref}; } intendedRtt = rtt.getSingleValue(); + } else { + // If there is no explicit RTT, use the canonical RTT for the static type. + intendedRtt = Literal::makeCanonicalRtt(curr->intendedType); } - cast.originalRef = ref.getSingleValue(); - if (cast.originalRef.isNull()) { - cast.outcome = cast.Null; - return cast; + Literal original = ref.getSingleValue(); + if (original.isNull()) { + return typename Cast::Null{original}; } // The input may not be GC data or a function; for example it could be an - // anyref of null (already handled above) or anything else (handled here, - // but this is for future use as atm the binaryen interpreter cannot - // represent external references). - if (!cast.originalRef.isData() && !cast.originalRef.isFunction()) { - cast.outcome = cast.Failure; - return cast; - } - if (cast.originalRef.isFunction()) { - // Function casts are simple in that they have no RTT hierarchies; instead - // each reference has the canonical RTT for the signature. - // We must have a module in order to perform the cast, to get the type. If - // we do not have one, or if the function is not present (which may happen - // if we are optimizing a function before the entire module is built), - // then this is something we cannot precompute. - auto* func = module - ? module->getFunctionOrNull(cast.originalRef.getFunc()) - : nullptr; + // externref or an i31. The cast definitely fails in these cases. + if (!original.isData() && !original.isFunction()) { + return typename Cast::Failure{original}; + } + Literal actualRtt; + if (original.isFunction()) { + // Function references always have the canonical RTTs of the functions + // they reference. We must have a module to look up the function's type to + // get that canonical RTT. + auto* func = + module ? module->getFunctionOrNull(original.getFunc()) : nullptr; if (!func) { - cast.outcome = cast.Break; - cast.breaking = NONCONSTANT_FLOW; - return cast; - } - if (curr->rtt) { - Literal seenRtt = Literal(Type(Rtt(0, func->type))); - if (!seenRtt.isSubRtt(intendedRtt)) { - cast.outcome = cast.Failure; - return cast; - } - cast.castRef = Literal( - func->name, Type(intendedRtt.type.getHeapType(), NonNullable)); - } else { - if (!HeapType::isSubType(func->type, curr->intendedType)) { - cast.outcome = cast.Failure; - return cast; - } - cast.castRef = - Literal(func->name, Type(curr->intendedType, NonNullable)); + return typename Cast::Breaking{NONCONSTANT_FLOW}; } + actualRtt = Literal::makeCanonicalRtt(func->type); } else { - // GC data store an RTT in each instance. - assert(cast.originalRef.isData()); - auto gcData = cast.originalRef.getGCData(); - assert(bool(curr->rtt) == gcData->hasRtt()); - if (curr->rtt) { - auto seenRtt = gcData->getRtt(); - if (!seenRtt.isSubRtt(intendedRtt)) { - cast.outcome = cast.Failure; - return cast; - } - cast.castRef = - Literal(gcData, Type(intendedRtt.type.getHeapType(), NonNullable)); + assert(original.isData()); + actualRtt = original.getGCData()->rtt; + }; + // We have the actual and intended RTTs, so perform the cast. + if (actualRtt.isSubRtt(intendedRtt)) { + Type resultType(intendedRtt.type.getHeapType(), NonNullable); + if (original.isFunction()) { + return typename Cast::Success{Literal{original.getFunc(), resultType}}; } else { - auto seenType = gcData->getHeapType(); - if (!HeapType::isSubType(seenType, curr->intendedType)) { - cast.outcome = cast.Failure; - return cast; - } - cast.castRef = Literal(gcData, Type(seenType, NonNullable)); + return + typename Cast::Success{Literal(original.getGCData(), resultType)}; } + } else { + return typename Cast::Failure{original}; } - cast.outcome = cast.Success; - return cast; } Flow visitRefTest(RefTest* curr) { NOTE_ENTER("RefTest"); auto cast = doCast(curr); - if (cast.outcome == cast.Break) { - return cast.breaking; + if (auto* breaking = cast.getBreaking()) { + return *breaking; + } else { + return Literal(int32_t(bool(cast.getSuccess()))); } - return Literal(int32_t(cast.outcome == cast.Success)); } Flow visitRefCast(RefCast* curr) { NOTE_ENTER("RefCast"); auto cast = doCast(curr); - if (cast.outcome == cast.Break) { - return cast.breaking; - } - if (cast.outcome == cast.Null) { + if (auto* breaking = cast.getBreaking()) { + return *breaking; + } else if (cast.getNull()) { return Literal::makeNull(Type(curr->type.getHeapType(), Nullable)); + } else if (auto* result = cast.getSuccess()) { + return *result; } - if (cast.outcome == cast.Failure) { - trap("cast error"); - } - assert(cast.outcome == cast.Success); - return cast.castRef; + assert(cast.getFailure()); + trap("cast error"); + WASM_UNREACHABLE("unreachable"); } Flow visitBrOn(BrOn* curr) { NOTE_ENTER("BrOn"); - // BrOnCast* uses the casting infrastructure, so handle it first. + // BrOnCast* uses the casting infrastructure, so handle them first. if (curr->op == BrOnCast || curr->op == BrOnCastFail) { auto cast = doCast(curr); - if (cast.outcome == cast.Break) { - return cast.breaking; - } - if (cast.outcome == cast.Null || cast.outcome == cast.Failure) { + if (auto* breaking = cast.getBreaking()) { + return *breaking; + } else if (auto* original = cast.getNullOrFailure()) { if (curr->op == BrOnCast) { - return cast.originalRef; + return *original; } else { - return Flow(curr->name, cast.originalRef); + return Flow(curr->name, *original); } - } - assert(cast.outcome == cast.Success); - if (curr->op == BrOnCast) { - return Flow(curr->name, cast.castRef); } else { - return cast.castRef; + auto* result = cast.getSuccess(); + assert(result); + if (curr->op == BrOnCast) { + return Flow(curr->name, *result); + } else { + return *result; + } } } // The others do a simpler check for the type. @@ -1619,7 +1605,9 @@ public: } return {value}; } - Flow visitRttCanon(RttCanon* curr) { return Literal(curr->type); } + Flow visitRttCanon(RttCanon* curr) { + return Literal::makeCanonicalRtt(curr->type.getHeapType()); + } Flow visitRttSub(RttSub* curr) { Flow parent = this->visit(curr->parent); if (parent.breaking()) { @@ -1627,34 +1615,22 @@ public: } auto parentValue = parent.getSingleValue(); auto newSupers = std::make_unique<RttSupers>(parentValue.getRttSupers()); - newSupers->push_back(parentValue.type); + newSupers->push_back(parentValue.type.getHeapType()); if (curr->fresh) { newSupers->back().makeFresh(); } return Literal(std::move(newSupers), curr->type); } - // Generates GC data for either dynamic (with an RTT) or static (with a type) - // typing. Dynamic typing will provide an rtt expression and an rtt flow with - // the value, while static typing only provides a heap type directly. - template<typename T> - std::shared_ptr<GCData> - makeGCData(Expression* rttExpr, Flow& rttFlow, HeapType type, T& data) { - if (rttExpr) { - return std::make_shared<GCData>(rttFlow.getSingleValue(), data); - } else { - return std::make_shared<GCData>(type, data); - } - } - Flow visitStructNew(StructNew* curr) { NOTE_ENTER("StructNew"); - Flow rtt; + Literal rttVal; if (curr->rtt) { - rtt = this->visit(curr->rtt); + Flow rtt = this->visit(curr->rtt); if (rtt.breaking()) { return rtt; } + rttVal = rtt.getSingleValue(); } if (curr->type == Type::unreachable) { // We cannot proceed to compute the heap type, as there isn't one. Just @@ -1681,8 +1657,10 @@ public: data[i] = value.getSingleValue(); } } - return Flow( - Literal(makeGCData(curr->rtt, rtt, heapType, data), curr->type)); + if (!curr->rtt) { + rttVal = Literal::makeCanonicalRtt(heapType); + } + return Literal(std::make_shared<GCData>(rttVal, data), curr->type); } Flow visitStructGet(StructGet* curr) { NOTE_ENTER("StructGet"); @@ -1725,12 +1703,13 @@ public: Flow visitArrayNew(ArrayNew* curr) { NOTE_ENTER("ArrayNew"); - Flow rtt; + Literal rttVal; if (curr->rtt) { - rtt = this->visit(curr->rtt); + Flow rtt = this->visit(curr->rtt); if (rtt.breaking()) { return rtt; } + rttVal = rtt.getSingleValue(); } auto size = this->visit(curr->size); if (size.breaking()) { @@ -1765,17 +1744,20 @@ public: data[i] = value; } } - return Flow( - Literal(makeGCData(curr->rtt, rtt, heapType, data), curr->type)); + if (!curr->rtt) { + rttVal = Literal::makeCanonicalRtt(heapType); + } + return Literal(std::make_shared<GCData>(rttVal, data), curr->type); } Flow visitArrayInit(ArrayInit* curr) { NOTE_ENTER("ArrayInit"); - Flow rtt; + Literal rttVal; if (curr->rtt) { - rtt = this->visit(curr->rtt); + Flow rtt = this->visit(curr->rtt); if (rtt.breaking()) { return rtt; } + rttVal = rtt.getSingleValue(); } Index num = curr->values.size(); if (num >= ArrayLimit) { @@ -1802,8 +1784,10 @@ public: } data[i] = truncateForPacking(value.getSingleValue(), field); } - return Flow( - Literal(makeGCData(curr->rtt, rtt, heapType, data), curr->type)); + if (!curr->rtt) { + rttVal = Literal::makeCanonicalRtt(heapType); + } + return Literal(std::make_shared<GCData>(rttVal, data), curr->type); } Flow visitArrayGet(ArrayGet* curr) { NOTE_ENTER("ArrayGet"); diff --git a/src/wasm-type.h b/src/wasm-type.h index 4c401be6b..1f6c5a7de 100644 --- a/src/wasm-type.h +++ b/src/wasm-type.h @@ -390,6 +390,10 @@ public: // `TypeSystem::Equirecursive` mode. std::optional<HeapType> getSuperType() const; + // Return the depth of this heap type in the nominal type hierarchy, i.e. the + // number of supertypes in its supertype chain. + size_t getDepth() const; + // Whether this is a nominal type in the sense of being a GC Milestone 4 // nominal type. Although all non-basic HeapTypes are nominal in // `TypeSystem::Nominal` mode, this will still return false unless the type is diff --git a/src/wasm/literal.cpp b/src/wasm/literal.cpp index 131a6d443..4753e5ca1 100644 --- a/src/wasm/literal.cpp +++ b/src/wasm/literal.cpp @@ -38,8 +38,7 @@ Literal::Literal(Type type) : type(type) { if (isData()) { new (&gcData) std::shared_ptr<GCData>(); } else if (type.isRtt()) { - // Allocate a new RttSupers (with no data). - new (&rttSupers) auto(std::make_unique<RttSupers>()); + new (this) Literal(Literal::makeCanonicalRtt(type.getHeapType())); } else { memset(&v128, 0, 16); } @@ -148,6 +147,18 @@ Literal& Literal::operator=(const Literal& other) { return *this; } +Literal Literal::makeCanonicalRtt(HeapType type) { + auto supers = std::make_unique<RttSupers>(); + std::optional<HeapType> supertype; + for (auto curr = type; (supertype = curr.getSuperType()); curr = *supertype) { + supers->emplace_back(*supertype); + } + // We want the highest types to be first. + std::reverse(supers->begin(), supers->end()); + size_t depth = supers->size(); + return Literal(std::move(supers), Type(Rtt(depth, type))); +} + template<typename LaneT, int Lanes> static void extractBytes(uint8_t (&dest)[16], const LaneArray<Lanes>& lanes) { std::array<uint8_t, 16> bytes; @@ -495,9 +506,7 @@ std::ostream& operator<<(std::ostream& o, Literal literal) { if (literal.isData()) { auto data = literal.getGCData(); if (data) { - o << "[ref "; - std::visit([&](auto& info) { o << info; }, data->typeInfo); - o << ' ' << data->values << ']'; + o << "[ref " << data->rtt << ' ' << data->values << ']'; } else { o << "[ref null " << literal.type << ']'; } @@ -2560,7 +2569,7 @@ bool Literal::isSubRtt(const Literal& other) const { // we have the same amount of supers, and must be completely identical to // other. if (otherSupers.size() < supers.size()) { - return other.type == supers[otherSupers.size()].type; + return other.type.getHeapType() == supers[otherSupers.size()].type; } else { return other.type == type; } diff --git a/src/wasm/wasm-type.cpp b/src/wasm/wasm-type.cpp index 94c6e96c6..002b401b5 100644 --- a/src/wasm/wasm-type.cpp +++ b/src/wasm/wasm-type.cpp @@ -1217,6 +1217,15 @@ std::optional<HeapType> HeapType::getSuperType() const { return {}; } +size_t HeapType::getDepth() const { + size_t depth = 0; + std::optional<HeapType> super; + for (auto curr = *this; (super = curr.getSuperType()); curr = *super) { + ++depth; + } + return depth; +} + bool HeapType::isNominal() const { if (isBasic()) { return false; diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index f8669961c..bb0e99cc2 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -2361,7 +2361,10 @@ void FunctionValidator::visitRttCanon(RttCanon* curr) { getModule()->features.hasGC(), curr, "rtt.canon requires gc to be enabled"); shouldBeTrue(curr->type.isRtt(), curr, "rtt.canon must have RTT type"); auto rtt = curr->type.getRtt(); - shouldBeEqual(rtt.depth, Index(0), curr, "rtt.canon has a depth of 0"); + shouldBeEqual(rtt.depth, + Index(curr->type.getHeapType().getDepth()), + curr, + "rtt.canon must have the depth of its heap type"); } void FunctionValidator::visitRttSub(RttSub* curr) { diff --git a/test/lit/exec/rtts.wast b/test/lit/exec/rtts.wast index 98304f8f2..7c090f180 100644 --- a/test/lit/exec/rtts.wast +++ b/test/lit/exec/rtts.wast @@ -3,8 +3,6 @@ ;; Check that allocation and casting instructions with and without RTTs can be ;; mixed correctly. -;; TODO: Fix the implementation to not fail an assertion on the functions commented out below. - ;; RUN: wasm-opt %s -all --fuzz-exec-before -q --structural -o /dev/null 2>&1 \ ;; RUN: | filecheck %s --check-prefix=EQREC @@ -52,11 +50,10 @@ ) ) - ;; TODO: This should succeed in --nominal mode ;; EQREC: [fuzz-exec] calling canon-sub ;; EQREC-NEXT: [LoggingExternalInterface logging 0] ;; NOMNL: [fuzz-exec] calling canon-sub - ;; NOMNL-NEXT: [LoggingExternalInterface logging 0] + ;; NOMNL-NEXT: [LoggingExternalInterface logging 1] (func "canon-sub" (call $log (ref.test @@ -66,19 +63,22 @@ ) ) - ;; (func "canon-static" - ;; (call $log - ;; (ref.test_static $sub-struct - ;; (call $make-sub-struct-canon) - ;; ) - ;; ) - ;; ) + ;; EQREC: [fuzz-exec] calling canon-static + ;; EQREC-NEXT: [LoggingExternalInterface logging 1] + ;; NOMNL: [fuzz-exec] calling canon-static + ;; NOMNL-NEXT: [LoggingExternalInterface logging 1] + (func "canon-static" + (call $log + (ref.test_static $sub-struct + (call $make-sub-struct-canon) + ) + ) + ) - ;; TODO: This should succeed in --nominal mode ;; EQREC: [fuzz-exec] calling sub-canon ;; EQREC-NEXT: [LoggingExternalInterface logging 0] ;; NOMNL: [fuzz-exec] calling sub-canon - ;; NOMNL-NEXT: [LoggingExternalInterface logging 0] + ;; NOMNL-NEXT: [LoggingExternalInterface logging 1] (func "sub-canon" (call $log (ref.test @@ -101,31 +101,43 @@ ) ) - ;; (func "sub-static" - ;; (call $log - ;; (ref.test_static $sub-struct - ;; (call $make-sub-struct-sub) - ;; ) - ;; ) - ;; ) - - ;; (func "static-canon" - ;; (call $log - ;; (ref.test - ;; (call $make-sub-struct-static) - ;; (rtt.canon $sub-struct) - ;; ) - ;; ) - ;; ) - - ;; (func "static-sub" - ;; (call $log - ;; (ref.test - ;; (call $make-sub-struct-static) - ;; (global.get $sub-rtt) - ;; ) - ;; ) - ;; ) + ;; EQREC: [fuzz-exec] calling sub-static + ;; EQREC-NEXT: [LoggingExternalInterface logging 0] + ;; NOMNL: [fuzz-exec] calling sub-static + ;; NOMNL-NEXT: [LoggingExternalInterface logging 1] + (func "sub-static" + (call $log + (ref.test_static $sub-struct + (call $make-sub-struct-sub) + ) + ) + ) + + ;; EQREC: [fuzz-exec] calling static-canon + ;; EQREC-NEXT: [LoggingExternalInterface logging 1] + ;; NOMNL: [fuzz-exec] calling static-canon + ;; NOMNL-NEXT: [LoggingExternalInterface logging 1] + (func "static-canon" + (call $log + (ref.test + (call $make-sub-struct-static) + (rtt.canon $sub-struct) + ) + ) + ) + + ;; EQREC: [fuzz-exec] calling static-sub + ;; EQREC-NEXT: [LoggingExternalInterface logging 0] + ;; NOMNL: [fuzz-exec] calling static-sub + ;; NOMNL-NEXT: [LoggingExternalInterface logging 1] + (func "static-sub" + (call $log + (ref.test + (call $make-sub-struct-static) + (global.get $sub-rtt) + ) + ) + ) ;; EQREC: [fuzz-exec] calling static-static ;; EQREC-NEXT: [LoggingExternalInterface logging 1] diff --git a/test/passes/Oz_fuzz-exec_all-features.txt b/test/passes/Oz_fuzz-exec_all-features.txt index 359cb83ed..a7c327d1e 100644 --- a/test/passes/Oz_fuzz-exec_all-features.txt +++ b/test/passes/Oz_fuzz-exec_all-features.txt @@ -79,7 +79,7 @@ [LoggingExternalInterface logging 0] [LoggingExternalInterface logging 1] [LoggingExternalInterface logging 0] -[LoggingExternalInterface logging 1] +[LoggingExternalInterface logging 0] [fuzz-exec] calling static-br_on_cast [LoggingExternalInterface logging 3] [fuzz-exec] calling static-br_on_cast_fail diff --git a/test/passes/Oz_fuzz-exec_all-features.wast b/test/passes/Oz_fuzz-exec_all-features.wast index ea29f259a..7ab9116dd 100644 --- a/test/passes/Oz_fuzz-exec_all-features.wast +++ b/test/passes/Oz_fuzz-exec_all-features.wast @@ -574,7 +574,9 @@ (struct.new_default $struct) ) ) - ;; Casting to a supertype works. + ;; Casting to a supertype does not work because the canonical RTT for the + ;; subtype is not a sub-rtt of the canonical RTT of the supertype in + ;; structural mode. (call $log (ref.test_static $struct (struct.new_default $extendedstruct) |