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..460de499280 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, 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)))) + + ;; 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, 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) + ) + + ;; 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. Since $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 mutable. + ;; 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. 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) + ) + + ;; 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. Since $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 in via the imported global, but does not flow out 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. 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) + ) + + ;; 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, but since $super does + ;; not flow out to JS, $sub-out does not need 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 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)))) + + ;; 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. 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) + ) + + ;; 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. Since $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 table and also flows back in because + ;; tables are mutable. + ;; 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. 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) + ) + + ;; 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. Since $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 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)))) + + ;; 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. 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) + ) + + ;; 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. Since $super flows + ;; out to JS, $sub-out will have to keep its descriptor. + (local.get $sub-out) + ) +)