Skip to content

Commit 32c180b

Browse files
panagosg7facebook-github-bot
authored andcommitted
[flow] better handing of unions in type guard checks
Summary: This change tries to prevent performance regression in code like the following ``` declare var x: T; declare var foo: (x: mixed) => x is T if (foo(x)) {} ``` where T is a large enum-like union. Before this change, `predicate_no_concretization` on a `LatentP` predicate would do concretize all types, including unions, leading to the breakup of the type of the large union `T`. In `types_differ`, each member of `T` is compared against each member of the type guard type, which also happens to be `T`. This effectively yields a O(n^2) operation which becomes too slow on large enough `T`s. This diff tries to address this by introducing a layer of faster checks before we get to the slow check above. Specifically, 1. we make the initial concretization preserve enum-like unions (which tend to be large) 2. we use fast comparison of this concretized type against the guard (`try_intersect`) 3. if we don't reach a good result, we proceed to concretize the input again, this time using full concretization on unions. 4. for each part of the newly concretized input, we run the same comparison against the guard (`try_intersect`) Changelog: [internal] Reviewed By: SamChou19815 Differential Revision: D69762017 fbshipit-source-id: 39f0af1541f3646c977cdc8860290a2c298fd622
1 parent d3c7390 commit 32c180b

File tree

4 files changed

+66
-10
lines changed

4 files changed

+66
-10
lines changed

src/typing/flow_js.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ struct
472472
ConcretizeT
473473
{
474474
reason = _;
475-
kind = ConcretizeForPredicate ConcretizeForMaybeOrExistPredicateTest;
475+
kind = ConcretizeForPredicate ConcretizeKeepOptimizedUnions;
476476
seen = _;
477477
collector;
478478
}

src/typing/predicate_kit.ml

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ let concretization_variant_of_predicate = function
3030
| MaybeP
3131
| NotP MaybeP
3232
| TruthyP
33-
| NotP TruthyP ->
34-
ConcretizeForMaybeOrExistPredicateTest
33+
| NotP TruthyP
34+
| LatentP _
35+
| NotP (LatentP _) ->
36+
ConcretizeKeepOptimizedUnions
3537
| _ -> ConcretizeForGeneralPredicateTest
3638

3739
type predicate_result_mut =
@@ -615,23 +617,56 @@ and intersect =
615617
in
616618
fun cx t1 t2 ->
617619
let reason1 = TypeUtil.reason_of_t t1 in
620+
(* Pre-processing of t1 has concretized it up to optimized unions. It is
621+
* important to keep it this way to prevent expensive checks right away.
622+
* Consider for example the code:
623+
*
624+
* declare var x: T;
625+
* declare var foo: (x: mixed) => x is T
626+
* if (foo(x)) {}
627+
*
628+
* where T is a really large enum-like union. `try_intersect` will quickly
629+
* return `t1` as the result here, without us having to try to fully
630+
* concretize `t1`.
631+
*)
618632
(* Input t1 is already concretized as input to the predicate mechanism *)
619633
match try_intersect cx reason1 (C.wrap_unsafe t1) t2 with
620634
| Some t -> t
621635
| None ->
622-
let r = update_desc_reason invalidate_rtype_alias reason1 in
623-
IntersectionT (r, InterRep.make t2 t1 [])
636+
(* No definitive refinement found. We fall back to more expensive
637+
* concretization that breaks up all unions (including optimized ones). *)
638+
possible_concrete_types_for_inspection cx reason1 t1
639+
|> Base.List.map ~f:(fun t1 ->
640+
(* t1 was just concretized *)
641+
match try_intersect cx reason1 (C.wrap_unsafe t1) t2 with
642+
| Some t -> t
643+
| None ->
644+
let r = update_desc_reason invalidate_rtype_alias reason1 in
645+
IntersectionT (r, InterRep.make t2 t1 [])
646+
)
647+
|> TypeUtil.union_of_ts (update_desc_reason invalidate_rtype_alias reason1)
624648

625649
(* This utility is expected to be used when negating the refinement of a type [t1]
626650
* with a type guard `x is t2`. The only case considered here is that of t1 <: t2.
627651
* This means that the positive branch will always be taken, and so we are left with
628652
* `empty` in the negated case. *)
629653
and type_guard_diff cx t1 t2 =
654+
let reason1 = TypeUtil.reason_of_t t1 in
630655
if TypeUtil.quick_subtype t1 t2 || speculative_subtyping_succeeds cx t1 t2 then
631-
let r = update_desc_reason invalidate_rtype_alias (TypeUtil.reason_of_t t1) in
656+
let r = update_desc_reason invalidate_rtype_alias reason1 in
632657
Type_filter.TypeFilterResult { type_ = DefT (r, EmptyT); changed = true }
633658
else
634-
Type_filter.TypeFilterResult { type_ = t1; changed = false }
659+
let t1s_conc = possible_concrete_types_for_inspection cx reason1 t1 in
660+
let (ts_rev, changed) =
661+
Base.List.fold t1s_conc ~init:([], false) ~f:(fun (acc, changed) t1 ->
662+
if TypeUtil.quick_subtype t1 t2 || speculative_subtyping_succeeds cx t1 t2 then
663+
(acc, changed)
664+
else
665+
(t1 :: acc, true)
666+
)
667+
in
668+
let r1 = update_desc_reason invalidate_rtype_alias reason1 in
669+
Type_filter.TypeFilterResult { type_ = TypeUtil.union_of_ts r1 (List.rev ts_rev); changed }
635670

636671
and prop_exists_test cx key sense obj result_collector =
637672
match has_prop cx (OrdinaryName key) obj with

src/typing/type.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,7 +1013,7 @@ module rec TypeTerm : sig
10131013

10141014
and predicate_concretizer_variant =
10151015
| ConcretizeForGeneralPredicateTest
1016-
| ConcretizeForMaybeOrExistPredicateTest
1016+
| ConcretizeKeepOptimizedUnions
10171017
| ConcretizeRHSForInstanceOfPredicateTest
10181018
| ConcretizeRHSForLiteralPredicateTest
10191019

@@ -4141,7 +4141,7 @@ let string_of_use_op_rec : use_op -> string =
41414141

41424142
let string_of_predicate_concretizer_variant = function
41434143
| ConcretizeForGeneralPredicateTest -> "ConcretizeForGeneralPredicateTest"
4144-
| ConcretizeForMaybeOrExistPredicateTest -> "ConcretizeForMaybeOrExistPredicateTest"
4144+
| ConcretizeKeepOptimizedUnions -> "ConcretizeKeepOptimizedUnions"
41454145
| ConcretizeRHSForInstanceOfPredicateTest -> "ConcretizeRHSForInstanceOfPredicateTest"
41464146
| ConcretizeRHSForLiteralPredicateTest -> "ConcretizeRHSForLiteralPredicateTest"
41474147

tests/type_guards/refinement.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ function sentinel_multi_tag() {
125125
}
126126

127127

128-
function negation() {
128+
function negation1() {
129129
declare function isNumber(x: mixed): x is number;
130130
declare function isString(x: mixed): x is string;
131131

@@ -296,3 +296,24 @@ function getRaccoon(s: string): ?Raccoon {
296296
}
297297
return null;
298298
}
299+
300+
function negation2() {
301+
declare var isA: (x: mixed) => x is 'a';
302+
declare var x: 'a' | 'c';
303+
304+
if (isA(x)) { return; }
305+
x as 'c'; // okay
306+
}
307+
308+
function negation3() {
309+
declare function isFalsey(value: ?mixed): value is null | void | false | '' | 0;
310+
311+
function foo(arr: ?Array<mixed> | $ReadOnlyArray<mixed>): void {
312+
if (isFalsey(arr)) {
313+
return;
314+
}
315+
arr as Array<mixed> | $ReadOnlyArray<mixed>; // okay
316+
}
317+
318+
foo([]); // triggrers union optimization
319+
}

0 commit comments

Comments
 (0)