Skip to content

Commit

Permalink
Use subtyping to handle splat destructuring
Browse files Browse the repository at this point in the history
Summary:
This diff changes splat (...) unpacking to use Tdestructure, and extends Tdestructure to have 3 main boxes for unpacking. There are two cases to explore:

# Tuple destructuring

Now, `Tdestructure` for splat (henceforth as "splat(...)") has three separate boxes for types. For example, the following example

```
function take(bool $b, float $f = 3.14, arraykey ...$aks): void {}
function f((bool, float, int, string) $tup): void {
  take(...$tup);
}
```
corresponds to the subtyping assertion
```
(bool, float, int, string) <: splat([#1], [opt#2], ...#3)
```
and results in
```
bool <: #1
float <: #2
arraykey <: #3
```

First, it is required that the tuple have enough elements to fill the required values -- in this case just `[#1]`. Then, if there are remaining elements, they are first used to fill the optional elements `[#2]`. Finally, the remainder are collected in the variadic element `#3`. If tuple elements remain but there is no variadic element, then we emit an error.

# Array destructuring

Previously, the typechecker would allow nonsense like
```
function f(int $i = 3): void {}
function g(int $i, int ...$j): void {}

function passes_typechecker(): void {
  f(...vec[1,2]); // throws
  f(...vec[]); // throws
}
```
Now, we require that splat operations with arrays have a variadic component to accept the values. We additionally ban unpacking into the required arguments of a function because the array could be empty. Unpacking into optionals is OK. Basically, we require that the function signature can accept the entire array.

**Note:** we still allow partial array destructuring with *list destructuring*, so
```
list($a, $b) = vec[1,2,3,4];
```
is still valid and is represented as
```
vec<int> <: list(#1, #2)
```

# Typing Rules

In these rules `r` is a type of a required field, `o` is a type of an optional field, `v` is the type of the variadic field.

#  destruct_tuple_list
```
forall i in 1...n

             t_i <: r_i
--------------------------------------
(t_1, ..., t_n) <: list(r_1, ..., r_n)
```
**Example**
```
function test_runner((int, string, bool) $tup): void {
  list($a, $b, $c) = $tup;
}
```

# destruct_tuple_splat
```
forall i in 1...m
forall j in (if p < n then m+1...p else m+1...n)
forall k in p+1...n

     m <= n    t_i <: r_i    t_j <: o_j    t_k <: v
-----------------------------------------------------------
(t_1, ..., t_n) <: splat(r_1, ..., r_m, o_m+1, ..., o_p, v)
```
**Example**
```
function take(int $i, string $j, bool $kopt = false, float $fopt = 3.14, mixed ...$rest): void {}

function test_runner((int, string) $tup): void { take(...$tup); }
function test_runner2((int, string, bool) $tup): void { take(...$tup); }
function test_runner3((int, string, bool, float) $tup): void { take(...$tup); }
function test_runner4((int, string, bool, float, null, int, mixed) $tup): void { take(...$tup); }
```

# destruct_array_list

This rule already exists, and it allows for operations that can throw.
```
      forall i in 1...n   t <: r_i
-------------------------------------------
        vec<t> <: list(r_1, ... r_n)
     Vector<t> <:
  ImmVector<t> <:
ConstVector<t> <:
```
**Example**
```
list($a, $b, $c) = vec[3]; // legal, but will throw
list($a) = vec[2,3,5]; // legal even though incomplete
```

# destructure_array_splat

Note the lack of required field in this row, and the presence of the variadic.

```
 forall i in 1..n   t <: o_i    t <: v
----------------------------------------
Traversable<t> <: splat(o_1, ... o_n, v)
```

**Example**
```
function take1(int $i = 3, int ...$is): void {}
function take2(int ...$is): void {}

function test(vec<int> $v): void {
   take1(...$v);
   take2(...$v);
}
```

Reviewed By: CatherineGasnier

Differential Revision: D18633271

fbshipit-source-id: 2b7a7beebf2ce30c2cb2765f75de2db6bdb3c24e
  • Loading branch information
vassilmladenov authored and facebook-github-bot committed Jan 20, 2020
1 parent 08d200a commit 60daa11
Show file tree
Hide file tree
Showing 50 changed files with 714 additions and 350 deletions.
4 changes: 2 additions & 2 deletions hphp/hack/src/decl/decl_pos_utils.ml
Expand Up @@ -75,7 +75,7 @@ struct
| 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)
| Runpack_param (p1, p2, i) -> Runpack_param (pos p1, pos p2, i)
| Rinout_param p -> Rinout_param (pos p)
| Rinstantiate (r1, x, r2) -> Rinstantiate (reason r1, x, reason r2)
| Rarray_filter (p, r) -> Rarray_filter (pos p, reason r)
Expand Down Expand Up @@ -119,7 +119,7 @@ struct
| Rlambda_param (p, r) -> Rlambda_param (pos p, reason r)
| Rshape (p, fun_name) -> Rshape (pos p, fun_name)
| Renforceable p -> Renforceable (pos p)
| Rdestructure (p, l) -> Rdestructure (pos p, l)
| Rdestructure p -> Rdestructure (pos p)
| Rkey_value_collection_key p -> Rkey_value_collection_key (pos p)

let pos_mapper =
Expand Down
4 changes: 3 additions & 1 deletion hphp/hack/src/errors/error_codes.ml
Expand Up @@ -617,7 +617,9 @@ module Typing = struct
| RecordUnknownField
| CyclicRecordDef
| InvalidDestructure
| StaticCallWithClassLevelReifiedGeneric (* EXTEND HERE WITH NEW VALUES IF NEEDED *)
| StaticCallWithClassLevelReifiedGeneric
| SplatArrayRequired
| SplatArrayVariadic (* EXTEND HERE WITH NEW VALUES IF NEEDED *)
[@@deriving enum, show { with_path = false }]

let err_code = to_enum
Expand Down
20 changes: 20 additions & 0 deletions hphp/hack/src/errors/errors.ml
Expand Up @@ -2777,6 +2777,26 @@ let invalid_destructure pos1 pos2 ty =
(pos2, "This is " ^ ty);
]

let unpack_array_required_argument p fp =
add_list
(Typing.err_code Typing.SplatArrayRequired)
[
( p,
"An array cannot be unpacked into the required arguments of a function"
);
(fp, "Definition is here");
]

let unpack_array_variadic_argument p fp =
add_list
(Typing.err_code Typing.SplatArrayRequired)
[
( p,
"A function that receives an unpacked array as an argument must have a variadic parameter to accept the elements of the array"
);
(fp, "Definition is here");
]

let array_get_arity pos1 name pos2 =
add_list
(Typing.err_code Typing.ArrayGetArity)
Expand Down
4 changes: 4 additions & 0 deletions hphp/hack/src/errors/errors.mli
Expand Up @@ -368,6 +368,10 @@ val unpacking_disallowed_builtin_function : Pos.t -> string -> unit

val invalid_destructure : Pos.t -> Pos.t -> string -> unit

val unpack_array_required_argument : Pos.t -> Pos.t -> unit

val unpack_array_variadic_argument : Pos.t -> Pos.t -> unit

val array_get_arity : Pos.t -> string -> Pos.t -> unit

val typing_error : Pos.t -> string -> unit
Expand Down
19 changes: 15 additions & 4 deletions hphp/hack/src/typing/type_mapper_generic.ml
Expand Up @@ -334,7 +334,7 @@ class type ['env] constraint_type_mapper_type =
'env -> Reason.t -> has_member -> 'env * constraint_type

method on_Tdestructure :
'env -> Reason.t -> locl_ty list -> 'env * constraint_type
'env -> Reason.t -> destructure -> 'env * constraint_type

method on_TCunion :
'env -> Reason.t -> locl_ty -> constraint_type -> 'env * constraint_type
Expand Down Expand Up @@ -371,9 +371,20 @@ class ['env] constraint_type_mapper : ['env] locl_constraint_type_mapper_type =
let hm = { hm_name; hm_type; hm_class_id } in
(env, mk_constraint_type (r, Thas_member hm))

method on_Tdestructure env r tyl =
let (env, tyl) = this#on_locl_ty_list env tyl in
(env, mk_constraint_type (r, Tdestructure tyl))
method on_Tdestructure env r { d_required; d_optional; d_variadic; d_kind }
=
let (env, d_required) = this#on_locl_ty_list env d_required in
let (env, d_optional) = this#on_locl_ty_list env d_optional in
let (env, d_variadic) =
match d_variadic with
| None -> (env, d_variadic)
| Some v ->
let (env, v) = this#on_type env v in
(env, Some v)
in
( env,
mk_constraint_type
(r, Tdestructure { d_required; d_optional; d_variadic; d_kind }) )

method on_TCunion env r lty cty =
let (env, lty) = this#on_type env lty in
Expand Down

0 comments on commit 60daa11

Please sign in to comment.