Skip to content

Commit 6542f53

Browse files
andrewjkennedyfacebook-github-bot
authored andcommitted
Fix non-transitive subtyping with generic parameters and recursion
Summary: Subtyping should be transitive. It's not, as the following test demonstrates: ``` interface I<-Ti> { } class B { } class C extends B implements I<B> { } class D<T as C> { public function fromTtoC(T $x):C { return $x; } public function fromCtoIT(C $c):I<T> { return $c; } public function fromTtoIT(T $x):I<T> { return $x; } } ``` Hack rejects the third method. This is due to the way that we detect cycles involving generic parameters: if during subtype simplification we arrive at a generic parameter that we have already seen, then we assume that subtyping will loop. This isn't sufficient. Consider the case above: under the assumption that `T <: C` we are checking `T <: I<T>`. This leads to the following chain of assertions: `T <: I<T>` => `C <: I<T>` (by assumption) => `I<B> <: I<T>` (by inheritance) => `T <: B` (by contravariance) At this point, we bail, because we've seen `T` before. But we should continue: `C <: B` (by assumption) => `B <: B` (by inheritance) => done. Instead, we now keep in essence a set of visited *goals* i.e. subtype assertions. We do this only for assertions of the form `T <: t` or `t <: T` where `T` is a generic parameter, in order to avoid regressing performance of subtyping. Reviewed By: vsiles Differential Revision: D25909931 fbshipit-source-id: 43047654daa2d521a38e4cefc5a7b48b6ec22520
1 parent f848b33 commit 6542f53

File tree

5 files changed

+139
-43
lines changed

5 files changed

+139
-43
lines changed

hphp/hack/src/typing/typing_subtype.ml

+103-43
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,70 @@ module ShapeMap = Nast.ShapeMap
2828
module ShapeSet = Ast_defs.ShapeSet
2929
module Nast = Aast
3030

31+
(* We maintain a "visited" set for subtype goals. We do this only
32+
* for goals of the form T <: t or t <: T where T is a generic parameter,
33+
* as this is the more common case.
34+
* T83096774: work out how to do this *efficiently* for all subtype goals.
35+
*
36+
* Here's a non-trivial example (assuming a contravariant type Contra).
37+
* Under assumption T <: Contra<Contra<T>> show T <: Contra<T>.
38+
* This leads to cycle of implications
39+
* T <: Contra<T> =>
40+
* Contra<Contra<T>> <: Contra<T> =>
41+
* T <: Contra<T>
42+
* at which point we are back at the original goal.
43+
*
44+
* Note that it's not enough to just keep a set of visited generic parameters,
45+
* else we would reject good code e.g. consider
46+
* class C extends B implements Contra<B>
47+
* Now under assumption T <: C show T <: Contra<T>
48+
* This leads to cycle of implications
49+
* T <: Contra<T> =>
50+
* C <: Contra<T> =>
51+
* Contra<B> <: Contra<T> =>
52+
* T <: B => // DO NOT REJECT here just because we've visited T before!
53+
* C <: B => done.
54+
*
55+
* We represent the visited set as a map from generic parameters
56+
* to pairs of sets of types, such that an entry T := ({t1,...,tm},{u1,...,un})
57+
* represents a set of goals
58+
* T <: u1, ..., t <: un , t1 <: T, ..., tn <: T
59+
*)
60+
module VisitedGoals = struct
61+
type t = (ITySet.t * ITySet.t) SMap.t
62+
63+
let empty : t = SMap.empty
64+
65+
(* Return None if (name <: ty) is already present, otherwise return Some v'
66+
* where v' has the pair added
67+
*)
68+
let try_add_visited_generic_sub v name ty =
69+
match SMap.find_opt name v with
70+
| None -> Some (SMap.add name (ITySet.empty, ITySet.singleton ty) v)
71+
| Some (lower, upper) ->
72+
if ITySet.mem ty upper then
73+
None
74+
else
75+
Some (SMap.add name (lower, ITySet.add ty upper) v)
76+
77+
(* Return None if (ty <: name) is already present, otherwise return Some v'
78+
* where v' has the pair added
79+
*)
80+
let try_add_visited_generic_super v ty name =
81+
match SMap.find_opt name v with
82+
| None -> Some (SMap.add name (ITySet.singleton ty, ITySet.empty) v)
83+
| Some (lower, upper) ->
84+
if ITySet.mem ty lower then
85+
None
86+
else
87+
Some (SMap.add name (ITySet.add ty lower, upper) v)
88+
end
89+
3190
type subtype_env = {
32-
seen_generic_params: SSet.t option;
91+
(* If set, finish as soon as we see a goal of the form T <: t or t <: T for generic parameter T *)
92+
ignore_generic_params: bool;
93+
(* If above is not set, maintain a visited goal set *)
94+
visited: VisitedGoals.t;
3395
no_top_bottom: bool;
3496
treat_dynamic_as_bottom: bool;
3597
(* Used with global flag --enable-sound-dynamic-type, allow_subtype_of_dynamic
@@ -38,28 +100,21 @@ type subtype_env = {
38100
on_error: Errors.typing_error_callback;
39101
}
40102

41-
let empty_seen = Some SSet.empty
42-
43103
let make_subtype_env
44-
?(seen_generic_params = empty_seen)
104+
?(ignore_generic_params = false)
45105
?(no_top_bottom = false)
46106
?(treat_dynamic_as_bottom = false)
47107
?(allow_subtype_of_dynamic = false)
48108
on_error =
49109
{
50-
seen_generic_params;
110+
ignore_generic_params;
111+
visited = VisitedGoals.empty;
51112
no_top_bottom;
52113
treat_dynamic_as_bottom;
53114
allow_subtype_of_dynamic;
54115
on_error;
55116
}
56117

57-
let add_seen_generic subtype_env name =
58-
match subtype_env.seen_generic_params with
59-
| Some seen ->
60-
{ subtype_env with seen_generic_params = Some (SSet.add name seen) }
61-
| None -> subtype_env
62-
63118
type reactivity_extra_info = {
64119
method_info: (* method_name *) (string * (* is_static *) bool) option;
65120
class_ty: phase_ty option;
@@ -419,15 +474,21 @@ and default_subtype
419474
| (_, Tgeneric (name_sub, tyargs)) ->
420475
(* TODO(T69551141) handle type arguments. right now, just passin tyargs to
421476
Env.get_upper_bounds *)
422-
(match subtype_env.seen_generic_params with
423-
| None -> default env
424-
| Some seen ->
477+
( if subtype_env.ignore_generic_params then
478+
default env
479+
else
425480
(* If we've seen this type parameter before then we must have gone
426481
* round a cycle so we fail
427482
*)
428-
if SSet.mem name_sub seen then
429-
invalid ~fail env
430-
else
483+
match
484+
VisitedGoals.try_add_visited_generic_sub
485+
subtype_env.visited
486+
name_sub
487+
ty_super
488+
with
489+
| None -> invalid ~fail env
490+
| Some new_visited ->
491+
let subtype_env = { subtype_env with visited = new_visited } in
431492
(* If the generic is actually an expression dependent type,
432493
we need to update this_ty
433494
*)
@@ -440,7 +501,6 @@ and default_subtype
440501
else
441502
this_ty
442503
in
443-
let subtype_env = add_seen_generic subtype_env name_sub in
444504
(* Otherwise, we collect all the upper bounds ("as" constraints) on
445505
the generic parameter, and check each of these in turn against
446506
ty_super until one of them succeeds
@@ -479,7 +539,7 @@ and default_subtype
479539
env
480540
|> try_bounds
481541
(Typing_set.elements
482-
(Env.get_upper_bounds env name_sub tyargs)))
542+
(Env.get_upper_bounds env name_sub tyargs)) )
483543
|> (* Turn error into a generic error about the type parameter *)
484544
if_unsat (invalid ~fail)
485545
| (_, Tdynamic) when subtype_env.treat_dynamic_as_bottom -> valid env
@@ -536,7 +596,7 @@ and default_subtype
536596
* C <: J ==> Unsat _
537597
* (Assuming we have T <: D in tpenv, and class D implements I)
538598
* vec<T> <: vec<I> ==> True
539-
* This last one would be left as T <: I if seen_generic_params is None
599+
* This last one would be left as T <: I if subtype_env.ignore_generic_params=true
540600
*)
541601
and simplify_subtype_i
542602
~(subtype_env : subtype_env)
@@ -935,7 +995,7 @@ and simplify_subtype_i
935995
| LoclType lty_sub ->
936996
(match deref lty_sub with
937997
| (_, (Tunion _ | Terr | Tvar _)) -> default_subtype env
938-
| (_, Tgeneric _) when Option.is_none subtype_env.seen_generic_params ->
998+
| (_, Tgeneric _) when subtype_env.ignore_generic_params ->
939999
default_subtype env
9401000
(* Num is not atomic: it is equivalent to int|float. The rule below relies
9411001
* on ty_sub not being a union e.g. consider num <: arraykey | float, so
@@ -1033,8 +1093,7 @@ and simplify_subtype_i
10331093
(* We do not want to decompose Toption for these cases *)
10341094
| ((_, (Tvar _ | Tunion _ | Tintersection _)), _) ->
10351095
default_subtype env
1036-
| ((_, Tgeneric _), _)
1037-
when Option.is_none subtype_env.seen_generic_params ->
1096+
| ((_, Tgeneric _), _) when subtype_env.ignore_generic_params ->
10381097
(* TODO(T69551141) handle type arguments ? *)
10391098
default_subtype env
10401099
(* If t1 <: ?t2 and t1 is an abstract type constrained as t1',
@@ -1164,16 +1223,21 @@ and simplify_subtype_i
11641223
| Terr ->
11651224
default_subtype env
11661225
| _ ->
1167-
(match subtype_env.seen_generic_params with
1168-
| None -> default env
1169-
| Some seen ->
1226+
if subtype_env.ignore_generic_params then
1227+
default env
1228+
else (
11701229
(* If we've seen this type parameter before then we must have gone
11711230
* round a cycle so we fail
11721231
*)
1173-
if SSet.mem name_super seen then
1174-
invalid_env env
1175-
else
1176-
let subtype_env = add_seen_generic subtype_env name_super in
1232+
match
1233+
VisitedGoals.try_add_visited_generic_super
1234+
subtype_env.visited
1235+
ety_sub
1236+
name_super
1237+
with
1238+
| None -> invalid_env env
1239+
| Some new_visited ->
1240+
let subtype_env = { subtype_env with visited = new_visited } in
11771241
(* Collect all the lower bounds ("super" constraints) on the
11781242
* generic parameter, and check ty_sub against each of them in turn
11791243
* until one of them succeeds *)
@@ -1190,7 +1254,8 @@ and simplify_subtype_i
11901254
|> try_bounds
11911255
(Typing_set.elements
11921256
(Env.get_lower_bounds env name_super tyargs_super))
1193-
|> if_unsat invalid_env)))
1257+
|> if_unsat invalid_env
1258+
)))
11941259
| (_, Tnonnull) ->
11951260
(match ety_sub with
11961261
| ConstraintType cty ->
@@ -3308,17 +3373,12 @@ and is_sub_type_alt_i
33083373
let (_env, prop) =
33093374
simplify_subtype_i
33103375
~subtype_env:
3311-
{
3312-
seen_generic_params =
3313-
( if ignore_generic_params then
3314-
None
3315-
else
3316-
empty_seen );
3317-
no_top_bottom;
3318-
treat_dynamic_as_bottom;
3319-
allow_subtype_of_dynamic;
3320-
on_error = Errors.unify_error_at pos;
3321-
}
3376+
(make_subtype_env
3377+
~ignore_generic_params
3378+
~no_top_bottom
3379+
~treat_dynamic_as_bottom
3380+
~allow_subtype_of_dynamic
3381+
(Errors.unify_error_at pos))
33223382
~this_ty
33233383
(* It is weird that this can cause errors, but I am wary to discard them.
33243384
* Using the generic unify_error to maintain current behavior. *)
@@ -3611,7 +3671,7 @@ let rec decompose_subtype
36113671
ty_super;
36123672
let (env, prop) =
36133673
simplify_subtype
3614-
~subtype_env:(make_subtype_env ~seen_generic_params:None on_error)
3674+
~subtype_env:(make_subtype_env ~ignore_generic_params:true on_error)
36153675
~this_ty:None
36163676
ty_sub
36173677
ty_super
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?hh
2+
// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
4+
interface I<-Ti> { }
5+
class B { }
6+
class C extends B implements I<B> { }
7+
class D<T as C> {
8+
public function fromTtoC(T $x):C {
9+
return $x;
10+
}
11+
public function fromCtoIT(C $c):I<T> {
12+
return $c;
13+
}
14+
public function fromTtoIT(T $x):I<T> {
15+
return $x;
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
No errors
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?hh
2+
// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
4+
class Cov<+Tc> { }
5+
function foo<T2 as T,T as Cov<T2>>(T $x):Cov<T> {
6+
return $x;
7+
}
8+
9+
class Contra<-TN> { }
10+
function bar<T as Contra<Contra<T>>>(T $x):Contra<T> {
11+
return $x;
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
File "recursive_bounds.php", line 11, characters 10-11:
2+
Invalid return type (Typing[4110])
3+
File "recursive_bounds.php", line 10, characters 44-52:
4+
Expected `Contra<T>`
5+
File "recursive_bounds.php", line 10, characters 38-38:
6+
But got `T`

0 commit comments

Comments
 (0)