From 26435d1397bea30641c7ffca6b040a2b9fd838c9 Mon Sep 17 00:00:00 2001 From: Michael Tingley Date: Mon, 27 Mar 2017 14:26:26 -0700 Subject: [PATCH] Implement typing for optional shape fields Summary: This diff models typing for optional shape fields. = Model These fields are modeled as follows: ```name=typing_defs.ml,lang=ocaml 'phase shape_field_type = { sft_optional : bool; sft_ty : 'phase ty; } ``` And integrated into the existing type structure as follows: ```name=typing_defs.ml,lang=diff ty_ = (* ...existing types... *) | Tshape : shape_fields_known * - ('phase ty Nast.ShapeMap.t) + ('phase shape_field_type Nast.ShapeMap.t) -> 'phase ty_ ``` = Justification There were two reasonable ways of modeling optional shape fields: # `Tshape` remains unchanged, and a new toplevel type `Toptional_shape_field` is introduced. # `Tshape` is changed to hold `shape_field_type`s that explicitly denote whether they are optional or not. I have opted for #2. Choice #1 has these trade-offs: # (good) The shape handling code remains unchanged. # (ok/good) `Tshape` is symmetric in a sense, and does not need additional handling logic. Handling logic can be placed closer to where toplevel processing for `ty` appears. # (bad) **Everywhere** that a `ty` is processed, a new case has to be added to handle the optional shape field. In most of these cases, optional shape fields are not even logically possible! # (extremely bad) There is special casing logic that needs to be written for optional shape fields, and this logic must be done **in the context of a `Tshape`**. This design would break the abstraction, forcing us to push through `Tshape` information to the toplevel processing for `ty`. Choice #2 has these trade-offs: # (good) An `shape_field_type` may //only// appear in the context of a `Tshape`. We will never process an `shape_field_type` in a place where it's not logically possible. Consequently, the processing always has visibility to the `Tshape` of which the `shape_field_type` is a part of. # (good) This design forces the developer to handle processing for `shape_field_type`s when processing a `Tshape`. It will be very hard to "forget" to handle this case, since it will be enforced by the type system. # (bad) It turns out there are lots of places that don't care about whether a type is optional or not. These points in the code now have to be filled with boilerplate to unwrap the `ty` from the `shape_field_type`. = Conclusion Whereas #1 has serious logic and maintainability concerns, #2 really just has concerns related to code cleanliness. Additionally, to handle those code cleanliness concerns, I created the `typing_helpers` library to handle patterns that occurred repeatedly in this diff. = Other notes There are lots of locations in this diff where I'm not completely sure what's going on. Please let me know if there are any areas that look suspect. If things do in fact look off, it might be useful for us to go through some parts of the diff in person. Since the existing typecheck tests and the new ones for the optional shape fields all pass, I'm reasonably confident that there's not a regression, but I can't be sure. Reviewed By: dlreeves Differential Revision: D4563246 fbshipit-source-id: da8d446429351bf804c0485335c29ab83fd049da --- hphp/hack/src/decl/decl_hint.ml | 5 +- hphp/hack/src/decl/decl_instantiate.ml | 2 +- hphp/hack/src/decl/decl_pos_utils.ml | 150 +++++++++--------- hphp/hack/src/typing/type_mapper.ml | 8 +- hphp/hack/src/typing/type_visitor.ml | 10 +- hphp/hack/src/typing/typing.ml | 13 +- hphp/hack/src/typing/typing_arrays.ml | 8 +- hphp/hack/src/typing/typing_defs.ml | 55 ++++++- hphp/hack/src/typing/typing_generic.ml | 2 +- hphp/hack/src/typing/typing_phase.ml | 2 +- hphp/hack/src/typing/typing_print.ml | 37 ++++- hphp/hack/src/typing/typing_shapes.ml | 15 +- hphp/hack/src/typing/typing_structure.ml | 41 +++-- hphp/hack/src/typing/typing_subtype.ml | 21 ++- hphp/hack/src/typing/typing_unify.ml | 32 +++- .../hack/src/typing/typing_unify_recursive.ml | 3 +- hphp/hack/src/typing/typing_utils.ml | 21 ++- hphp/hack/src/typing/typing_variance.ml | 4 +- hphp/hack/src/utils/errors/errors.ml | 8 + hphp/hack/src/utils/errors/errors_sig.ml | 1 + ...g_with_optional_and_non_optional_types.php | 14 ++ ...th_optional_and_non_optional_types.php.exp | 6 + ...ional_and_non_optional_types.php.no_format | 0 ...tructural_subtyping_with_optional_type.php | 13 ++ ...tural_subtyping_with_optional_type.php.exp | 1 + ...subtyping_with_optional_type.php.no_format | 0 ...ing_with_optional_type_and_extra_types.php | 15 ++ ...with_optional_type_and_extra_types.php.exp | 1 + ...ptional_type_and_extra_types.php.no_format | 0 ...ture_with_optional_and_required_fields.php | 10 ++ ..._with_optional_and_required_fields.php.exp | 9 ++ ...optional_and_required_fields.php.no_format | 0 ...n_allows_fields_that_are_both_optional.php | 9 ++ ...lows_fields_that_are_both_optional.php.exp | 1 + ...ields_that_are_both_optional.php.no_format | 0 ...field_where_required_field_is_consumed.php | 9 ++ ...d_where_required_field_is_consumed.php.exp | 8 + ...e_required_field_is_consumed.php.no_format | 0 ...field_where_optional_field_is_consumed.php | 9 ++ ...d_where_optional_field_is_consumed.php.exp | 8 + ...e_optional_field_is_consumed.php.no_format | 0 ...rtype_field_and_required_subtype_field.php | 14 ++ ...e_field_and_required_subtype_field.php.exp | 1 + ...d_and_required_subtype_field.php.no_format | 0 ...rtype_field_and_optional_subtype_field.php | 14 ++ ...e_field_and_optional_subtype_field.php.exp | 6 + ...d_and_optional_subtype_field.php.no_format | 0 hphp/hack/test/typecheck/shape14.php | 2 +- .../hack/test/typecheck/shape14.php.no_format | 0 hphp/hack/test/typecheck/shape24.php | 2 +- .../hack/test/typecheck/shape24.php.no_format | 0 hphp/hack/test/typecheck/shape25.php | 2 +- .../hack/test/typecheck/shape25.php.no_format | 0 hphp/hack/test/typecheck/shape26.php.exp | 8 +- hphp/hack/test/typecheck/shape27.php | 4 +- .../hack/test/typecheck/shape27.php.no_format | 0 hphp/hack/test/typecheck/shape29.php.exp | 4 +- hphp/hack/test/typecheck/shape_idx3.php | 4 +- .../test/typecheck/shape_idx3.php.no_format | 0 hphp/hack/test/typecheck/shape_modified6.php | 2 +- .../typecheck/shape_modified6.php.no_format | 0 .../hack/test/typecheck/shape_remove_key6.php | 2 +- .../typecheck/shape_remove_key6.php.no_format | 0 .../generic_type_structure.php.exp | 2 +- .../type_structure/type_structure1.php.exp | 2 +- 65 files changed, 470 insertions(+), 150 deletions(-) create mode 100644 hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php create mode 100644 hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php.exp create mode 100644 hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php.no_format create mode 100644 hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php create mode 100644 hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php.exp create mode 100644 hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php.no_format create mode 100644 hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php create mode 100644 hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php.exp create mode 100644 hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php.no_format create mode 100644 hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php create mode 100644 hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php.exp create mode 100644 hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php.no_format create mode 100644 hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php create mode 100644 hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php.exp create mode 100644 hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php.no_format create mode 100644 hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php create mode 100644 hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php.exp create mode 100644 hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php.no_format create mode 100644 hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php create mode 100644 hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php.exp create mode 100644 hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php.no_format create mode 100644 hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php create mode 100644 hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php.exp create mode 100644 hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php.no_format create mode 100644 hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php create mode 100644 hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php.exp create mode 100644 hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php.no_format create mode 100644 hphp/hack/test/typecheck/shape14.php.no_format create mode 100644 hphp/hack/test/typecheck/shape24.php.no_format create mode 100644 hphp/hack/test/typecheck/shape25.php.no_format create mode 100644 hphp/hack/test/typecheck/shape27.php.no_format create mode 100644 hphp/hack/test/typecheck/shape_idx3.php.no_format create mode 100644 hphp/hack/test/typecheck/shape_modified6.php.no_format create mode 100644 hphp/hack/test/typecheck/shape_remove_key6.php.no_format diff --git a/hphp/hack/src/decl/decl_hint.ml b/hphp/hack/src/decl/decl_hint.ml index b48acb3bbf1d4..5c18c316337b1 100644 --- a/hphp/hack/src/decl/decl_hint.ml +++ b/hphp/hack/src/decl/decl_hint.ml @@ -21,9 +21,8 @@ let rec hint env (p, h) = let h = hint_ p env h in Typing_reason.Rhint p, h -(* TODO(tingley): Record the optional status and use this to reconcile types. *) -and shape_field_info_to_shape_field_type env { sfi_optional=_; sfi_hint } = - hint env sfi_hint +and shape_field_info_to_shape_field_type env { sfi_optional; sfi_hint } = + { sft_optional = sfi_optional; sft_ty = hint env sfi_hint } and hint_ p env = function | Hany -> Tany diff --git a/hphp/hack/src/decl/decl_instantiate.ml b/hphp/hack/src/decl/decl_instantiate.ml index f410580978103..18ab32ffb1724 100644 --- a/hphp/hack/src/decl/decl_instantiate.ml +++ b/hphp/hack/src/decl/decl_instantiate.ml @@ -100,7 +100,7 @@ and instantiate_ subst x = let tyl = List.map tyl (instantiate subst) in Tapply (x, tyl) | Tshape (fields_known, fdm) -> - let fdm = Nast.ShapeMap.map (instantiate subst) fdm in + let fdm = ShapeFieldMap.map (instantiate subst) fdm in Tshape (fields_known, fdm) let instantiate_ce subst ({ ce_type = x; _ } as ce) = diff --git a/hphp/hack/src/decl/decl_pos_utils.ml b/hphp/hack/src/decl/decl_pos_utils.ml index 83e72d94e4401..aa7692b486d1b 100644 --- a/hphp/hack/src/decl/decl_pos_utils.ml +++ b/hphp/hack/src/decl/decl_pos_utils.ml @@ -1,12 +1,12 @@ (** - * Copyright (c) 2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the "hack" directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - *) +* Copyright (c) 2015, Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the "hack" directory of this source tree. An additional grant +* of patent rights can be found in the PATENTS file in the same directory. +* +*) open Core open Decl_defs @@ -16,74 +16,74 @@ module ShapeMap = Nast.ShapeMap (*****************************************************************************) (* Functor traversing a type, but applies a user defined function for - * positions. - *) +* positions. +*) (*****************************************************************************) module TraversePos(ImplementPos: sig val pos: Pos.t -> Pos.t end) = struct - open Typing_reason - - let pos = ImplementPos.pos - - let rec reason = function - | Rnone -> Rnone - | Rwitness p -> Rwitness (pos p) - | Ridx (p, r) -> Ridx (pos p, reason r) - | Ridx_vector p -> Ridx_vector (pos p) - | Rappend p -> Rappend (pos p) - | Rfield p -> Rfield (pos p) - | Rforeach p -> Rforeach (pos p) - | Rasyncforeach p -> Rasyncforeach (pos p) - | Raccess p -> Raccess (pos p) - | Rarith p -> Rarith (pos p) - | Rarith_ret p -> Rarith_ret (pos p) - | Rarray_plus_ret p -> Rarray_plus_ret (pos p) - | Rstring2 p -> Rstring2 (pos p) - | Rcomp p -> Rcomp (pos p) - | Rconcat p -> Rconcat (pos p) - | Rconcat_ret p -> Rconcat_ret (pos p) - | Rlogic p -> Rlogic (pos p) - | Rlogic_ret p -> Rlogic_ret (pos p) - | Rbitwise p -> Rbitwise (pos p) - | Rbitwise_ret p -> Rbitwise_ret (pos p) - | Rstmt p -> Rstmt (pos p) - | Rno_return p -> Rno_return (pos p) - | Rno_return_async p -> Rno_return_async (pos p) - | Rret_fun_kind (p, k) -> Rret_fun_kind (pos p, k) - | Rhint p -> Rhint (pos p) - | Rnull_check p -> Rnull_check (pos p) - | Rnot_in_cstr p -> Rnot_in_cstr (pos p) - | Rthrow p -> Rthrow (pos p) - | Rplaceholder p -> Rplaceholder (pos p) - | Rattr p -> Rattr (pos p) - | Rxhp p -> Rxhp (pos p) - | Rret_div p -> Rret_div (pos p) - | Ryield_gen p -> Ryield_gen (pos p) - | Ryield_asyncgen p -> Ryield_asyncgen (pos p) - | Ryield_asyncnull p -> Ryield_asyncnull (pos p) - | Ryield_send p -> Ryield_send (pos p) - | Rlost_info (s, r1, p2) -> Rlost_info (s, reason r1, pos p2) - | Rcoerced (p1, p2, x) -> Rcoerced (pos p1, pos p2, x) - | Rformat (p1, s, r) -> Rformat (pos p1, s, reason r) - | Rclass_class (p, s) -> Rclass_class (pos p, s) - | Runknown_class p -> Runknown_class (pos p) - | Rdynamic_yield (p1, p2, s1, s2) -> Rdynamic_yield(pos p1, pos p2, s1, s2) - | Rmap_append p -> Rmap_append (pos p) - | Rvar_param p -> Rvar_param (pos p) - | Runpack_param p -> Runpack_param (pos p) - | Rinstantiate (r1,x,r2) -> Rinstantiate (reason r1, x, reason r2) - | Rarray_filter (p, r) -> Rarray_filter (pos p, reason r) - | Rtype_access (r1, x, r2) -> Rtype_access (reason r1, x, reason r2) - | Rexpr_dep_type (r, p, n) -> Rexpr_dep_type (reason r, pos p, n) - | Rnullsafe_op p -> Rnullsafe_op (pos p) - | Rtconst_no_cstr (p, s) -> Rtconst_no_cstr (pos p, s) - | Rused_as_map p -> Rused_as_map (pos p) - | Rused_as_shape p -> Rused_as_shape (pos p) - | Rpredicated (p, f) -> Rpredicated (pos p, f) - | Rinstanceof (p, f) -> Rinstanceof (pos p, f) - let string_id (p, x) = pos p, x - - let rec ty (p, x) = - reason p, ty_ x +open Typing_reason + +let pos = ImplementPos.pos + +let rec reason = function + | Rnone -> Rnone + | Rwitness p -> Rwitness (pos p) + | Ridx (p, r) -> Ridx (pos p, reason r) + | Ridx_vector p -> Ridx_vector (pos p) + | Rappend p -> Rappend (pos p) + | Rfield p -> Rfield (pos p) + | Rforeach p -> Rforeach (pos p) + | Rasyncforeach p -> Rasyncforeach (pos p) + | Raccess p -> Raccess (pos p) + | Rarith p -> Rarith (pos p) + | Rarith_ret p -> Rarith_ret (pos p) + | Rarray_plus_ret p -> Rarray_plus_ret (pos p) + | Rstring2 p -> Rstring2 (pos p) + | Rcomp p -> Rcomp (pos p) + | Rconcat p -> Rconcat (pos p) + | Rconcat_ret p -> Rconcat_ret (pos p) + | Rlogic p -> Rlogic (pos p) + | Rlogic_ret p -> Rlogic_ret (pos p) + | Rbitwise p -> Rbitwise (pos p) + | Rbitwise_ret p -> Rbitwise_ret (pos p) + | Rstmt p -> Rstmt (pos p) + | Rno_return p -> Rno_return (pos p) + | Rno_return_async p -> Rno_return_async (pos p) + | Rret_fun_kind (p, k) -> Rret_fun_kind (pos p, k) + | Rhint p -> Rhint (pos p) + | Rnull_check p -> Rnull_check (pos p) + | Rnot_in_cstr p -> Rnot_in_cstr (pos p) + | Rthrow p -> Rthrow (pos p) + | Rplaceholder p -> Rplaceholder (pos p) + | Rattr p -> Rattr (pos p) + | Rxhp p -> Rxhp (pos p) + | Rret_div p -> Rret_div (pos p) + | Ryield_gen p -> Ryield_gen (pos p) + | Ryield_asyncgen p -> Ryield_asyncgen (pos p) + | Ryield_asyncnull p -> Ryield_asyncnull (pos p) + | Ryield_send p -> Ryield_send (pos p) + | Rlost_info (s, r1, p2) -> Rlost_info (s, reason r1, pos p2) + | Rcoerced (p1, p2, x) -> Rcoerced (pos p1, pos p2, x) + | Rformat (p1, s, r) -> Rformat (pos p1, s, reason r) + | Rclass_class (p, s) -> Rclass_class (pos p, s) + | Runknown_class p -> Runknown_class (pos p) + | Rdynamic_yield (p1, p2, s1, s2) -> Rdynamic_yield(pos p1, pos p2, s1, s2) + | Rmap_append p -> Rmap_append (pos p) + | Rvar_param p -> Rvar_param (pos p) + | Runpack_param p -> Runpack_param (pos p) + | Rinstantiate (r1,x,r2) -> Rinstantiate (reason r1, x, reason r2) + | Rarray_filter (p, r) -> Rarray_filter (pos p, reason r) + | Rtype_access (r1, x, r2) -> Rtype_access (reason r1, x, reason r2) + | Rexpr_dep_type (r, p, n) -> Rexpr_dep_type (reason r, pos p, n) + | Rnullsafe_op p -> Rnullsafe_op (pos p) + | Rtconst_no_cstr (p, s) -> Rtconst_no_cstr (pos p, s) + | Rused_as_map p -> Rused_as_map (pos p) + | Rused_as_shape p -> Rused_as_shape (pos p) + | Rpredicated (p, f) -> Rpredicated (pos p, f) + | Rinstanceof (p, f) -> Rinstanceof (pos p, f) +let string_id (p, x) = pos p, x + +let rec ty (p, x) = + reason p, ty_ x and ty_: decl ty_ -> decl ty_ = function | Tany @@ -103,7 +103,7 @@ module TraversePos(ImplementPos: sig val pos: Pos.t -> Pos.t end) = struct Taccess (ty root_ty, List.map ids string_id) | Tshape (fields_known, fdm) -> Tshape (shape_fields_known fields_known, - ShapeMap.map_and_rekey fdm shape_field_name ty) + ShapeFieldMap.map_and_rekey fdm shape_field_name ty) and ty_opt x = Option.map x ty diff --git a/hphp/hack/src/typing/type_mapper.ml b/hphp/hack/src/typing/type_mapper.ml index 16971ca6e43d0..f5bbef361a023 100644 --- a/hphp/hack/src/typing/type_mapper.ml +++ b/hphp/hack/src/typing/type_mapper.ml @@ -49,7 +49,11 @@ class type type_mapper_type = object method on_tclass : env -> Reason.t -> Nast.sid -> locl ty list -> result method on_tobject : env -> Reason.t -> result method on_tshape : - env -> Reason.t -> shape_fields_known -> locl ty Nast.ShapeMap.t -> result + env + -> Reason.t + -> shape_fields_known + -> locl shape_field_type Nast.ShapeMap.t + -> result method on_type : env -> locl ty -> result end @@ -179,7 +183,7 @@ class deep_type_mapper = object(this) let env, tyl = List.map_env env tyl this#on_type in env, (r, Tclass (x, tyl)) method! on_tshape env r fields_known fdm = - let env, fdm = Nast.ShapeMap.map_env this#on_type env fdm in + let env, fdm = ShapeFieldMap.map_env this#on_type env fdm in env, (r, Tshape (fields_known, fdm)) method private on_opt_type env x = match x with diff --git a/hphp/hack/src/typing/type_visitor.ml b/hphp/hack/src/typing/type_visitor.ml index 59093d3bb7336..9dffb9dbf77df 100644 --- a/hphp/hack/src/typing/type_visitor.ml +++ b/hphp/hack/src/typing/type_visitor.ml @@ -31,7 +31,11 @@ class type ['a] type_visitor_type = object method on_tunresolved : 'a -> Reason.t -> locl ty list -> 'a method on_tobject : 'a -> Reason.t -> 'a method on_tshape : - 'a -> Reason.t -> shape_fields_known -> 'b ty Nast.ShapeMap.t -> 'a + 'a + -> Reason.t + -> shape_fields_known + -> 'b shape_field_type Nast.ShapeMap.t + -> 'a method on_taccess : 'a -> Reason.t -> taccess_type -> 'a method on_tclass : 'a -> Reason.t -> Nast.sid -> locl ty list -> 'a method on_tarraykind : 'a -> Reason.t -> array_kind -> 'a @@ -77,9 +81,9 @@ class virtual ['a] type_visitor : ['a] type_visitor_type = object(this) method on_tunresolved acc _ tyl = List.fold_left tyl ~f:this#on_type ~init:acc method on_tobject acc _ = acc method on_tshape: type a. _ -> Reason.t -> shape_fields_known - -> a ty Nast.ShapeMap.t -> _ = + -> a shape_field_type Nast.ShapeMap.t -> _ = fun acc _ _ fdm -> - let f _ v acc = this#on_type acc v in + let f _ { sft_ty; _ } acc = this#on_type acc sft_ty in Nast.ShapeMap.fold f fdm acc method on_tclass acc _ _ tyl = List.fold_left tyl ~f:this#on_type ~init:acc diff --git a/hphp/hack/src/typing/typing.ml b/hphp/hack/src/typing/typing.ml index cf37b9919b1b2..f772df07c6dd8 100644 --- a/hphp/hack/src/typing/typing.ml +++ b/hphp/hack/src/typing/typing.ml @@ -163,7 +163,7 @@ let rec check_memoizable env param ty = | _, Tshape (_, fdm) -> ShapeMap.iter begin fun name _ -> match ShapeMap.get name fdm with - | Some ty -> check_memoizable env param ty + | Some { sft_ty; _ } -> check_memoizable env param sft_ty | None -> let ty_str = Typing_print.error (snd ty) in let msgl = Reason.to_string ("This is "^ty_str) (fst ty) in @@ -1621,9 +1621,12 @@ and expr_ (fun env e -> let env, te, ty = expr env e in env, (te,ty)) env fdm in let env, fdm = - ShapeMap.map_env - (fun env (_,ty) -> TUtils.unresolved env ty) - env tfdm in + let convert_expr_and_type_to_shape_field_type env (_, ty) = + let env, sft_ty = TUtils.unresolved env ty in + (* An expression evaluation always corresponds to a shape_field_type + with sft_optional = false. *) + env, { sft_optional = false; sft_ty } in + ShapeMap.map_env convert_expr_and_type_to_shape_field_type env tfdm in let env = check_shape_keys_validity env p (ShapeMap.keys fdm) in (* Fields are fully known, because this shape is constructed * using shape keyword and we know exactly what fields are set. *) @@ -2962,7 +2965,7 @@ and array_get is_lvalue p env ty1 e2 ty2 = Errors.undefined_field p (TUtils.get_printable_shape_field_name field); env, (Reason.Rwitness p, Terr) - | Some ty -> env, ty) + | Some { sft_ty; _ } -> env, sft_ty) ) | Toption _ -> Errors.null_container p diff --git a/hphp/hack/src/typing/typing_arrays.ml b/hphp/hack/src/typing/typing_arrays.ml index 37c3b056d3f18..c849c2d272a7b 100644 --- a/hphp/hack/src/typing/typing_arrays.ml +++ b/hphp/hack/src/typing/typing_arrays.ml @@ -203,7 +203,13 @@ let update_array_type p access_type ~lvar_assignment env ty = method! on_tshape env r fields_known fdm = match access_type with | AKshape_key field_name when lvar_assignment -> - let env, tv = Env.fresh_unresolved_type env in + let env, sft_ty = Env.fresh_unresolved_type env in + (* When we assign to a shape, like: + * + * $shape['field'] = // some type + * + * We want to infer the shape field as non-optional. *) + let tv = { sft_optional = false; sft_ty } in let fdm = ShapeMap.add field_name tv fdm in env, (Reason.Rwitness p, Tshape (fields_known, fdm)) | _ -> diff --git a/hphp/hack/src/typing/typing_defs.ml b/hphp/hack/src/typing/typing_defs.ml index a8b139a078da1..ebf614834efc2 100644 --- a/hphp/hack/src/typing/typing_defs.ml +++ b/hphp/hack/src/typing/typing_defs.ml @@ -30,6 +30,20 @@ type decl = private DeclPhase type locl = private LoclPhase type 'phase ty = Reason.t * 'phase ty_ + +(* A shape may specify whether or not fields are required. For example, consider + this typedef: + + type ShapeWithOptionalField = shape(?'a' => ?int); + + With this definition, the field 'a' may be unprovided in a shape. In this + case, the field 'a' would have sf_optional set to true. + *) +and 'phase shape_field_type = { + sft_optional : bool; + sft_ty : 'phase ty; +} + and _ ty_ = (*========== Following Types Exist Only in the Declared Phase ==========*) (* The late static bound type of a class *) @@ -108,7 +122,9 @@ and _ ty_ = (* Whether all fields of this shape are known, types of each of the * known arms. *) - | Tshape : shape_fields_known * ('phase ty Nast.ShapeMap.t) -> 'phase ty_ + | Tshape + : shape_fields_known * ('phase shape_field_type Nast.ShapeMap.t) + -> 'phase ty_ (*========== Below Are Types That Cannot Be Declared In User Code ==========*) @@ -493,6 +509,43 @@ module AbstractKind = struct String.concat "::" (dt::ids) end +module ShapeFieldMap = struct + include Nast.ShapeMap + + let map_and_rekey shape_map key_f value_f = + let f_over_shape_field_type ({ sft_ty; _ } as shape_field_type) = + { shape_field_type with sft_ty = value_f sft_ty } in + Nast.ShapeMap.map_and_rekey + shape_map + key_f + f_over_shape_field_type + + let map_env f env shape_map = + let f_over_shape_field_type env ({ sft_ty; _ } as shape_field_type) = + let env, sft_ty = f env sft_ty in + env, { shape_field_type with sft_ty } in + Nast.ShapeMap.map_env f_over_shape_field_type env shape_map + + let map f shape_map = map_and_rekey shape_map (fun x -> x) f + + let iter f shape_map = + let f_over_shape_field_type shape_map_key { sft_ty; _ } = + f shape_map_key sft_ty in + Nast.ShapeMap.iter f_over_shape_field_type shape_map + + let iter_values f = iter (fun _ -> f) +end + +module ShapeFieldList = struct + include Core.List + + let map_env env xs ~f = + let f_over_shape_field_type env ({ sft_ty; _ } as shape_field_type) = + let env, sft_ty = f env sft_ty in + env, { shape_field_type with sft_ty } in + Core.List.map_env env xs ~f:f_over_shape_field_type +end + (*****************************************************************************) (* Suggest mode *) (*****************************************************************************) diff --git a/hphp/hack/src/typing/typing_generic.ml b/hphp/hack/src/typing/typing_generic.ml index 300d3c6014e5c..36fe9d325f2f5 100644 --- a/hphp/hack/src/typing/typing_generic.ml +++ b/hphp/hack/src/typing/typing_generic.ml @@ -60,7 +60,7 @@ end = struct | Tunresolved tyl -> List.iter tyl ty | Tobject -> () | Tshape (_, fdm) -> - ShapeMap.iter (fun _ v -> ty v) fdm + ShapeFieldMap.iter (fun _ v -> ty v) fdm and ty_opt = function None -> () | Some x -> ty x diff --git a/hphp/hack/src/typing/typing_phase.ml b/hphp/hack/src/typing/typing_phase.ml index 3cc1ab92b40de..f03cde649430c 100644 --- a/hphp/hack/src/typing/typing_phase.ml +++ b/hphp/hack/src/typing/typing_phase.ml @@ -173,7 +173,7 @@ let rec localize_with_env ~ety_env env (dty: decl ty) = let env, root_ty = localize ~ety_env env root_ty in TUtils.expand_typeconst ety_env env r root_ty ids | r, Tshape (fields_known, tym) -> - let env, tym = ShapeMap.map_env (localize ~ety_env) env tym in + let env, tym = ShapeFieldMap.map_env (localize ~ety_env) env tym in env, (ety_env, (r, Tshape (fields_known, tym))) and localize ~ety_env env ty = diff --git a/hphp/hack/src/typing/typing_print.ml b/hphp/hack/src/typing/typing_print.ml index a87274d33a3a3..fbd9f9c3d4f40 100644 --- a/hphp/hack/src/typing/typing_print.ml +++ b/hphp/hack/src/typing/typing_print.ml @@ -245,13 +245,10 @@ module Full = struct | [x] -> f x | x :: rl -> f x; o s; list_sep o s f rl - let shape_map o fdm f = + let shape_map o fdm o_field = let cmp = (fun (k1, _) (k2, _) -> compare (Env.get_shape_field_name k1) (Env.get_shape_field_name k2)) in let fields = List.sort ~cmp (Nast.ShapeMap.elements fdm) in - let o_field = (fun (k, v) -> - o (Env.get_shape_field_name k); o " => "; f v;) - in (match fields with | [] -> () | f::l -> @@ -283,8 +280,14 @@ module Full = struct | Tarray (Some x, Some y) -> o "array<"; k x; o ", "; k y; o ">" | Tarraykind AKdarray (x, y) -> o "darray<"; k x; o ", "; k y; o ">" | Tarraykind (AKmap (x, y)) -> o "array<"; k x; o ", "; k y; o ">" - | Tarraykind (AKshape fdm) -> o "shape-like-array("; - shape_map o fdm (fun (_tk, tv) -> k tv); o ")" + | Tarraykind (AKshape fdm) -> + let o_field (shape_map_key, (_tk, tv)) = + o (Env.get_shape_field_name shape_map_key); + o " => "; + k tv in + o "shape-like-array("; + shape_map o fdm o_field; + o ")" | Tarraykind (AKtuple fields) -> o "tuple-like-array("; list k (List.rev (IMap.values fields)); o ")" | Tarray (None, Some _) -> assert false @@ -342,7 +345,27 @@ module Full = struct end end; o "("; - shape_map o fdm (fun t -> k t); + let optional_shape_field_enabled = + TypecheckerOptions.experimental_feature_enabled + (Env.get_options env) + TypecheckerOptions.experimental_optional_shape_field in + let o_field (shape_map_key, { sft_optional; sft_ty }) = + if optional_shape_field_enabled then + begin + o (Env.get_shape_field_name shape_map_key); + o " => shape("; + if sft_optional then o "optional => true, "; + o "type => "; + k sft_ty; + o ")" + end + else + begin + o (Env.get_shape_field_name shape_map_key); + o " => "; + k sft_ty + end in + shape_map o fdm o_field; o ")" and prim o x = diff --git a/hphp/hack/src/typing/typing_shapes.ml b/hphp/hack/src/typing/typing_shapes.ml index 74bb43a31cb1e..afbc6f7f92d2d 100644 --- a/hphp/hack/src/typing/typing_shapes.ml +++ b/hphp/hack/src/typing/typing_shapes.ml @@ -49,13 +49,22 @@ let idx env fty shape_ty field default = match TUtils.shape_field_name env (fst field) (snd field) with | None -> env, (Reason.Rwitness (fst field), Tany) | Some field_name -> + let optional_shape_field_enabled = + TypecheckerOptions.experimental_feature_enabled + (Env.get_options env) + TypecheckerOptions.experimental_optional_shape_field in + let fake_shape_field = + if optional_shape_field_enabled then + { sft_optional = true; sft_ty = res } + else + { sft_optional = false; sft_ty = (Reason.Rnone, Toption res) } in let fake_shape = ( (* Rnone because we don't want the fake shape to show up in messages about * field non existing. Errors.missing_optional_field filters them out *) Reason.Rnone, Tshape ( FieldsPartiallyKnown Nast.ShapeMap.empty, - Nast.ShapeMap.singleton field_name (Reason.Rnone, Toption res) + Nast.ShapeMap.singleton field_name fake_shape_field ) ) in let env = @@ -85,7 +94,8 @@ let to_array env shape_ty res = match fields_known with | FieldsFullyKnown -> let env, values = - List.map_env env (ShapeMap.values fdm) (Typing_utils.unresolved) in + ShapeFieldList.map_env + env (ShapeMap.values fdm) (Typing_utils.unresolved) in let keys = ShapeMap.keys fdm in let env, keys = List.map_env env keys begin fun env key -> let env, ty = match key with @@ -103,6 +113,7 @@ let to_array env shape_ty res = end in let env, key = Typing_arrays.array_type_list_to_single_type env keys in + let values = List.map ~f:(fun { sft_ty; _ } -> sft_ty) values in let env, value = Typing_arrays.array_type_list_to_single_type env values in env, (r, Tarraykind (AKmap (key, value))) diff --git a/hphp/hack/src/typing/typing_structure.ml b/hphp/hack/src/typing/typing_structure.ml index cb7893776e5f7..4eb9512594c20 100644 --- a/hphp/hack/src/typing/typing_structure.ml +++ b/hphp/hack/src/typing/typing_structure.ml @@ -55,29 +55,36 @@ let rec transform_shapemap ?(nullable = false) env ty shape = if is_unbound then (env, shape) else let is_generic = match snd ety with Tabstract (AKgeneric _, _) -> true | _ -> false in - ShapeMap.fold begin fun field field_ty (env, shape) -> + let transform_shape_field field { sft_ty; _ } (env, shape) = let open Ast in - match field, field_ty, TUtils.get_base_type env ety with + + (* Accumulates the provided type for this iteration of the fold, adding + it to the accumulation ShapeMap for the current field. Since the + field must have been explicitly set, we set sft_optional to true. *) + let acc_field_with_type sft_ty = + ShapeMap.add field { sft_optional = false; sft_ty } shape in + + match field, sft_ty, TUtils.get_base_type env ety with | SFlit (_, "nullable"), (_, Toption (fty)), _ when nullable -> - env, ShapeMap.add field fty shape + env, acc_field_with_type fty | SFlit (_, "nullable"), (_, Toption (fty)), (_, Toption _) -> - env, ShapeMap.add field fty shape + env, acc_field_with_type fty | SFlit (_, "classname"), (_, Toption (fty)), (_, (Tclass _ | Tabstract (AKenum _, _))) -> - env, ShapeMap.add field fty shape + env, acc_field_with_type fty | SFlit (_, "elem_types"), _, (r, Ttuple tyl) -> let env, tyl = List.map_env env tyl make_ts in - env, ShapeMap.add field (r, Ttuple tyl) shape + env, acc_field_with_type (r, Ttuple tyl) | SFlit (_, "param_types"), _, (r, (Tfun funty)) -> let tyl = List.map ~f:snd funty.ft_params in let env, tyl = List.map_env env tyl make_ts in - env, ShapeMap.add field (r, Ttuple tyl) shape + env, acc_field_with_type (r, Ttuple tyl) | SFlit (_, "return_type"), _, (r, Tfun funty) -> let env, ty = make_ts env funty.ft_ret in - env, ShapeMap.add field (r, Ttuple [ty]) shape + env, acc_field_with_type (r, Ttuple [ty]) | SFlit (_, "fields"), _, (r, Tshape (fk, fields)) -> - let env, fields = ShapeMap.map_env make_ts env fields in - env, ShapeMap.add field (r, Tshape (fk, fields)) shape + let env, fields = ShapeFieldMap.map_env make_ts env fields in + env, acc_field_with_type (r, Tshape (fk, fields)) (* For generics we cannot specialize the generic_types field. Consider: * * class C {} @@ -90,23 +97,23 @@ let rec transform_shapemap ?(nullable = false) env ty shape = * For test(TypeStructure) there will not be a generic_types field *) | SFlit (_, "generic_types"), _, _ when is_generic -> - env, ShapeMap.add field field_ty shape + env, acc_field_with_type sft_ty | SFlit (_, "generic_types"), _, (r, Tarraykind (AKvec ty)) when not is_generic -> let env, ty = make_ts env ty in - env, ShapeMap.add field (r, Ttuple [ty]) shape + env, acc_field_with_type (r, Ttuple [ty]) | SFlit (_, "generic_types"), _, (r, Tarraykind (AKmap (ty1, ty2))) when not is_generic -> let tyl = [ty1; ty2] in let env, tyl = List.map_env env tyl make_ts in - env, ShapeMap.add field (r, Ttuple tyl) shape + env, acc_field_with_type (r, Ttuple tyl) | SFlit (_, "generic_types"), _, (r, Tclass (_, tyl)) when List.length tyl > 0 -> let env, tyl = List.map_env env tyl make_ts in - env, ShapeMap.add field (r, Ttuple tyl) shape + env, acc_field_with_type (r, Ttuple tyl) | SFlit (_, ("kind" | "name" | "alias")), _, _ -> - env, ShapeMap.add field field_ty shape + env, acc_field_with_type sft_ty | _, _, _ -> - env, shape - end shape (env, ShapeMap.empty) + env, shape in + ShapeMap.fold transform_shape_field shape (env, ShapeMap.empty) diff --git a/hphp/hack/src/typing/typing_subtype.ml b/hphp/hack/src/typing/typing_subtype.ml index d7c79b2c3cbf6..2400e6105fe99 100644 --- a/hphp/hack/src/typing/typing_subtype.ml +++ b/hphp/hack/src/typing/typing_subtype.ml @@ -827,8 +827,27 @@ and sub_type_with_uenv env (uenv_sub, ty_sub) (uenv_super, ty_super) = ) | (r_sub, Tshape (fields_known_sub, fdm_sub)), (r_super, Tshape (fields_known_super, fdm_super)) -> + (** + * shape_field_type A <: shape_field_type B iff: + * 1. A is no more optional than B + * 2. A's type <: B.type + *) + let on_common_field + (env, acc) + name + { sft_optional = optional_super; sft_ty = ty_super } + { sft_optional = optional_sub; sft_ty = ty_sub } = + match optional_super, optional_sub with + | true, _ | false, false -> + sub_type env ty_sub ty_super, acc + | false, true -> + Errors.required_field_is_optional + (Reason.to_pos r_sub) + (Reason.to_pos r_super) + (Env.get_shape_field_name name); + env, acc in fst (TUtils.apply_shape - ~on_common_field:(fun (env, acc) _ x y -> sub_type env y x, acc) + ~on_common_field ~on_missing_optional_field:(fun acc _ _ -> acc) (env, None) (r_super, fields_known_super, fdm_super) diff --git a/hphp/hack/src/typing/typing_unify.ml b/hphp/hack/src/typing/typing_unify.ml index 65eeca2d9e68c..aeeeccc0b9945 100644 --- a/hphp/hack/src/typing/typing_unify.ml +++ b/hphp/hack/src/typing/typing_unify.ml @@ -315,11 +315,33 @@ and unify_ ?follow_bounds:(follow_bounds=true) env r1 ty1 r2 ty2 = | Tobject, Tclass _ | Tclass _, Tobject -> env, Tobject | Tshape (fields_known1, fdm1), Tshape (fields_known2, fdm2) -> - let on_common_field (env, acc) name ty1 ty2 = - let env, ty = unify env ty1 ty2 in - env, Nast.ShapeMap.add name ty acc in - let on_missing_optional_field (env, acc) name ty = - env, Nast.ShapeMap.add name ty acc in + (** + * shape_field_type A and shape_field_type B are unifiable iff: + * 1. A and B have the same optionality + * 2. A's type and B's type are unifiable + *) + let on_common_field + (env, acc) + name + { sft_optional = sft_optional1; sft_ty = ty1 } + { sft_optional = sft_optional2; sft_ty = ty2 } = + if sft_optional1 = sft_optional2 + then + let env, sft_ty = unify env ty1 ty2 in + let common_shape_field_type = { + sft_optional = sft_optional1; + sft_ty; + } in + env, Nast.ShapeMap.add name common_shape_field_type acc + else + let optional, required = if sft_optional1 then r1, r2 else r2, r1 in + Errors.required_field_is_optional + (Reason.to_pos optional) + (Reason.to_pos required) + (Env.get_shape_field_name name); + env, acc in + let on_missing_optional_field (env, acc) name missing_shape_field_type = + env, Nast.ShapeMap.add name missing_shape_field_type acc in (* We do it both directions to verify that no field is missing *) let res = Nast.ShapeMap.empty in let env, res = TUtils.apply_shape diff --git a/hphp/hack/src/typing/typing_unify_recursive.ml b/hphp/hack/src/typing/typing_unify_recursive.ml index bca7f4142d52c..901c30041dd46 100644 --- a/hphp/hack/src/typing/typing_unify_recursive.ml +++ b/hphp/hack/src/typing/typing_unify_recursive.ml @@ -50,7 +50,8 @@ let rec occurs env n rty = | Tabstract(ak,topt) -> occurs_ak env n ak || occurs_opt env n topt | Tarraykind ak -> occurs_array env n ak | Tfun ft -> occurs_ft env n ft - | Tshape(_,sm) -> Nast.ShapeMap.exists (fun _ t -> occurs env n t) sm + | Tshape(_,sm) -> + Nast.ShapeMap.exists (fun _ { sft_ty; _ } -> occurs env n sft_ty) sm and occurs_opt env n topt = match topt with | None -> false diff --git a/hphp/hack/src/typing/typing_utils.ml b/hphp/hack/src/typing/typing_utils.ml index 92bfd1762d5fd..cda03c2bfc4f4 100644 --- a/hphp/hack/src/typing/typing_utils.ml +++ b/hphp/hack/src/typing/typing_utils.ml @@ -57,6 +57,17 @@ let rec is_option env ty = List.exists tyl (is_option env) | _ -> false +let is_shape_field_optional env { sft_optional; sft_ty } = + let optional_shape_field_enabled = + TypecheckerOptions.experimental_feature_enabled + (Env.get_options env) + TypecheckerOptions.experimental_optional_shape_field in + + if optional_shape_field_enabled then + sft_optional + else + is_option env sft_ty + let is_class ty = match snd ty with | Tclass _ -> true | _ -> false @@ -204,15 +215,15 @@ let apply_shape ~on_common_field ~on_missing_optional_field (env, acc) end unset_fields1 | _ -> () end; - ShapeMap.fold begin fun name ty1 (env, acc) -> + ShapeMap.fold begin fun name shape_field_type_1 (env, acc) -> match ShapeMap.get name fdm2 with - | None when is_option env ty1 -> + | None when is_shape_field_optional env shape_field_type_1 -> let can_omit = match fields_known2 with | FieldsFullyKnown -> true | FieldsPartiallyKnown unset_fields -> ShapeMap.mem name unset_fields in if can_omit then - on_missing_optional_field (env, acc) name ty1 + on_missing_optional_field (env, acc) name shape_field_type_1 else let pos1 = Reason.to_pos r1 in let pos2 = Reason.to_pos r2 in @@ -224,8 +235,8 @@ let apply_shape ~on_common_field ~on_missing_optional_field (env, acc) let pos2 = Reason.to_pos r2 in Errors.missing_field pos2 pos1 (get_printable_shape_field_name name); (env, acc) - | Some ty2 -> - on_common_field (env, acc) name ty1 ty2 + | Some shape_field_type_2 -> + on_common_field (env, acc) name shape_field_type_1 shape_field_type_2 end fdm1 (env, acc) let shape_field_name_ env field = diff --git a/hphp/hack/src/typing/typing_variance.ml b/hphp/hack/src/typing/typing_variance.ml index a09a8eefcf0d7..27bb008d7a977 100644 --- a/hphp/hack/src/typing/typing_variance.ml +++ b/hphp/hack/src/typing/typing_variance.ml @@ -526,8 +526,8 @@ and type_ tcopt root variance env (reason, ty) = (* when we add type params to type consts might need to change *) | Taccess _ -> env | Tshape (_, ty_map) -> - Nast.ShapeMap.fold begin fun _field_name ty env -> - type_ tcopt root variance env ty + Nast.ShapeMap.fold begin fun _ { sft_ty; _ } env -> + type_ tcopt root variance env sft_ty end ty_map env (* `as` constraints on method type parameters must be contravariant diff --git a/hphp/hack/src/utils/errors/errors.ml b/hphp/hack/src/utils/errors/errors.ml index 7bf306c993dce..06e20457304a4 100644 --- a/hphp/hack/src/utils/errors/errors.ml +++ b/hphp/hack/src/utils/errors/errors.ml @@ -689,6 +689,7 @@ module Typing = struct let instanceof_always_true = 4160 (* DONT MODIFY!!!! *) let ambiguous_member = 4161 (* DONT MODIFY!!!! *) let instanceof_generic_classname = 4162 (* DONT MODIFY!!!! *) + let required_field_is_optional = 4163 (* DONT MODIFY!!!! *) (* EXTEND HERE WITH NEW VALUES IF NEEDED *) end @@ -2060,6 +2061,13 @@ let instanceof_generic_classname pos name = name ^ "' may be instantiated with a type such as \ 'C' that cannot be checked at runtime") +let required_field_is_optional pos1 pos2 name = + add_list Typing.required_field_is_optional + [ + pos1, "The field '"^name^"' is optional"; + pos2, "The field '"^name^"' is defined as required" + ] + (*****************************************************************************) (* Typing decl errors *) (*****************************************************************************) diff --git a/hphp/hack/src/utils/errors/errors_sig.ml b/hphp/hack/src/utils/errors/errors_sig.ml index de5ea73fed131..39a01ebea1f7f 100644 --- a/hphp/hack/src/utils/errors/errors_sig.ml +++ b/hphp/hack/src/utils/errors/errors_sig.ml @@ -356,4 +356,5 @@ module type S = sig val darray_not_supported : Pos.t -> unit val varray_not_supported : Pos.t -> unit val too_few_type_arguments : Pos.t -> unit + val required_field_is_optional : Pos.t -> Pos.t -> string -> unit end diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php new file mode 100644 index 0000000000000..8d2a7abf64dc2 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php @@ -0,0 +1,14 @@ + int, + 'b' => int, +); + +function foo(ShapeWithOptionalAndNonOptionalFields $argument): void {} + +function bar(): void { + foo(shape()); +} diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php.exp b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php.exp new file mode 100644 index 0000000000000..907caa1d86256 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php.exp @@ -0,0 +1,6 @@ +File "shape_structural_subtyping_with_optional_and_non_optional_types.php", line 13, characters 7-11: +Invalid argument (Typing[4057]) +File "shape_structural_subtyping_with_optional_and_non_optional_types.php", line 13, characters 7-11: +The field 'b' is missing +File "shape_structural_subtyping_with_optional_and_non_optional_types.php", line 10, characters 14-50: +The field 'b' is defined diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php.no_format b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php new file mode 100644 index 0000000000000..2f8e28faa977f --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php @@ -0,0 +1,13 @@ + int, + ?'b' => int, +); + +function foo(ShapeWithOptionalField $argument) : void {} + +function bar() : void { + foo(shape()); +} diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php.exp b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php.exp new file mode 100644 index 0000000000000..4269126fcebc1 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php.exp @@ -0,0 +1 @@ +No errors diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php.no_format b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php new file mode 100644 index 0000000000000..c93ec5b1257c1 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php @@ -0,0 +1,15 @@ + int, + ?'b' => int, +); + +function foo(ShapeWithOptionalField $argument) : void {} + +function bar() : void { + foo(shape('a' => 42, 'c' => "Hello")); +} diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php.exp b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php.exp new file mode 100644 index 0000000000000..4269126fcebc1 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php.exp @@ -0,0 +1 @@ +No errors diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php.no_format b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php b/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php new file mode 100644 index 0000000000000..3afb487ec4842 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php @@ -0,0 +1,10 @@ + int, 'b' => bool); + +function test(TypeStructure $type_structure): void { + hh_show($type_structure['kind']); + hh_show($type_structure['alias']); + hh_show($type_structure['name']); + hh_show($type_structure['fields']); +} diff --git a/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php.exp b/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php.exp new file mode 100644 index 0000000000000..0103b38b0123c --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php.exp @@ -0,0 +1,9 @@ +File "shape_type_structure_with_optional_and_required_fields.php", line 6, characters 3-34: + int +File "shape_type_structure_with_optional_and_required_fields.php", line 7, characters 3-35: + ?string +File "shape_type_structure_with_optional_and_required_fields.php", line 8, characters 3-34: + ?string +File "shape_type_structure_with_optional_and_required_fields.php", line 9, characters 3-36: + shape(unset fields:)(a => shape(optional => true, type => TypeStructure), b => shape(type => TypeStructure)) +No errors diff --git a/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php.no_format b/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php b/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php new file mode 100644 index 0000000000000..48bd05d2b5a1c --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php @@ -0,0 +1,9 @@ + {} + +function take_shape(MyWrapper int)> $x): void {} + +function test(MyWrapper int)> $x): void { + take_shape($x); +} diff --git a/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php.exp b/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php.exp new file mode 100644 index 0000000000000..4269126fcebc1 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php.exp @@ -0,0 +1 @@ +No errors diff --git a/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php.no_format b/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php new file mode 100644 index 0000000000000..2a58c4f56095e --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php @@ -0,0 +1,9 @@ + {} + +function take_shape(MyWrapper int)> $x): void {} + +function test(MyWrapper int)> $x): void { + take_shape($x); +} diff --git a/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php.exp b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php.exp new file mode 100644 index 0000000000000..1841e7f0dcb84 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php.exp @@ -0,0 +1,8 @@ +File "shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php", line 8, characters 14-15: +Invalid argument (Typing[4163]) +File "shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php", line 7, characters 25-29: +The field 'x' is optional +File "shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php", line 5, characters 31-35: +The field 'x' is defined as required +File "shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php", line 5, characters 31-35: +Considering that this type argument is invariant with respect to MyWrapper diff --git a/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php.no_format b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php new file mode 100644 index 0000000000000..f90a459b8c038 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php @@ -0,0 +1,9 @@ + {} + +function take_shape(MyWrapper int)> $x): void {} + +function test(MyWrapper int)> $x): void { + take_shape($x); +} diff --git a/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php.exp b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php.exp new file mode 100644 index 0000000000000..8e8f22b8cbb42 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php.exp @@ -0,0 +1,8 @@ +File "shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php", line 8, characters 14-15: +Invalid argument (Typing[4163]) +File "shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php", line 5, characters 31-35: +The field 'x' is optional +File "shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php", line 7, characters 25-29: +The field 'x' is defined as required +File "shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php", line 5, characters 31-35: +Considering that this type argument is invariant with respect to MyWrapper diff --git a/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php.no_format b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php b/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php new file mode 100644 index 0000000000000..40a499c3901eb --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php @@ -0,0 +1,14 @@ + int); +type Subtype = shape('a' => int); + +function consumesSupertype(Supertype $argument): void {} + +function providesSubtype(): Subtype { + return shape('a' => 0); +} + +function test(): void { + consumesSupertype(providesSubtype()); +} diff --git a/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php.exp b/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php.exp new file mode 100644 index 0000000000000..4269126fcebc1 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php.exp @@ -0,0 +1 @@ +No errors diff --git a/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php.no_format b/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php b/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php new file mode 100644 index 0000000000000..0ff67585bfe97 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php @@ -0,0 +1,14 @@ + int); +type Subtype = shape(?'a' => int); + +function consumesSupertype(Supertype $argument): void {} + +function providesSubtype(): Subtype { + return shape('a' => 0); +} + +function test(): void { + consumesSupertype(providesSubtype()); +} diff --git a/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php.exp b/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php.exp new file mode 100644 index 0000000000000..f325a10e23b97 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php.exp @@ -0,0 +1,6 @@ +File "shape_with_required_supertype_field_and_optional_subtype_field.php", line 13, characters 21-37: +Invalid argument (Typing[4163]) +File "shape_with_required_supertype_field_and_optional_subtype_field.php", line 8, characters 29-35: +The field 'a' is optional +File "shape_with_required_supertype_field_and_optional_subtype_field.php", line 6, characters 28-36: +The field 'a' is defined as required diff --git a/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php.no_format b/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape14.php b/hphp/hack/test/typecheck/shape14.php index 8a08dcb3e8a71..8b6f2a4f5a30f 100644 --- a/hphp/hack/test/typecheck/shape14.php +++ b/hphp/hack/test/typecheck/shape14.php @@ -9,7 +9,7 @@ * */ -type myshape = shape('my_optional_field' => ?int); +type myshape = shape(?'my_optional_field' => int); function test(): myshape { return shape(); diff --git a/hphp/hack/test/typecheck/shape14.php.no_format b/hphp/hack/test/typecheck/shape14.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape24.php b/hphp/hack/test/typecheck/shape24.php index fb529e857d0c9..264172993f4d7 100644 --- a/hphp/hack/test/typecheck/shape24.php +++ b/hphp/hack/test/typecheck/shape24.php @@ -8,7 +8,7 @@ type t = shape( 'x' => int, - 'z' => ?bool, + ?'z' => bool, ); // Error: s declares only 'x', but at runtime it can also have a 'z' that diff --git a/hphp/hack/test/typecheck/shape24.php.no_format b/hphp/hack/test/typecheck/shape24.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape25.php b/hphp/hack/test/typecheck/shape25.php index e237fbe9da81e..a48f1bde33188 100644 --- a/hphp/hack/test/typecheck/shape25.php +++ b/hphp/hack/test/typecheck/shape25.php @@ -5,7 +5,7 @@ */ type t = shape( 'x' => int, - 'z' => ?bool, + ?'z' => bool, ); // No error: we are sure that there is no field 'z' in returned shape diff --git a/hphp/hack/test/typecheck/shape25.php.no_format b/hphp/hack/test/typecheck/shape25.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape26.php.exp b/hphp/hack/test/typecheck/shape26.php.exp index 3171cd9e5afc7..94ac8c7aeadf4 100644 --- a/hphp/hack/test/typecheck/shape26.php.exp +++ b/hphp/hack/test/typecheck/shape26.php.exp @@ -1,9 +1,9 @@ File "shape26.php", line 21, characters 3-13: - ^(shape(x => ^(int)) | shape(y => ^(string))) + ^(shape(x => shape(type => ^(int))) | shape(y => shape(type => ^(string)))) File "shape26.php", line 29, characters 3-13: - ^(shape(x => ^(string | int))) + ^(shape(x => shape(type => ^(string | int)))) File "shape26.php", line 37, characters 3-13: - ^(shape(unset fields:)(x => int) | shape(unset fields:)(x => int, y => ?string)) + ^(shape(unset fields:)(x => shape(type => int)) | shape(unset fields:)(x => shape(type => int), y => shape(type => ?string))) File "shape26.php", line 45, characters 3-13: - ^(shape(x => ^(int)) | shape(unset fields:)(x => int, y => ?string)) + ^(shape(x => shape(type => ^(int))) | shape(unset fields:)(x => shape(type => int), y => shape(type => ?string))) No errors diff --git a/hphp/hack/test/typecheck/shape27.php b/hphp/hack/test/typecheck/shape27.php index 166ebce58930f..fa6bde916e9c5 100644 --- a/hphp/hack/test/typecheck/shape27.php +++ b/hphp/hack/test/typecheck/shape27.php @@ -4,7 +4,7 @@ * Accessing fields/passing shapes to functions * doesn't change the "known fields" property */ -type s = shape('z' => ?int); +type s = shape(?'z' => int); function test(): s { $s = shape(); @@ -12,7 +12,7 @@ function test(): s { return $s; } -type t = shape('x' => ?int); +type t = shape(?'x' => int); function f(t $_): void {} diff --git a/hphp/hack/test/typecheck/shape27.php.no_format b/hphp/hack/test/typecheck/shape27.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape29.php.exp b/hphp/hack/test/typecheck/shape29.php.exp index 7b7d6e6ef12ec..2e50d17b3affe 100644 --- a/hphp/hack/test/typecheck/shape29.php.exp +++ b/hphp/hack/test/typecheck/shape29.php.exp @@ -1,5 +1,5 @@ File "shape29.php", line 6, characters 3-13: - shape(bar => ^(bool), baz => ^(string), foo => ^(int), xyz => ^(float)) + shape(bar => shape(type => ^(bool)), baz => shape(type => ^(string)), foo => shape(type => ^(int)), xyz => shape(type => ^(float))) File "shape29.php", line 7, characters 3-13: - shape(bar => ^(bool), baz => ^(string), foo => ^(int), xyz => ^(float)) + shape(bar => shape(type => ^(bool)), baz => shape(type => ^(string)), foo => shape(type => ^(int)), xyz => shape(type => ^(float))) No errors diff --git a/hphp/hack/test/typecheck/shape_idx3.php b/hphp/hack/test/typecheck/shape_idx3.php index 4090ab791cec5..92fefa0cb92f3 100644 --- a/hphp/hack/test/typecheck/shape_idx3.php +++ b/hphp/hack/test/typecheck/shape_idx3.php @@ -1,7 +1,7 @@ ?arraykey); -type my_shapeB = shape('x' => ?int); +type my_shapeA = shape(?'x' => arraykey); +type my_shapeB = shape(?'x' => int); function test( my_shapeA $s1, diff --git a/hphp/hack/test/typecheck/shape_idx3.php.no_format b/hphp/hack/test/typecheck/shape_idx3.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape_modified6.php b/hphp/hack/test/typecheck/shape_modified6.php index 80be7817b5481..1b6a59dbf8dd0 100644 --- a/hphp/hack/test/typecheck/shape_modified6.php +++ b/hphp/hack/test/typecheck/shape_modified6.php @@ -11,7 +11,7 @@ type my_shape = shape( 'x' => int, - 'y' => ?bool, + ?'y' => bool, ); function foo(bool $cond): my_shape { diff --git a/hphp/hack/test/typecheck/shape_modified6.php.no_format b/hphp/hack/test/typecheck/shape_modified6.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape_remove_key6.php b/hphp/hack/test/typecheck/shape_remove_key6.php index 7fdda6a9581a3..9df2016a0f7b5 100644 --- a/hphp/hack/test/typecheck/shape_remove_key6.php +++ b/hphp/hack/test/typecheck/shape_remove_key6.php @@ -8,7 +8,7 @@ type t = shape( 'x' => int, - 'z' => ?bool, + ?'z' => bool, ); function test(s $s): t { diff --git a/hphp/hack/test/typecheck/shape_remove_key6.php.no_format b/hphp/hack/test/typecheck/shape_remove_key6.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/type_structure/generic_type_structure.php.exp b/hphp/hack/test/typecheck/type_structure/generic_type_structure.php.exp index f7553ba7c9ef1..84462f5e18e4f 100644 --- a/hphp/hack/test/typecheck/type_structure/generic_type_structure.php.exp +++ b/hphp/hack/test/typecheck/type_structure/generic_type_structure.php.exp @@ -1,7 +1,7 @@ File "generic_type_structure.php", line 23, characters 3-15: TypeStructure<^(int)> File "generic_type_structure.php", line 24, characters 3-15: - TypeStructure<^(shape(unset fields:)(x => ?int, y => (string, bool)))> + TypeStructure<^(shape(unset fields:)(x => shape(type => ?int), y => shape(type => (string, bool))))> File "generic_type_structure.php", line 25, characters 3-36: (TypeStructure<^bool>, TypeStructure<^string>) No errors diff --git a/hphp/hack/test/typecheck/type_structure/type_structure1.php.exp b/hphp/hack/test/typecheck/type_structure/type_structure1.php.exp index cefe5e14a0eb8..42ec982e27fb2 100644 --- a/hphp/hack/test/typecheck/type_structure/type_structure1.php.exp +++ b/hphp/hack/test/typecheck/type_structure/type_structure1.php.exp @@ -23,7 +23,7 @@ File "type_structure1.php", line 38, characters 5-48: File "type_structure1.php", line 39, characters 5-68: classname> File "type_structure1.php", line 42, characters 5-17: - shape(unset fields:)(bar => TypeStructure (num, num))>, foo => TypeStructure) + shape(unset fields:)(bar => shape(type => TypeStructure shape(type => (num, num)))>), foo => shape(type => TypeStructure)) File "type_structure1.php", line 43, characters 5-24: TypeStructure File "type_structure1.php", line 44, characters 5-56: