Skip to content

Commit a37f19d

Browse files
viratyosinfacebook-github-bot
authored andcommitted
Avoid subtyping incompleteness in Typing_solver.bind_to_lower_bound by detecting when freshen_inside_ty does nothing
Summary: NOTE: I found this when working on a class refinement refactor. The refactor involves creating tyvars with lower bounds where the lower bounds are types involves intersections and unions. The refactor triggered false errors in WWW that this diff aims to fix. I have reproduced the crux of the issue via union and intersection type hints. In `bind_to_lower_bound`, if `freshen_inside_ty` doesn't actually freshen anything, we end up doing a `T <: T` subtype check which (a) doesn't have any effect on the environment and (b) can sometimes fail due to incompleteness issues despite it being always true. (see added test for incompleteness example) One option would be to use `ty_equal` if the subtype check fails (see alternative D68981350), but this diff instead changes `freshen_inside_ty` to return an option—`None` when nothing was freshened and the output would have been the same as the input. Now, in`bind_to_lower_bound` when `freshen_inside_ty` returns None, we skip the subtyping step. --- In the test case: ``` (A & ((B & C) | D)) <: (A & ((B & C) | D)) ``` is broken down into: ``` (A & ((B & C) | D)) < A &&& (A & ((B & C) | D)) < ((B & C) | D) ``` The latter of these propositions fails because [this heuristic](https://fburl.com/code/r88xune0) sees the RHS union contains an intersection and splits the RHS union instead of the LHS intersection Reviewed By: dlreeves Differential Revision: D69210301 fbshipit-source-id: 539c2ce1e05b695daa3f62ea8778d05fc494b62c
1 parent 03fa6f0 commit a37f19d

File tree

3 files changed

+118
-58
lines changed

3 files changed

+118
-58
lines changed

hphp/hack/src/typing/typing_solver.ml

Lines changed: 88 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,23 @@ let log_remaining_prop env =
6666
* Contra<#1,t2>
6767
* leaving the invariant component alone.
6868
*)
69-
let rec freshen_inside_ty env ty =
70-
let default () = (env, ty) in
69+
let rec freshen_inside_ty env ty :
70+
(Typing_env_types.env * Typing_defs.locl_ty) option =
71+
let freshen_inside_tyl env tyl :
72+
(Typing_env_types.env * Typing_defs.locl_ty list) option =
73+
let (env, ty_either_l) =
74+
List.map_env env tyl ~f:(fun env ty ->
75+
match freshen_inside_ty env ty with
76+
| Some (env, ty) -> (env, Either.First ty)
77+
| None -> (env, Either.Second ty))
78+
in
79+
if List.for_all ty_either_l ~f:Either.is_second then
80+
None
81+
else
82+
Some (env, List.map ty_either_l ~f:Either.value)
83+
in
84+
let open Option.Let_syntax in
85+
let default () = None in
7186
let (env, ty) = Env.expand_type env ty in
7287
let (r, ty_) = deref ty in
7388
match ty_ with
@@ -87,44 +102,47 @@ let rec freshen_inside_ty env ty =
87102
let variancel =
88103
List.replicate ~num:(List.length tyl) Ast_defs.Invariant
89104
in
90-
let (env, tyl) = freshen_tparams env variancel tyl in
91-
(env, mk (r, Tnewtype (name, tyl, ty)))
105+
let* (env, tyl) = freshen_tparams env variancel tyl in
106+
return (env, mk (r, Tnewtype (name, tyl, ty)))
92107
| Tdependent _ -> default ()
93108
(* Nullable is covariant *)
94109
| Toption ty ->
95-
let (env, ty) = freshen_inside_ty env ty in
96-
(env, mk (r, Toption ty))
110+
let* (env, ty) = freshen_inside_ty env ty in
111+
return (env, mk (r, Toption ty))
97112
| Tunion tyl ->
98-
let (env, tyl) = List.map_env env tyl ~f:freshen_inside_ty in
99-
(env, mk (r, Tunion tyl))
113+
let* (env, tyl) = freshen_inside_tyl env tyl in
114+
return (env, mk (r, Tunion tyl))
100115
| Tintersection tyl ->
101-
let (env, tyl) = List.map_env env tyl ~f:freshen_inside_ty in
102-
Inter.intersect_list env r tyl
116+
let* (env, tyl) = freshen_inside_tyl env tyl in
117+
return (Inter.intersect_list env r tyl)
103118
(* Tuples are covariant *)
104119
| Ttuple { t_required; t_extra = Textra { t_optional; t_variadic } } ->
105120
let (env, t_required) = List.map_env env t_required ~f:freshen_ty in
106121
let (env, t_optional) = List.map_env env t_optional ~f:freshen_ty in
107122
let (env, t_variadic) = freshen_ty env t_variadic in
108-
( env,
109-
mk (r, Ttuple { t_required; t_extra = Textra { t_optional; t_variadic } })
110-
)
123+
Some
124+
( env,
125+
mk
126+
(r, Ttuple { t_required; t_extra = Textra { t_optional; t_variadic } })
127+
)
111128
| Ttuple { t_required; t_extra = Tsplat t_splat } ->
112129
let (env, t_required) = List.map_env env t_required ~f:freshen_ty in
113130
let (env, t_splat) = freshen_ty env t_splat in
114-
(env, mk (r, Ttuple { t_required; t_extra = Tsplat t_splat }))
131+
Some (env, mk (r, Ttuple { t_required; t_extra = Tsplat t_splat }))
115132
(* Shape data is covariant *)
116133
| Tshape { s_origin = _; s_unknown_value = shape_kind; s_fields = fdm } ->
117134
let (env, fdm) = ShapeFieldMap.map_env freshen_ty env fdm in
118135
(* TODO(shapes) should freshening impact unknown type? *)
119-
( env,
120-
mk
121-
( r,
122-
Tshape
123-
{
124-
s_origin = Missing_origin;
125-
s_unknown_value = shape_kind;
126-
s_fields = fdm;
127-
} ) )
136+
Some
137+
( env,
138+
mk
139+
( r,
140+
Tshape
141+
{
142+
s_origin = Missing_origin;
143+
s_unknown_value = shape_kind;
144+
s_fields = fdm;
145+
} ) )
128146
(* Functions are covariant in return type, contravariant in parameter types *)
129147
| Tfun ft ->
130148
let (env, ft_ret) = freshen_ty env ft.ft_ret in
@@ -133,11 +151,11 @@ let rec freshen_inside_ty env ty =
133151
let (env, fp_type) = freshen_ty env p.fp_type in
134152
(env, { p with fp_type }))
135153
in
136-
(env, mk (r, Tfun { ft with ft_ret; ft_params }))
154+
Some (env, mk (r, Tfun { ft with ft_ret; ft_params }))
137155
| Tnewtype (name, _, ty)
138156
when String.equal name Naming_special_names.Classes.cSupportDyn ->
139-
let (env, ty) = freshen_inside_ty env ty in
140-
(env, MakeType.supportdyn r ty)
157+
let* (env, ty) = freshen_inside_ty env ty in
158+
return (env, MakeType.supportdyn r ty)
141159
| Tnewtype (name, tyl, ty) ->
142160
if List.is_empty tyl then
143161
default ()
@@ -148,8 +166,8 @@ let rec freshen_inside_ty env ty =
148166
default ()
149167
| Decl_entry.Found td ->
150168
let variancel = List.map td.td_tparams ~f:(fun t -> t.tp_variance) in
151-
let (env, tyl) = freshen_tparams env variancel tyl in
152-
(env, mk (r, Tnewtype (name, tyl, ty)))
169+
let* (env, tyl) = freshen_tparams env variancel tyl in
170+
return (env, mk (r, Tnewtype (name, tyl, ty)))
153171
end
154172
| Tclass ((p, cid), e, tyl) ->
155173
if List.is_empty tyl then
@@ -163,24 +181,24 @@ let rec freshen_inside_ty env ty =
163181
let variancel =
164182
List.map (Cls.tparams cls) ~f:(fun t -> t.tp_variance)
165183
in
166-
let (env, tyl) = freshen_tparams env variancel tyl in
167-
(env, mk (r, Tclass ((p, cid), e, tyl)))
184+
let* (env, tyl) = freshen_tparams env variancel tyl in
185+
return (env, mk (r, Tclass ((p, cid), e, tyl)))
168186
end
169187
| Tvec_or_dict (ty1, ty2) ->
170188
let (env, ty1) = freshen_ty env ty1 in
171189
let (env, ty2) = freshen_ty env ty2 in
172-
(env, mk (r, Tvec_or_dict (ty1, ty2)))
190+
Some (env, mk (r, Tvec_or_dict (ty1, ty2)))
173191
| Tvar _ -> default ()
174192
| Taccess (ty, ids) ->
175193
let (env, ty) = freshen_ty env ty in
176-
(env, mk (r, Taccess (ty, ids)))
194+
Some (env, mk (r, Taccess (ty, ids)))
177195
| Tunapplied_alias _ -> default ()
178196
| Tclass_ptr ty ->
179197
let (env, ty) = freshen_ty env ty in
180198
(* TODO(T199606542) Matches classname but does it actually make sense
181199
* to freshen class pointer? This will disappear when we move to direct
182200
* strings *)
183-
(env, mk (r, Tclass_ptr ty))
201+
Some (env, mk (r, Tclass_ptr ty))
184202

185203
and freshen_ty env ty =
186204
if TUtils.is_tyvar_error env ty then
@@ -189,18 +207,24 @@ and freshen_ty env ty =
189207
Env.fresh_type_invariant env (get_pos ty |> Pos_or_decl.unsafe_to_raw_pos)
190208

191209
and freshen_tparams env variancel tyl =
192-
match (variancel, tyl) with
193-
| ([], []) -> (env, [])
194-
| (variance :: variancel, ty :: tyl) ->
195-
let (env, tyl) = freshen_tparams env variancel tyl in
196-
let (env, ty) =
197-
if Ast_defs.(equal_variance variance Invariant) then
198-
(env, ty)
199-
else
200-
freshen_ty env ty
210+
if List.for_all variancel ~f:Ast_defs.(equal_variance Invariant) then
211+
None
212+
else
213+
let rec freshen_tparams_ env variancel tyl =
214+
match (variancel, tyl) with
215+
| ([], []) -> (env, [])
216+
| (variance :: variancel, ty :: tyl) ->
217+
let (env, tyl) = freshen_tparams_ env variancel tyl in
218+
let (env, ty) =
219+
if Ast_defs.(equal_variance variance Invariant) then
220+
(env, ty)
221+
else
222+
freshen_ty env ty
223+
in
224+
(env, ty :: tyl)
225+
| _ -> (env, tyl)
201226
in
202-
(env, ty :: tyl)
203-
| _ -> (env, tyl)
227+
Some (freshen_tparams_ env variancel tyl)
204228

205229
let bind env var (ty : locl_ty) =
206230
let old_env = env in
@@ -267,19 +291,21 @@ let bind_to_lower_bound ~freshen env r var lower_bounds =
267291
*)
268292
let (env, freshen_ty_err, ty) =
269293
if freshen then
270-
let (env, newty) = freshen_inside_ty env ty in
271-
(* In theory, the following subtype would only fail if the lower bound
272-
* was already in conflict with another bound. However we don't
273-
* add such conflicting bounds to avoid cascading errors, so in theory,
274-
* the following subtype calls should not fail, and the error callback
275-
* should not matter. *)
276-
let on_error =
277-
Some
278-
(Typing_error.Reasons_callback.unify_error_at
279-
@@ Env.get_tyvar_pos env var)
280-
in
281-
let (env, ty_err_opt) = TUtils.sub_type env ty newty on_error in
282-
(env, ty_err_opt, newty)
294+
match freshen_inside_ty env ty with
295+
| None -> (env, None, ty)
296+
| Some (env, newty) ->
297+
(* In theory, the following subtype would only fail if the lower bound
298+
* was already in conflict with another bound. However we don't
299+
* add such conflicting bounds to avoid cascading errors, so in theory,
300+
* the following subtype calls should not fail, and the error callback
301+
* should not matter. *)
302+
let on_error =
303+
Some
304+
(Typing_error.Reasons_callback.unify_error_at
305+
@@ Env.get_tyvar_pos env var)
306+
in
307+
let (env, ty_err_opt) = TUtils.sub_type env ty newty on_error in
308+
(env, ty_err_opt, newty)
283309
else
284310
(env, None, ty)
285311
in
@@ -481,7 +507,11 @@ let try_bind_to_equal_bound ~freshen env r var =
481507
in
482508
(match shallow_match with
483509
| Some (LoclType shallow_match) ->
484-
let (env, ty) = freshen_inside_ty env shallow_match in
510+
let (env, ty) =
511+
Option.value
512+
(freshen_inside_ty env shallow_match)
513+
~default:(env, shallow_match)
514+
in
485515
let var_ty = mk (r, Tvar var) in
486516
(* In theory, the following subtype would only fail if the shallow match
487517
* we've found was already in conflict with another bound. However we don't
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?hh
2+
3+
<<file: __EnableUnstableFeatures('union_intersection_type_hints')>>
4+
5+
final class MyContainer<T> {
6+
public function put(T $x): void {}
7+
public function get(): T {
8+
throw new Exception();
9+
}
10+
}
11+
12+
interface IHasMeth {
13+
public function meth(): int;
14+
}
15+
16+
interface A extends IHasMeth {}
17+
interface B extends IHasMeth {}
18+
interface C extends IHasMeth {}
19+
interface D extends IHasMeth {}
20+
21+
// (A & ((B & C) | D)) is special because in our current subtyping logic
22+
// (A & ((B & C) | D)) <: (A & ((B & C) | D)) fails due to incompleteness
23+
function myfoo((A & ((B & C) | D)) $x): void {
24+
$c = new MyContainer();
25+
$c->put($x);
26+
$y = $c->get();
27+
// $y : tyvar w/ lower bound: A & ((B & C) | D)
28+
$y->meth(); // forces solve
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
No errors

0 commit comments

Comments
 (0)