Skip to content

Commit ee4f82d

Browse files
authored
Handle exact types on JS boundary in Unsubtyping (#8451)
By considering exactness of types flowing in from JS, we can optimize more precisely because exact casts require fewer subtype relationships to be kept.
1 parent ffc5bfc commit ee4f82d

2 files changed

Lines changed: 184 additions & 15 deletions

File tree

src/passes/Unsubtyping.cpp

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,9 @@ struct TypeTree {
171171
// The index of the described and descriptor types, if they are necessary.
172172
std::optional<Index> described;
173173
std::optional<Index> descriptor;
174-
// Whether this type might flow out to JS from a JS-called function or via
175-
// extern.convert_any.
176-
bool exposedToJS = false;
174+
// Whether subtypes of this type might flow out to JS from a JS-called
175+
// function or via extern.convert_any.
176+
bool subtypesExposedToJS = false;
177177

178178
Node(HeapType type, Index index) : type(type), parent(index) {}
179179
};
@@ -251,17 +251,17 @@ struct TypeTree {
251251
return std::nullopt;
252252
}
253253

254-
void setExposedToJS(HeapType type) {
254+
void setSubtypesExposedToJS(HeapType type) {
255255
auto index = getIndex(type);
256-
nodes[index].exposedToJS = true;
256+
nodes[index].subtypesExposedToJS = true;
257257
}
258258

259-
bool isExposedToJS(HeapType type) const {
259+
bool areSubtypesExposedToJS(HeapType type) const {
260260
auto index = maybeGetIndex(type);
261261
if (!index) {
262262
return false;
263263
}
264-
return nodes[*index].exposedToJS;
264+
return nodes[*index].subtypesExposedToJS;
265265
}
266266

267267
struct SupertypeIterator {
@@ -614,13 +614,22 @@ struct Unsubtyping : Pass, Noter<Unsubtyping> {
614614
work.push_back({Kind::Descriptor, described, descriptor});
615615
}
616616

617-
void noteExposedToJS(HeapType type) {
618-
types.setExposedToJS(type);
617+
void noteExposedToJS(Type type) {
618+
if (!type.isRef()) {
619+
return;
620+
}
621+
noteExposedToJS(type.getHeapType(), type.getExactness());
622+
}
623+
624+
void noteExposedToJS(HeapType type, Exactness exact = Inexact) {
619625
// Keep any descriptor that may configure a prototype.
620626
if (auto desc = type.getDescriptorType();
621627
desc && StructUtils::hasPossibleJSPrototypeField(*desc)) {
622628
noteDescriptor(type, *desc);
623629
}
630+
if (exact == Inexact) {
631+
types.setSubtypesExposedToJS(type);
632+
}
624633
}
625634

626635
void analyzePublicTypes(Module& wasm) {
@@ -653,9 +662,8 @@ struct Unsubtyping : Pass, Noter<Unsubtyping> {
653662
// prototypes.
654663
auto flowOut = [&](Type type) {
655664
if (Type::isSubType(type, anyref)) {
656-
auto heapType = type.getHeapType();
657-
noteSubtype(heapType, HeapType::any);
658-
noteExposedToJS(heapType);
665+
noteSubtype(type.getHeapType(), HeapType::any);
666+
noteExposedToJS(type);
659667
}
660668
};
661669

@@ -751,7 +759,7 @@ struct Unsubtyping : Pass, Noter<Unsubtyping> {
751759
Set<std::pair<HeapType, HeapType>> descriptors;
752760

753761
// Observed externalized types.
754-
Set<HeapType> exposedToJS;
762+
Set<Type> exposedToJS;
755763
};
756764

757765
struct Collector
@@ -854,7 +862,7 @@ struct Unsubtyping : Pass, Noter<Unsubtyping> {
854862
// extern.convert_any makes its operand type visible to JS, which may
855863
// require us to keep descriptors that configure prototypes.
856864
if (curr->op == ExternConvertAny && curr->value->type.isRef()) {
857-
info.exposedToJS.insert(curr->value->type.getHeapType());
865+
info.exposedToJS.insert(curr->value->type);
858866
}
859867
}
860868
};
@@ -945,7 +953,7 @@ struct Unsubtyping : Pass, Noter<Unsubtyping> {
945953
types.setSupertype(sub, super);
946954

947955
// If the supertype is exposed to JS, the subtype potentially is as well.
948-
if (types.isExposedToJS(super)) {
956+
if (types.areSubtypesExposedToJS(super)) {
949957
noteExposedToJS(sub);
950958
}
951959

test/lit/passes/unsubtyping-jsinterop.wast

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,33 @@
542542
)
543543
)
544544

545+
(module
546+
;; Same, but now with an exact type flowing in.
547+
;; CHECK: (type $super (sub (struct)))
548+
(type $super (sub (struct)))
549+
(rec
550+
;; CHECK: (rec
551+
;; CHECK-NEXT: (type $sub (sub (descriptor $desc) (struct (field i32))))
552+
(type $sub (sub $super (descriptor $desc) (struct (field i32))))
553+
;; CHECK: (type $desc (describes $sub) (struct (field externref)))
554+
(type $desc (describes $sub) (struct (field externref)))
555+
)
556+
;; CHECK: (type $3 (func (param (ref (exact $super))) (result anyref)))
557+
558+
;; CHECK: (export "test" (func $test))
559+
560+
;; CHECK: (func $test (type $3) (param $0 (ref (exact $super))) (result anyref)
561+
;; CHECK-NEXT: (local $sub (ref null $sub))
562+
;; CHECK-NEXT: (local.get $sub)
563+
;; CHECK-NEXT: )
564+
(func $test (export "test") (param (ref (exact $super))) (result anyref)
565+
(local $sub (ref null $sub))
566+
;; Now the cast when $super flows in from JS is exact, so we do not need to
567+
;; keep $sub a subtype of $super.
568+
(local.get $sub)
569+
)
570+
)
571+
545572
(module
546573
;; Imported function. Parameters flow out and results flow in.
547574
;; CHECK: (type $super (sub (struct)))
@@ -580,6 +607,44 @@
580607
)
581608
)
582609

610+
(module
611+
;; Same, but now with an exact type flowing in.
612+
;; CHECK: (type $super (sub (struct)))
613+
(type $super (sub (struct)))
614+
(rec
615+
;; CHECK: (rec
616+
;; CHECK-NEXT: (type $sub (sub (descriptor $desc) (struct (field i32))))
617+
(type $sub (sub $super (descriptor $desc) (struct (field i32))))
618+
;; CHECK: (type $desc (describes $sub) (struct (field externref)))
619+
(type $desc (describes $sub) (struct (field externref)))
620+
)
621+
;; CHECK: (type $3 (func))
622+
623+
;; CHECK: (type $4 (func (param anyref) (result (ref (exact $super)))))
624+
625+
;; CHECK: (import "" "" (func $import (type $4) (param anyref) (result (ref (exact $super)))))
626+
(import "" "" (func $import (param anyref) (result (ref (exact $super)))))
627+
;; CHECK: (func $test (type $3)
628+
;; CHECK-NEXT: (local $sub (ref null $sub))
629+
;; CHECK-NEXT: (drop
630+
;; CHECK-NEXT: (call $import
631+
;; CHECK-NEXT: (local.get $sub)
632+
;; CHECK-NEXT: )
633+
;; CHECK-NEXT: )
634+
;; CHECK-NEXT: )
635+
(func $test
636+
(local $sub (ref null $sub))
637+
;; Now $sub flows out via the parameter and $super flows back in via the
638+
;; result. But because the cast on the way back in is exact, we do not need
639+
;; to keep $sub a subtype of $super.
640+
(drop
641+
(call $import
642+
(local.get $sub)
643+
)
644+
)
645+
)
646+
)
647+
583648
(module
584649
;; Exported immutable global flows out.
585650
;; CHECK: (type $super (sub (struct)))
@@ -678,6 +743,55 @@
678743
)
679744
)
680745

746+
(module
747+
;; Same, but now with an exact global type.
748+
;; CHECK: (type $super (sub (struct)))
749+
(type $super (sub (struct)))
750+
(rec
751+
;; CHECK: (rec
752+
;; CHECK-NEXT: (type $sub-in (sub (struct)))
753+
(type $sub-in (sub $super (struct)))
754+
;; CHECK: (type $sub-out (sub $super (struct (field i32))))
755+
(type $sub-out (sub $super (descriptor $desc) (struct (field i32))))
756+
(type $desc (describes $sub-out) (struct (field externref)))
757+
)
758+
759+
;; $super flows out via the exported global and also flows back in because the
760+
;; global is mutable.
761+
;; CHECK: (type $3 (func (result anyref)))
762+
763+
;; CHECK: (type $4 (func (result (ref null $super))))
764+
765+
;; CHECK: (global $g (mut (ref null (exact $super))) (ref.null none))
766+
(global $g (export "g") (mut (ref null (exact $super))) (ref.null none))
767+
768+
;; CHECK: (export "g" (global $g))
769+
770+
;; CHECK: (func $test-in (type $3) (result anyref)
771+
;; CHECK-NEXT: (local $sub-in (ref null $sub-in))
772+
;; CHECK-NEXT: (local.get $sub-in)
773+
;; CHECK-NEXT: )
774+
(func $test-in (result anyref)
775+
(local $sub-in (ref null $sub-in))
776+
;; This requires that $sub-in is a subtype of any. Since $super flows in
777+
;; from JS, it is cast from any to $super. But because that cast is exact,
778+
;; we do not need $sub-in to remain a subtype of $super.
779+
(local.get $sub-in)
780+
)
781+
782+
;; CHECK: (func $test-out (type $4) (result (ref null $super))
783+
;; CHECK-NEXT: (local $sub-out (ref null $sub-out))
784+
;; CHECK-NEXT: (local.get $sub-out)
785+
;; CHECK-NEXT: )
786+
(func $test-out (result (ref null $super))
787+
(local $sub-out (ref null $sub-out))
788+
;; This requires that $sub-out is a subtype of $super. Since only exact
789+
;; $super flows out to JS, $sub-out does not flow out and does not need to
790+
;; keep its descriptor.
791+
(local.get $sub-out)
792+
)
793+
)
794+
681795
(module
682796
;; Imported immutable global flows in.
683797
;; CHECK: (type $super (sub (struct)))
@@ -866,3 +980,50 @@
866980
(local.get $sub-out)
867981
)
868982
)
983+
984+
(module
985+
;; Same, but with an exact table type.
986+
;; CHECK: (type $super (sub (struct)))
987+
(type $super (sub (struct)))
988+
(rec
989+
;; CHECK: (rec
990+
;; CHECK-NEXT: (type $sub-in (sub (struct)))
991+
(type $sub-in (sub $super (struct)))
992+
;; CHECK: (type $sub-out (sub $super (struct (field i32))))
993+
(type $sub-out (sub $super (descriptor $desc) (struct (field i32))))
994+
(type $desc (describes $sub-out) (struct (field externref)))
995+
)
996+
997+
;; $super flows in via the imported table and also flows back out because
998+
;; tables are mutable.
999+
;; CHECK: (type $3 (func (result anyref)))
1000+
1001+
;; CHECK: (type $4 (func (result (ref null $super))))
1002+
1003+
;; CHECK: (import "" "" (table $t 1 (ref null (exact $super))))
1004+
(import "" "" (table $t 1 (ref null (exact $super))))
1005+
1006+
;; CHECK: (func $test-in (type $3) (result anyref)
1007+
;; CHECK-NEXT: (local $sub-in (ref null $sub-in))
1008+
;; CHECK-NEXT: (local.get $sub-in)
1009+
;; CHECK-NEXT: )
1010+
(func $test-in (result anyref)
1011+
(local $sub-in (ref null $sub-in))
1012+
;; This requires that $sub-in is a subtype of any. Since $super flows in
1013+
;; from JS, it is cast from any to $super. But because that cast is exact,
1014+
;; we do not need $sub-in to remain a subtype of $super.
1015+
(local.get $sub-in)
1016+
)
1017+
1018+
;; CHECK: (func $test-out (type $4) (result (ref null $super))
1019+
;; CHECK-NEXT: (local $sub-out (ref null $sub-out))
1020+
;; CHECK-NEXT: (local.get $sub-out)
1021+
;; CHECK-NEXT: )
1022+
(func $test-out (result (ref null $super))
1023+
(local $sub-out (ref null $sub-out))
1024+
;; This requires that $sub-out is a subtype of $super. Since only exact
1025+
;; $super flows out to JS, $sub-out does not flow out and does not need to
1026+
;; keep its descriptor.
1027+
(local.get $sub-out)
1028+
)
1029+
)

0 commit comments

Comments
 (0)