diff --git a/src/ir/struct-utils.h b/src/ir/struct-utils.h index 979c7c628f8..471a580a9d4 100644 --- a/src/ir/struct-utils.h +++ b/src/ir/struct-utils.h @@ -184,8 +184,7 @@ struct FunctionStructValuesMap // Descriptors are treated as fields in that we call the above functions on // them. We pass DescriptorIndex for their index as a fake value. template -struct StructScanner - : public WalkerPass>> { +struct StructScanner : public WalkerPass> { bool isFunctionParallel() override { return true; } bool modifiesBinaryenIR() override { return false; } diff --git a/src/passes/GlobalTypeOptimization.cpp b/src/passes/GlobalTypeOptimization.cpp index c038a7a0a06..1961c18ae4b 100644 --- a/src/passes/GlobalTypeOptimization.cpp +++ b/src/passes/GlobalTypeOptimization.cpp @@ -23,6 +23,7 @@ // #include "ir/eh-utils.h" +#include "ir/intrinsics.h" #include "ir/localize.h" #include "ir/names.h" #include "ir/ordering.h" @@ -111,6 +112,22 @@ struct FieldInfoScanner info.noteRead(); info.noteWrite(); } + + // Converting a reference to externref makes the prototype field on its + // descriptor available to be read by JS, if such a field exists. + void visitRefAs(RefAs* curr) { + if (curr->op != ExternConvertAny) { + return; + } + if (!curr->value->type.isRef()) { + return; + } + auto exact = curr->value->type.getExactness(); + if (auto desc = curr->value->type.getHeapType().getDescriptorType(); + desc && desc->hasPossibleJSPrototypeField()) { + functionSetGetInfos[getFunction()][{*desc, exact}][0].noteRead(); + } + } }; struct GlobalTypeOptimization : public Pass { @@ -151,6 +168,10 @@ struct GlobalTypeOptimization : public Pass { // Combine the data from the functions. functionSetGetInfos.combineInto(combinedSetGetInfos); + // Analyze functions called by JS to find fields holding configured + // prototypes that cannot be removed. + analyzeJSCalledFunctions(*module); + // Propagate information to super and subtypes on set/get infos: // // * For removing unread fields, we can only remove a field if it is never @@ -201,7 +222,7 @@ struct GlobalTypeOptimization : public Pass { auto& fields = type.getStruct().fields; // Use the exact entry because information from the inexact entry in // dataFromSupersMap will have been propagated down into it but not vice - // versa. (This doesn't matter or dataFromSubsAndSupers because the exact + // versa. (This doesn't matter for dataFromSubsAndSupers because the exact // and inexact entries will have the same data.) auto ht = std::make_pair(type, Exact); auto& dataFromSubsAndSupers = dataFromSubsAndSupersMap[ht]; @@ -395,6 +416,25 @@ struct GlobalTypeOptimization : public Pass { } } + void analyzeJSCalledFunctions(Module& wasm) { + if (!wasm.features.hasCustomDescriptors()) { + return; + } + for (auto func : Intrinsics(wasm).getConfigureAllFunctions()) { + for (auto type : wasm.getFunction(func)->getResults()) { + if (!type.isRef()) { + continue; + } + if (auto desc = type.getHeapType().getDescriptorType(); + desc && desc->hasPossibleJSPrototypeField()) { + // This field holds a JS-visible prototype. Do not remove it. + auto exact = type.getExactness(); + combinedSetGetInfos[std::make_pair(*desc, exact)][0].noteRead(); + } + } + } + } + void updateTypes(Module& wasm) { class TypeRewriter : public GlobalTypeRewriter { GlobalTypeOptimization& parent; diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index 844ce127edd..fea8cd7b43f 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -554,6 +554,7 @@ struct Unsubtyping : Pass, Noter { // Initialize the subtype relation based on what is immediately required to // keep the code and public types valid. analyzePublicTypes(*wasm); + analyzeJSCalledFunctions(*wasm); analyzeModule(*wasm); // Find further subtypings and iterate to a fixed point. @@ -605,6 +606,35 @@ struct Unsubtyping : Pass, Noter { } } + void analyzeJSCalledFunctions(Module& wasm) { + if (!wasm.features.hasCustomDescriptors()) { + return; + } + Type anyref(HeapType::any, Nullable); + for (auto func : Intrinsics(wasm).getConfigureAllFunctions()) { + for (auto type : wasm.getFunction(func)->getParams()) { + if (Type::isSubType(type, anyref)) { + // This type is cast from anyref on the boundary. + noteCast(HeapType::any, type); + } + } + for (auto type : wasm.getFunction(func)->getResults()) { + if (Type::isSubType(type, anyref)) { + auto heapType = type.getHeapType(); + // This type is implicitly converted to externref, which means it can + // flow into locations typed any. + noteSubtype(heapType, HeapType::any); + if (auto desc = heapType.getDescriptorType(); + desc && desc->hasPossibleJSPrototypeField()) { + // This descriptor will expose a prototype to JS, so we must keep + // it. + noteDescriptor(heapType, *desc); + } + } + } + } + } + void analyzeModule(Module& wasm) { struct Info { // (source, target) pairs for casts. @@ -746,6 +776,14 @@ struct Unsubtyping : Pass, Noter { for (auto& [sub, super] : collectedInfo.subtypings) { noteSubtype(sub, super); } + // Combine casts we have already noted into the newly gathered casts. + for (auto& [src, dsts] : casts) { + for (auto dst : dsts) { + collectedInfo.casts.insert({src, dst}); + } + dsts.clear(); + } + // Record the deduplicated cast info. for (auto [src, dst] : collectedInfo.casts) { casts[src].push_back(dst); } diff --git a/src/wasm-type.h b/src/wasm-type.h index 0636181d299..5f40c53c602 100644 --- a/src/wasm-type.h +++ b/src/wasm-type.h @@ -198,6 +198,10 @@ class HeapType { std::optional getDescribedType() const; DescriptorChain getDescriptorChain() const; + // Whether this is a struct type whose first field is immutable and a subtype + // of externref. + bool hasPossibleJSPrototypeField() const; + // Return the depth of this heap type in the type hierarchy, i.e. the number // of supertypes in its supertype chain. size_t getDepth() const; diff --git a/src/wasm/wasm-type.cpp b/src/wasm/wasm-type.cpp index 16bc1d0c380..84664717526 100644 --- a/src/wasm/wasm-type.cpp +++ b/src/wasm/wasm-type.cpp @@ -1045,6 +1045,23 @@ size_t HeapType::getDepth() const { return depth; } +bool HeapType::hasPossibleJSPrototypeField() const { + if (!isStruct()) { + return false; + } + const auto& fields = getStruct().fields; + if (fields.empty()) { + return false; + } + if (fields[0].mutable_ == Mutable) { + return false; + } + if (!fields[0].type.isRef()) { + return false; + } + return fields[0].type.getHeapType().isMaybeShared(HeapType::ext); +} + HeapType::BasicHeapType HeapType::getUnsharedBottom() const { if (isBasic()) { switch (getBasic(Unshared)) { diff --git a/test/lit/passes/gto-jsinterop.wast b/test/lit/passes/gto-jsinterop.wast new file mode 100644 index 00000000000..23bc2a5fd02 --- /dev/null +++ b/test/lit/passes/gto-jsinterop.wast @@ -0,0 +1,301 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: foreach %s %t wasm-opt -all --closed-world --gto --preserve-type-order -S -o - | filecheck %s + +(module + (rec + ;; We can optimize out the externref field on the field because it is never + ;; used. + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (descriptor $desc) (struct)) + (type $struct (descriptor $desc) (struct (field externref))) + ;; The externef field on the descriptor is visible from JS because it holds + ;; the prototype, so we cannot remove it. + ;; CHECK: (type $desc (describes $struct) (struct (field externref))) + (type $desc (describes $struct) (struct (field externref))) + ) + ;; CHECK: (type $2 (func)) + + ;; CHECK: (type $3 (func (result (ref $struct)))) + + ;; CHECK: (type $prototypes (array (mut externref))) + (type $prototypes (array (mut externref))) + ;; CHECK: (type $funcs (array (mut funcref))) + (type $funcs (array (mut funcref))) + ;; CHECK: (type $data (array (mut i8))) + (type $data (array (mut i8))) + ;; CHECK: (type $configureAll (func (param (ref null $prototypes) (ref null $funcs) (ref null $data) externref))) + (type $configureAll (func (param (ref null $prototypes)) (param (ref null $funcs)) (param (ref null $data)) (param externref))) + + ;; CHECK: (import "wasm:js-prototypes" "configureAll" (func $configureAll (type $configureAll) (param (ref null $prototypes) (ref null $funcs) (ref null $data) externref))) + (import "wasm:js-prototypes" "configureAll" (func $configureAll (type $configureAll))) + + ;; CHECK: (data $data "12345678") + (data $data "12345678") + + ;; CHECK: (elem $prototypes externref (item (ref.null noextern))) + (elem $prototypes externref (ref.null extern)) + + ;; CHECK: (elem $funcs func $return-struct) + (elem $funcs funcref (ref.func $return-struct)) + + ;; CHECK: (start $start) + (start $start) + + ;; CHECK: (func $start (type $2) + ;; CHECK-NEXT: (call $configureAll + ;; CHECK-NEXT: (array.new_elem $prototypes $prototypes + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (array.new_elem $funcs $funcs + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (array.new_data $data $data + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null noextern) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $start + (call $configureAll + (array.new_elem $prototypes $prototypes (i32.const 0) (i32.const 1)) + (array.new_elem $funcs $funcs (i32.const 0) (i32.const 1)) + (array.new_data $data $data (i32.const 0) (i32.const 8)) + (ref.null extern) + ) + ) + + ;; CHECK: (func $return-struct (type $3) (result (ref $struct)) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $return-struct (result (ref $struct)) + ;; It doesn't matter that the function does not actually return a struct; we + ;; just look at the signature. + (unreachable) + ) +) + +(module + (rec + ;; We can optimize out the externref field on the field because it is never + ;; used. + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (descriptor $desc) (struct)) + (type $struct (descriptor $desc) (struct (field externref))) + ;; The externef field on the descriptor is visible from JS because it holds + ;; the prototype, so we cannot remove it. + ;; CHECK: (type $desc (describes $struct) (struct (field externref))) + (type $desc (describes $struct) (struct (field externref))) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $externalize (type $2) + ;; CHECK-NEXT: (local $struct (ref null $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (extern.convert_any + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $externalize + (local $struct (ref null $struct)) + ;; Externalizing a reference may in general make it available to JS and + ;; and force us to keep fields holding exposed prototypes. + (drop + (extern.convert_any + (local.get $struct) + ) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (descriptor $desc) (struct)) + (type $struct (descriptor $desc) (struct)) + ;; Non-nullable extern refs can also hold prototypes. + ;; CHECK: (type $desc (describes $struct) (struct (field (ref extern)))) + (type $desc (describes $struct) (struct (field (ref extern)))) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $externalize (type $2) + ;; CHECK-NEXT: (local $struct (ref null $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (extern.convert_any + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $externalize + (local $struct (ref null $struct)) + (drop + (extern.convert_any + (local.get $struct) + ) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (descriptor $desc) (struct)) + (type $struct (descriptor $desc) (struct)) + ;; Maybe we will support shared prototypes someday. + ;; CHECK: (type $desc (describes $struct) (struct (field (ref null (shared extern))))) + (type $desc (describes $struct) (struct (field (ref null (shared extern))))) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $externalize (type $2) + ;; CHECK-NEXT: (local $struct (ref null $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (extern.convert_any + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $externalize + (local $struct (ref null $struct)) + (drop + (extern.convert_any + (local.get $struct) + ) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (descriptor $desc) (struct)) + (type $struct (descriptor $desc) (struct)) + ;; Only externref fields can hold prototypes. We can optimize out e.g. + ;; anyref fields. + ;; CHECK: (type $desc (describes $struct) (struct)) + (type $desc (describes $struct) (struct (field anyref))) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $externalize (type $2) + ;; CHECK-NEXT: (local $struct (ref null $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (extern.convert_any + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $externalize + (local $struct (ref null $struct)) + (drop + (extern.convert_any + (local.get $struct) + ) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (descriptor $desc) (struct)) + (type $struct (descriptor $desc) (struct)) + ;; Bottom externrefs can only hold null prototypes, but that's the default + ;; prototype value anyway, so we can still optimize them out. + ;; CHECK: (type $desc (describes $struct) (struct)) + (type $desc (describes $struct) (struct (field nullexternref))) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $externalize (type $2) + ;; CHECK-NEXT: (local $struct (ref null $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (extern.convert_any + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $externalize + (local $struct (ref null $struct)) + (drop + (extern.convert_any + (local.get $struct) + ) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (descriptor $desc) (struct)) + (type $struct (descriptor $desc) (struct)) + ;; Strings cannot be prototypes. If the prototype field holds a string, it + ;; will appear as a null prototype. That is the default prototype value, so + ;; we can optimize out prototype fields if we know they will be strings. + ;; CHECK: (type $desc (describes $struct) (struct)) + (type $desc (describes $struct) (struct (field stringref))) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $externalize (type $2) + ;; CHECK-NEXT: (local $struct (ref null $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (extern.convert_any + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $externalize + (local $struct (ref null $struct)) + (drop + (extern.convert_any + (local.get $struct) + ) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (descriptor $desc) (struct)) + (type $struct (descriptor $desc) (struct)) + ;; Mutable fields cannot hold exposed prototypes, so they can be optimized + ;; normally. + ;; CHECK: (type $desc (describes $struct) (struct)) + (type $desc (describes $struct) (struct (field (mut externref)))) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $externalize (type $2) + ;; CHECK-NEXT: (local $struct (ref null $struct)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (extern.convert_any + ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $externalize + (local $struct (ref null $struct)) + ;; Externalizing a reference may in general make it available to JS and + ;; and force us to keep fields holding exposed prototypes. + (drop + (extern.convert_any + (local.get $struct) + ) + ) + ) +) diff --git a/test/lit/passes/unsubtyping-jsinterop.wast b/test/lit/passes/unsubtyping-jsinterop.wast new file mode 100644 index 00000000000..d1a151792a4 --- /dev/null +++ b/test/lit/passes/unsubtyping-jsinterop.wast @@ -0,0 +1,167 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: wasm-opt %s -all --closed-world --preserve-type-order \ +;; RUN: --unsubtyping --remove-unused-types -all -S -o - | filecheck %s + +(module + (rec + ;; We must maintain this descriptor relationship so that when $struct flows + ;; out to JS it still has the prototype configured on its descriptor. + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct-proto (descriptor $desc-proto) (struct)) + (type $struct-proto (descriptor $desc-proto) (struct)) + ;; CHECK: (type $desc-proto (describes $struct-proto) (struct (field externref))) + (type $desc-proto (describes $struct-proto) (struct (field externref))) + + ;; But this descriptor cannot have a configured prototype, so we do not need + ;; to maintain the relationship. + ;; CHECK: (type $struct-no-proto (struct)) + (type $struct-no-proto (descriptor $desc-no-proto) (struct)) + (type $desc-no-proto (describes $struct-no-proto) (struct (field (mut externref)))) + + ;; This descriptor has a valid prototype field, but is not sent to JS. It + ;; can be optimized out. + ;; CHECK: (type $struct-not-sent (struct)) + (type $struct-not-sent (descriptor $desc-not-sent) (struct)) + (type $desc-not-sent (describes $struct-not-sent) (struct (field externref))) + + ;; Receiving a type from JS as means it is implicitly cast from any. + ;; CHECK: (type $super-param (sub (struct))) + (type $super-param (sub (struct))) + ;; CHECK: (type $sub-param (sub $super-param (struct))) + (type $sub-param (sub $super-param (struct))) + + ;; Returning a type to JS means it can come back in as an anyref. + ;; CHECK: (type $super-result (sub (struct))) + (type $super-result (sub (struct))) + ;; CHECK: (type $sub-result (sub $super-result (struct))) + (type $sub-result (sub $super-result (struct))) + ) + + ;; CHECK: (type $8 (func)) + + ;; CHECK: (type $9 (func (result (ref $struct-proto)))) + + ;; CHECK: (type $10 (func (result (ref $struct-no-proto)))) + + ;; CHECK: (type $11 (func (param (ref $struct-not-sent)))) + + ;; CHECK: (type $12 (func (param (ref null $super-param)))) + + ;; CHECK: (type $13 (func (result (ref null $sub-result)))) + + ;; CHECK: (type $prototypes (array (mut externref))) + (type $prototypes (array (mut externref))) + ;; CHECK: (type $funcs (array (mut funcref))) + (type $funcs (array (mut funcref))) + ;; CHECK: (type $data (array (mut i8))) + (type $data (array (mut i8))) + ;; CHECK: (type $configureAll (func (param (ref null $prototypes) (ref null $funcs) (ref null $data) externref))) + (type $configureAll (func (param (ref null $prototypes)) (param (ref null $funcs)) (param (ref null $data)) (param externref))) + + ;; CHECK: (import "wasm:js-prototypes" "configureAll" (func $configureAll (type $configureAll) (param (ref null $prototypes) (ref null $funcs) (ref null $data) externref))) + (import "wasm:js-prototypes" "configureAll" (func $configureAll (type $configureAll))) + + ;; CHECK: (data $data "12345678") + (data $data "12345678") + + ;; CHECK: (elem $prototypes externref (item (ref.null noextern))) + (elem $prototypes externref (ref.null extern)) + + ;; CHECK: (elem $funcs func $return-struct-proto $return-struct-no-proto $receive-proto $receive $return) + (elem $funcs funcref (ref.func $return-struct-proto) (ref.func $return-struct-no-proto) (ref.func $receive-proto) (ref.func $receive) (ref.func $return)) + + ;; CHECK: (start $start) + (start $start) + + ;; CHECK: (func $start (type $8) + ;; CHECK-NEXT: (call $configureAll + ;; CHECK-NEXT: (array.new_elem $prototypes $prototypes + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (array.new_elem $funcs $funcs + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (array.new_data $data $data + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (i32.const 8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null noextern) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $start + (call $configureAll + (array.new_elem $prototypes $prototypes (i32.const 0) (i32.const 1)) + (array.new_elem $funcs $funcs (i32.const 0) (i32.const 5)) + (array.new_data $data $data (i32.const 0) (i32.const 8)) + (ref.null extern) + ) + ) + + ;; CHECK: (func $return-struct-proto (type $9) (result (ref $struct-proto)) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $return-struct-proto (result (ref $struct-proto)) + ;; Looking at the function signature, $struct-proto flows out to JS. Since + ;; its descriptor can have a configured prototype, it must be kept. It + ;; doesn't matter that the function actually traps. + (unreachable) + ) + + ;; CHECK: (func $return-struct-no-proto (type $10) (result (ref $struct-no-proto)) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $return-struct-no-proto (result (ref $struct-no-proto)) + ;; No need to keep a descriptor that cannot hold a configured prototype. + (unreachable) + ) + + ;; CHECK: (func $receive-proto (type $11) (param $0 (ref $struct-not-sent)) + ;; CHECK-NEXT: ) + (func $receive-proto (param (ref $struct-not-sent)) + ;; Since we are not passing $struct-not-sent to JS, we do not need to + ;; preserve its descriptor, even though that descriptor could hold a + ;; prototype. + ) + + ;; CHECK: (func $receive (type $12) (param $0 (ref null $super-param)) + ;; CHECK-NEXT: (local $any anyref) + ;; CHECK-NEXT: (local $sub-param (ref null $sub-param)) + ;; CHECK-NEXT: (local.set $any + ;; CHECK-NEXT: (local.get $sub-param) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $receive (param (ref null $super-param)) + ;; There is an implicit cast from any to $super-param on the boundary. This + ;; combined with $sub-param flowing into any here means that the subtyping + ;; must be maintained. + (local $any anyref) + (local $sub-param (ref null $sub-param)) + (local.set $any + (local.get $sub-param) + ) + ) + + ;; CHECK: (func $return (type $13) (result (ref null $sub-result)) + ;; CHECK-NEXT: (local $any anyref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.test (ref null $super-result) + ;; CHECK-NEXT: (local.get $any) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $return (result (ref null $sub-result)) + ;; $sub-result implicitly flows into any on the boundary. This combined with + ;; the cast from any to $super-result here means that the subtyping must be + ;; maintained. + (local $any anyref) + (drop + (ref.test (ref null $super-result) + (local.get $any) + ) + ) + (unreachable) + ) +)