From 724ffd9d60cc2787a2c8613b9e3d5c987a4140db Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 9 Mar 2026 22:05:48 -0700 Subject: [PATCH 1/2] Handle flow via imports and exports in Unsubtyping We previously upated Unsubtyping so that any subtype of the result of a JS-called function would keep its descriptor if that descriptor could configure a JS prototype. But configured prototypes can also become visible indirectly because they flow out of the module via imported or exported functions, globals, or tables. Handle these cases of values flowing in from or out to JS. --- src/passes/Unsubtyping.cpp | 123 +++++-- test/lit/passes/unsubtyping-jsinterop.wast | 352 +++++++++++++++++++++ 2 files changed, 455 insertions(+), 20 deletions(-) diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index d648635b4af..fd688e7dc9c 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -451,6 +451,8 @@ struct TypeTree { // the final update of data structures is different. This CRTP utility // deduplicates the shared logic. template struct Noter { + DBG(Module* wasm = nullptr); + Self& self() { return *static_cast(this); } void noteSubtype(HeapType sub, HeapType super) { @@ -559,8 +561,6 @@ struct Unsubtyping : Pass, Noter { // Map from cast source types to their destinations. Map> casts; - DBG(Module* wasm = nullptr); - void run(Module* wasm) override { DBG(this->wasm = wasm); if (!wasm->features.hasGC()) { @@ -574,7 +574,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); + analyzeJSInterface(*wasm); analyzeModule(*wasm); // Find further subtypings and iterate to a fixed point. @@ -635,25 +635,105 @@ struct Unsubtyping : Pass, Noter { } } - void analyzeJSCalledFunctions(Module& wasm) { + void analyzeJSInterface(Module& wasm) { if (!wasm.features.hasCustomDescriptors()) { return; } Type anyref(HeapType::any, Nullable); - for (auto func : Intrinsics(wasm).getJSCalledFunctions()) { - // Parameter types flow into Wasm and are implicitly cast from any. - for (auto type : wasm.getFunction(func)->getParams()) { - if (Type::isSubType(type, anyref)) { - noteCast(HeapType::any, type); + + // Values flowing in from JS are implicitly cast from any. + auto flowIn = [&](Type type) { + if (Type::isSubType(type, anyref)) { + noteCast(HeapType::any, type); + } + }; + + // Values flowing out to JS are converted to extern and might come back in + // as anyrefs. Their descriptors may need to be kept to configure JS + // prototypes. + auto flowOut = [&](Type type) { + if (Type::isSubType(type, anyref)) { + auto heapType = type.getHeapType(); + noteSubtype(heapType, HeapType::any); + noteExposedToJS(heapType); + } + }; + + // @binaryen.js.called functions are called from JS. Their parameters flow + // in from JS and their results flow back out. + for (auto f : Intrinsics(wasm).getJSCalledFunctions()) { + auto* func = wasm.getFunction(f); + for (auto type : func->getParams()) { + flowIn(type); + } + for (auto type : func->getResults()) { + flowOut(type); + } + } + + for (auto& ex : wasm.exports) { + switch (ex->kind) { + case ExternalKindImpl::Function: { + // Exported functions are also called from JS. Their parameters flow + // in from JS and their result flow back out. + auto* func = wasm.getFunction(*ex->getInternalName()); + for (auto type : func->getParams()) { + flowIn(type); + } + for (auto type : func->getResults()) { + flowOut(type); + } + break; + } + case ExternalKindImpl::Table: { + // Exported tables let values flow in and out. + auto* table = wasm.getTable(*ex->getInternalName()); + flowOut(table->type); + flowIn(table->type); + break; + } + case ExternalKindImpl::Global: { + // Exported globals let values flow out. Iff they are mutable, they + // also let values flow back in. + auto* global = wasm.getGlobal(*ex->getInternalName()); + flowOut(global->type); + if (global->mutable_) { + flowIn(global->type); + } + break; + } + case ExternalKindImpl::Memory: + case ExternalKindImpl::Tag: + case ExternalKindImpl::Invalid: + break; + } + } + for (auto& func : wasm.functions) { + // Imported functions are the opposite of exported functions. Their + // parameters flow out and their results flow in. + if (func->imported()) { + for (auto type : func->getParams()) { + flowOut(type); } + for (auto type : func->getResults()) { + flowIn(type); + } + } + } + for (auto& table : wasm.tables) { + // Imported tables, like exported tables, let values flow in and out. + if (table->imported()) { + flowOut(table->type); + flowIn(table->type); } - for (auto type : wasm.getFunction(func)->getResults()) { - // Result types flow into JS and are implicitly converted from any to - // extern. They may also expose configured prototypes that we must keep. - if (Type::isSubType(type, anyref)) { - auto heapType = type.getHeapType(); - noteSubtype(heapType, HeapType::any); - noteExposedToJS(heapType); + } + for (auto& global : wasm.globals) { + // Imported mutable globals let values flow in and out. Imported immutable + // globals imply that values will flow in. + if (global->imported()) { + flowIn(global->type); + if (global->mutable_) { + flowOut(global->type); } } } @@ -683,8 +763,10 @@ struct Unsubtyping : Pass, Noter { Info& info; bool trapsNeverHappen; - Collector(Info& info, bool trapsNeverHappen) - : info(info), trapsNeverHappen(trapsNeverHappen) {} + Collector(Info& info, bool trapsNeverHappen, Module* wasm) + : info(info), trapsNeverHappen(trapsNeverHappen) { + DBG(this->wasm = wasm); + } void doNoteSubtype(HeapType sub, HeapType super) { info.subtypings.insert({sub, super}); @@ -783,7 +865,8 @@ struct Unsubtyping : Pass, Noter { ModuleUtils::ParallelFunctionAnalysis analysis( wasm, [&](Function* func, Info& info) { if (!func->imported()) { - Collector(info, trapsNeverHappen).walkFunctionInModule(func, &wasm); + Collector(info, trapsNeverHappen, &wasm) + .walkFunctionInModule(func, &wasm); } }); @@ -799,7 +882,7 @@ struct Unsubtyping : Pass, Noter { } // Collect constraints from module-level code as well. - Collector collector(collectedInfo, trapsNeverHappen); + Collector collector(collectedInfo, trapsNeverHappen, &wasm); collector.walkModuleCode(&wasm); collector.setModule(&wasm); for (auto& global : wasm.globals) { diff --git a/test/lit/passes/unsubtyping-jsinterop.wast b/test/lit/passes/unsubtyping-jsinterop.wast index f043d5a16d1..ca860a31e94 100644 --- a/test/lit/passes/unsubtyping-jsinterop.wast +++ b/test/lit/passes/unsubtyping-jsinterop.wast @@ -514,3 +514,355 @@ ;; CHECK: (global $private (ref null $private-sub) (ref.null none)) (global $private (ref null $private-sub) (ref.null none)) ) + +(module + ;; Exported function. Parameters flow in and results flow out. + ;; CHECK: (type $super (sub (struct))) + (type $super (sub (struct))) + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub (sub $super (descriptor $desc) (struct (field i32)))) + (type $sub (sub $super (descriptor $desc) (struct (field i32)))) + ;; CHECK: (type $desc (describes $sub) (struct (field externref))) + (type $desc (describes $sub) (struct (field externref))) + ) + ;; CHECK: (type $3 (func (param (ref $super)) (result anyref))) + + ;; CHECK: (export "test" (func $test)) + + ;; CHECK: (func $test (type $3) (param $0 (ref $super)) (result anyref) + ;; CHECK-NEXT: (local $sub (ref null $sub)) + ;; CHECK-NEXT: (local.get $sub) + ;; CHECK-NEXT: ) + (func $test (export "test") (param (ref $super)) (result anyref) + (local $sub (ref null $sub)) + ;; $super flowing in from JS means it is cast from any. Since $sub flows out + ;; via any, it could be flowing back in and must remain a subtype of $super. + (local.get $sub) + ) +) + +(module + ;; Imported function. Parameters flow out and results flow in. + ;; CHECK: (type $super (sub (struct))) + (type $super (sub (struct))) + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub (sub $super (descriptor $desc) (struct (field i32)))) + (type $sub (sub $super (descriptor $desc) (struct (field i32)))) + ;; CHECK: (type $desc (describes $sub) (struct (field externref))) + (type $desc (describes $sub) (struct (field externref))) + ) + ;; CHECK: (type $3 (func)) + + ;; CHECK: (type $4 (func (param anyref) (result (ref $super)))) + + ;; CHECK: (import "" "" (func $import (type $4) (param anyref) (result (ref $super)))) + (import "" "" (func $import (param anyref) (result (ref $super)))) + ;; CHECK: (func $test (type $3) + ;; CHECK-NEXT: (local $sub (ref null $sub)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (call $import + ;; CHECK-NEXT: (local.get $sub) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + (local $sub (ref null $sub)) + ;; Now $sub flows out via the parameter and $super flows back in via the + ;; result. Once again, $sub must maintain its descriptor and must remain a + ;; subtype of $super. + (drop + (call $import + (local.get $sub) + ) + ) + ) +) + +(module + ;; Exported immutable global flows out. + ;; CHECK: (type $super (sub (struct))) + (type $super (sub (struct))) + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub-in (sub (struct))) + (type $sub-in (sub $super (struct))) + ;; CHECK: (type $sub-out (sub $super (descriptor $desc) (struct (field i32)))) + (type $sub-out (sub $super (descriptor $desc) (struct (field i32)))) + ;; CHECK: (type $desc (describes $sub-out) (struct (field externref))) + (type $desc (describes $sub-out) (struct (field externref))) + ) + + ;; $super flows out via the exported global and also flows back in because the + ;; global is immutable. + ;; CHECK: (type $4 (func (result anyref))) + + ;; CHECK: (type $5 (func (result (ref null $super)))) + + ;; CHECK: (global $g (ref null $super) (ref.null none)) + (global $g (export "g") (ref null $super) (ref.null none)) + + ;; CHECK: (export "g" (global $g)) + + ;; CHECK: (func $test-in (type $4) (result anyref) + ;; CHECK-NEXT: (local $sub-in (ref null $sub-in)) + ;; CHECK-NEXT: (local.get $sub-in) + ;; CHECK-NEXT: ) + (func $test-in (result anyref) + (local $sub-in (ref null $sub-in)) + ;; This requires that $sub-in is a subtype of $any. If $super flows in from + ;; JS, the cast from any to $super will force $sub-in to remain a subtype of + ;; $super. + (local.get $sub-in) + ) + + ;; CHECK: (func $test-out (type $5) (result (ref null $super)) + ;; CHECK-NEXT: (local $sub-out (ref null $sub-out)) + ;; CHECK-NEXT: (local.get $sub-out) + ;; CHECK-NEXT: ) + (func $test-out (result (ref null $super)) + (local $sub-out (ref null $sub-out)) + ;; This requires that $sub-out is a subtype of $super. If $super flows out + ;; to JS, $sub-out will have to keep its descriptor. + (local.get $sub-out) + ) +) + +(module + ;; Exported mutable global flows in and out. + ;; CHECK: (type $super (sub (struct))) + (type $super (sub (struct))) + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub-in (sub $super (struct))) + (type $sub-in (sub $super (struct))) + ;; CHECK: (type $sub-out (sub $super (descriptor $desc) (struct (field i32)))) + (type $sub-out (sub $super (descriptor $desc) (struct (field i32)))) + ;; CHECK: (type $desc (describes $sub-out) (struct (field externref))) + (type $desc (describes $sub-out) (struct (field externref))) + ) + + ;; $super flows out via the exported global and also flows back in because the + ;; global is immutable. + ;; CHECK: (type $4 (func (result anyref))) + + ;; CHECK: (type $5 (func (result (ref null $super)))) + + ;; CHECK: (global $g (mut (ref null $super)) (ref.null none)) + (global $g (export "g") (mut (ref null $super)) (ref.null none)) + + ;; CHECK: (export "g" (global $g)) + + ;; CHECK: (func $test-in (type $4) (result anyref) + ;; CHECK-NEXT: (local $sub-in (ref null $sub-in)) + ;; CHECK-NEXT: (local.get $sub-in) + ;; CHECK-NEXT: ) + (func $test-in (result anyref) + (local $sub-in (ref null $sub-in)) + ;; This requires that $sub-in is a subtype of $any. If $super flows in from + ;; JS, the cast from any to $super will force $sub-in to remain a subtype of + ;; $super. + (local.get $sub-in) + ) + + ;; CHECK: (func $test-out (type $5) (result (ref null $super)) + ;; CHECK-NEXT: (local $sub-out (ref null $sub-out)) + ;; CHECK-NEXT: (local.get $sub-out) + ;; CHECK-NEXT: ) + (func $test-out (result (ref null $super)) + (local $sub-out (ref null $sub-out)) + ;; This requires that $sub-out is a subtype of $super. If $super flows out + ;; to JS, $sub-out will have to keep its descriptor. + (local.get $sub-out) + ) +) + +(module + ;; Imported immutable global flows in. + ;; CHECK: (type $super (sub (struct))) + (type $super (sub (struct))) + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub-in (sub $super (struct))) + (type $sub-in (sub $super (struct))) + ;; CHECK: (type $sub-out (sub $super (struct (field i32)))) + (type $sub-out (sub $super (descriptor $desc) (struct (field i32)))) + (type $desc (describes $sub-out) (struct (field externref))) + ) + + ;; $super flows out via the exported global and also flows back in because the + ;; global is immutable. + ;; CHECK: (type $3 (func (result anyref))) + + ;; CHECK: (type $4 (func (result (ref null $super)))) + + ;; CHECK: (import "" "" (global $g (ref null $super))) + (import "" "" (global $g (ref null $super))) + + ;; CHECK: (func $test-in (type $3) (result anyref) + ;; CHECK-NEXT: (local $sub-in (ref null $sub-in)) + ;; CHECK-NEXT: (local.get $sub-in) + ;; CHECK-NEXT: ) + (func $test-in (result anyref) + (local $sub-in (ref null $sub-in)) + ;; This requires that $sub-in is a subtype of $any. If $super flows in from + ;; JS, the cast from any to $super will force $sub-in to remain a subtype of + ;; $super. + (local.get $sub-in) + ) + + ;; CHECK: (func $test-out (type $4) (result (ref null $super)) + ;; CHECK-NEXT: (local $sub-out (ref null $sub-out)) + ;; CHECK-NEXT: (local.get $sub-out) + ;; CHECK-NEXT: ) + (func $test-out (result (ref null $super)) + (local $sub-out (ref null $sub-out)) + ;; This requires that $sub-out is a subtype of $super. If $super flows out + ;; to JS, $sub-out will have to keep its descriptor. + (local.get $sub-out) + ) +) + +(module + ;; Imported mutable global flows in and out. + ;; CHECK: (type $super (sub (struct))) + (type $super (sub (struct))) + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub-in (sub $super (struct))) + (type $sub-in (sub $super (struct))) + ;; CHECK: (type $sub-out (sub $super (descriptor $desc) (struct (field i32)))) + (type $sub-out (sub $super (descriptor $desc) (struct (field i32)))) + ;; CHECK: (type $desc (describes $sub-out) (struct (field externref))) + (type $desc (describes $sub-out) (struct (field externref))) + ) + + ;; $super flows out via the exported global and also flows back in because the + ;; global is immutable. + ;; CHECK: (type $4 (func (result anyref))) + + ;; CHECK: (type $5 (func (result (ref null $super)))) + + ;; CHECK: (import "" "" (global $g (mut (ref null $super)))) + (import "" "" (global $g (mut (ref null $super)))) + + ;; CHECK: (func $test-in (type $4) (result anyref) + ;; CHECK-NEXT: (local $sub-in (ref null $sub-in)) + ;; CHECK-NEXT: (local.get $sub-in) + ;; CHECK-NEXT: ) + (func $test-in (result anyref) + (local $sub-in (ref null $sub-in)) + ;; This requires that $sub-in is a subtype of $any. If $super flows in from + ;; JS, the cast from any to $super will force $sub-in to remain a subtype of + ;; $super. + (local.get $sub-in) + ) + + ;; CHECK: (func $test-out (type $5) (result (ref null $super)) + ;; CHECK-NEXT: (local $sub-out (ref null $sub-out)) + ;; CHECK-NEXT: (local.get $sub-out) + ;; CHECK-NEXT: ) + (func $test-out (result (ref null $super)) + (local $sub-out (ref null $sub-out)) + ;; This requires that $sub-out is a subtype of $super. If $super flows out + ;; to JS, $sub-out will have to keep its descriptor. + (local.get $sub-out) + ) +) + +(module + ;; Exported table flows in and out. + ;; CHECK: (type $super (sub (struct))) + (type $super (sub (struct))) + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub-in (sub $super (struct))) + (type $sub-in (sub $super (struct))) + ;; CHECK: (type $sub-out (sub $super (descriptor $desc) (struct (field i32)))) + (type $sub-out (sub $super (descriptor $desc) (struct (field i32)))) + ;; CHECK: (type $desc (describes $sub-out) (struct (field externref))) + (type $desc (describes $sub-out) (struct (field externref))) + ) + + ;; $super flows out via the exported global and also flows back in because the + ;; global is immutable. + ;; CHECK: (type $4 (func (result anyref))) + + ;; CHECK: (type $5 (func (result (ref null $super)))) + + ;; CHECK: (table $t 1 (ref null $super)) + (table $t (export "t") 1 (ref null $super)) + + ;; CHECK: (export "t" (table $t)) + + ;; CHECK: (func $test-in (type $4) (result anyref) + ;; CHECK-NEXT: (local $sub-in (ref null $sub-in)) + ;; CHECK-NEXT: (local.get $sub-in) + ;; CHECK-NEXT: ) + (func $test-in (result anyref) + (local $sub-in (ref null $sub-in)) + ;; This requires that $sub-in is a subtype of $any. If $super flows in from + ;; JS, the cast from any to $super will force $sub-in to remain a subtype of + ;; $super. + (local.get $sub-in) + ) + + ;; CHECK: (func $test-out (type $5) (result (ref null $super)) + ;; CHECK-NEXT: (local $sub-out (ref null $sub-out)) + ;; CHECK-NEXT: (local.get $sub-out) + ;; CHECK-NEXT: ) + (func $test-out (result (ref null $super)) + (local $sub-out (ref null $sub-out)) + ;; This requires that $sub-out is a subtype of $super. If $super flows out + ;; to JS, $sub-out will have to keep its descriptor. + (local.get $sub-out) + ) +) + +(module + ;; Imported table flows in and out. + ;; CHECK: (type $super (sub (struct))) + (type $super (sub (struct))) + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $sub-in (sub $super (struct))) + (type $sub-in (sub $super (struct))) + ;; CHECK: (type $sub-out (sub $super (descriptor $desc) (struct (field i32)))) + (type $sub-out (sub $super (descriptor $desc) (struct (field i32)))) + ;; CHECK: (type $desc (describes $sub-out) (struct (field externref))) + (type $desc (describes $sub-out) (struct (field externref))) + ) + + ;; $super flows out via the exported global and also flows back in because the + ;; global is immutable. + ;; CHECK: (type $4 (func (result anyref))) + + ;; CHECK: (type $5 (func (result (ref null $super)))) + + ;; CHECK: (import "" "" (table $t 1 (ref null $super))) + (import "" "" (table $t 1 (ref null $super))) + + ;; CHECK: (func $test-in (type $4) (result anyref) + ;; CHECK-NEXT: (local $sub-in (ref null $sub-in)) + ;; CHECK-NEXT: (local.get $sub-in) + ;; CHECK-NEXT: ) + (func $test-in (result anyref) + (local $sub-in (ref null $sub-in)) + ;; This requires that $sub-in is a subtype of $any. If $super flows in from + ;; JS, the cast from any to $super will force $sub-in to remain a subtype of + ;; $super. + (local.get $sub-in) + ) + + ;; CHECK: (func $test-out (type $5) (result (ref null $super)) + ;; CHECK-NEXT: (local $sub-out (ref null $sub-out)) + ;; CHECK-NEXT: (local.get $sub-out) + ;; CHECK-NEXT: ) + (func $test-out (result (ref null $super)) + (local $sub-out (ref null $sub-out)) + ;; This requires that $sub-out is a subtype of $super. If $super flows out + ;; to JS, $sub-out will have to keep its descriptor. + (local.get $sub-out) + ) +) From b69dfe87123db3e47e1c22e35154716fbb10c87c Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 10 Mar 2026 22:06:07 -0700 Subject: [PATCH 2/2] fix comments --- test/lit/passes/unsubtyping-jsinterop.wast | 80 +++++++++++----------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/test/lit/passes/unsubtyping-jsinterop.wast b/test/lit/passes/unsubtyping-jsinterop.wast index ca860a31e94..460de499280 100644 --- a/test/lit/passes/unsubtyping-jsinterop.wast +++ b/test/lit/passes/unsubtyping-jsinterop.wast @@ -594,8 +594,8 @@ (type $desc (describes $sub-out) (struct (field externref))) ) - ;; $super flows out via the exported global and also flows back in because the - ;; global is immutable. + ;; $super flows out via the exported global, but does not flow back in because + ;; the global is immutable. ;; CHECK: (type $4 (func (result anyref))) ;; CHECK: (type $5 (func (result (ref null $super)))) @@ -611,9 +611,9 @@ ;; CHECK-NEXT: ) (func $test-in (result anyref) (local $sub-in (ref null $sub-in)) - ;; This requires that $sub-in is a subtype of $any. If $super flows in from - ;; JS, the cast from any to $super will force $sub-in to remain a subtype of - ;; $super. + ;; This requires that $sub-in is a subtype of any, but since $super does not + ;; flow back in and is not cast from any, we can still remove the $sub-in <: + ;; $super relationship. (local.get $sub-in) ) @@ -623,8 +623,8 @@ ;; CHECK-NEXT: ) (func $test-out (result (ref null $super)) (local $sub-out (ref null $sub-out)) - ;; This requires that $sub-out is a subtype of $super. If $super flows out - ;; to JS, $sub-out will have to keep its descriptor. + ;; This requires that $sub-out is a subtype of $super. Since $super flows + ;; out to JS, $sub-out will have to keep its descriptor. (local.get $sub-out) ) ) @@ -644,7 +644,7 @@ ) ;; $super flows out via the exported global and also flows back in because the - ;; global is immutable. + ;; global is mutable. ;; CHECK: (type $4 (func (result anyref))) ;; CHECK: (type $5 (func (result (ref null $super)))) @@ -660,9 +660,9 @@ ;; CHECK-NEXT: ) (func $test-in (result anyref) (local $sub-in (ref null $sub-in)) - ;; This requires that $sub-in is a subtype of $any. If $super flows in from - ;; JS, the cast from any to $super will force $sub-in to remain a subtype of - ;; $super. + ;; This requires that $sub-in is a subtype of any. Since $super flows in + ;; from JS, the cast from any to $super forces $sub-in to remain a subtype + ;; of $super. (local.get $sub-in) ) @@ -672,8 +672,8 @@ ;; CHECK-NEXT: ) (func $test-out (result (ref null $super)) (local $sub-out (ref null $sub-out)) - ;; This requires that $sub-out is a subtype of $super. If $super flows out - ;; to JS, $sub-out will have to keep its descriptor. + ;; This requires that $sub-out is a subtype of $super. Since $super flows + ;; out to JS, $sub-out will have to keep its descriptor. (local.get $sub-out) ) ) @@ -691,7 +691,7 @@ (type $desc (describes $sub-out) (struct (field externref))) ) - ;; $super flows out via the exported global and also flows back in because the + ;; $super flows in via the imported global, but does not flow out because the ;; global is immutable. ;; CHECK: (type $3 (func (result anyref))) @@ -706,9 +706,9 @@ ;; CHECK-NEXT: ) (func $test-in (result anyref) (local $sub-in (ref null $sub-in)) - ;; This requires that $sub-in is a subtype of $any. If $super flows in from - ;; JS, the cast from any to $super will force $sub-in to remain a subtype of - ;; $super. + ;; This requires that $sub-in is a subtype of any. Since $super flows in + ;; from JS, the cast from any to $super forces $sub-in to remain a subtype + ;; of $super. (local.get $sub-in) ) @@ -718,8 +718,8 @@ ;; CHECK-NEXT: ) (func $test-out (result (ref null $super)) (local $sub-out (ref null $sub-out)) - ;; This requires that $sub-out is a subtype of $super. If $super flows out - ;; to JS, $sub-out will have to keep its descriptor. + ;; This requires that $sub-out is a subtype of $super, but since $super does + ;; not flow out to JS, $sub-out does not need to keep its descriptor. (local.get $sub-out) ) ) @@ -738,8 +738,8 @@ (type $desc (describes $sub-out) (struct (field externref))) ) - ;; $super flows out via the exported global and also flows back in because the - ;; global is immutable. + ;; $super flows in via the imported global and also flows back out because the + ;; global is mutable. ;; CHECK: (type $4 (func (result anyref))) ;; CHECK: (type $5 (func (result (ref null $super)))) @@ -753,9 +753,9 @@ ;; CHECK-NEXT: ) (func $test-in (result anyref) (local $sub-in (ref null $sub-in)) - ;; This requires that $sub-in is a subtype of $any. If $super flows in from - ;; JS, the cast from any to $super will force $sub-in to remain a subtype of - ;; $super. + ;; This requires that $sub-in is a subtype of any. Since $super flows in + ;; from JS, the cast from any to $super forces $sub-in to remain a subtype + ;; of $super. (local.get $sub-in) ) @@ -765,8 +765,8 @@ ;; CHECK-NEXT: ) (func $test-out (result (ref null $super)) (local $sub-out (ref null $sub-out)) - ;; This requires that $sub-out is a subtype of $super. If $super flows out - ;; to JS, $sub-out will have to keep its descriptor. + ;; This requires that $sub-out is a subtype of $super. Since $super flows + ;; out to JS, $sub-out will have to keep its descriptor. (local.get $sub-out) ) ) @@ -785,8 +785,8 @@ (type $desc (describes $sub-out) (struct (field externref))) ) - ;; $super flows out via the exported global and also flows back in because the - ;; global is immutable. + ;; $super flows out via the exported table and also flows back in because + ;; tables are mutable. ;; CHECK: (type $4 (func (result anyref))) ;; CHECK: (type $5 (func (result (ref null $super)))) @@ -802,9 +802,9 @@ ;; CHECK-NEXT: ) (func $test-in (result anyref) (local $sub-in (ref null $sub-in)) - ;; This requires that $sub-in is a subtype of $any. If $super flows in from - ;; JS, the cast from any to $super will force $sub-in to remain a subtype of - ;; $super. + ;; This requires that $sub-in is a subtype of any. Since $super flows in + ;; from JS, the cast from any to $super forces $sub-in to remain a subtype + ;; of $super. (local.get $sub-in) ) @@ -814,8 +814,8 @@ ;; CHECK-NEXT: ) (func $test-out (result (ref null $super)) (local $sub-out (ref null $sub-out)) - ;; This requires that $sub-out is a subtype of $super. If $super flows out - ;; to JS, $sub-out will have to keep its descriptor. + ;; This requires that $sub-out is a subtype of $super. Since $super flows + ;; out to JS, $sub-out will have to keep its descriptor. (local.get $sub-out) ) ) @@ -834,8 +834,8 @@ (type $desc (describes $sub-out) (struct (field externref))) ) - ;; $super flows out via the exported global and also flows back in because the - ;; global is immutable. + ;; $super flows in via the imported table and also flows back out because + ;; tables are mutable. ;; CHECK: (type $4 (func (result anyref))) ;; CHECK: (type $5 (func (result (ref null $super)))) @@ -849,9 +849,9 @@ ;; CHECK-NEXT: ) (func $test-in (result anyref) (local $sub-in (ref null $sub-in)) - ;; This requires that $sub-in is a subtype of $any. If $super flows in from - ;; JS, the cast from any to $super will force $sub-in to remain a subtype of - ;; $super. + ;; This requires that $sub-in is a subtype of any. Since $super flows in + ;; from JS, the cast from any to $super forces $sub-in to remain a subtype + ;; of $super. (local.get $sub-in) ) @@ -861,8 +861,8 @@ ;; CHECK-NEXT: ) (func $test-out (result (ref null $super)) (local $sub-out (ref null $sub-out)) - ;; This requires that $sub-out is a subtype of $super. If $super flows out - ;; to JS, $sub-out will have to keep its descriptor. + ;; This requires that $sub-out is a subtype of $super. Since $super flows + ;; out to JS, $sub-out will have to keep its descriptor. (local.get $sub-out) ) )