Skip to content

Commit 508e2b3

Browse files
cristianoczth
andauthored
Allow type variables when spreading record type definitions (#6309)
* exploration * simple substitution of type parameters, and some cleanup * do deep substitution of type variables * allow renaming type variables, and spreading with instantiated type variables * cleanup * redo pairing type params logic * refactor: better type inference * Don't ask. * remove open * rename record type spread utils file * move more things related to record type spreads into dedicated file * test for deep sub * extend test a bit * changelog --------- Co-authored-by: Gabriel Nordeborn <[email protected]>
1 parent adee7d0 commit 508e2b3

File tree

7 files changed

+214
-13
lines changed

7 files changed

+214
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#### :rocket: New Feature
1616
- Untagged variants: consider regexp as an object type. https://github.com/rescript-lang/rescript-compiler/pull/6296
1717
- Semantic-based optimization of code generated for untagged variants https://github.com/rescript-lang/rescript-compiler/issues/6108
18+
- Record type spreads: Allow using type variables in type spreads. Both uninstantiated and instantiated ones https://github.com/rescript-lang/rescript-compiler/pull/6309
1819

1920
# 11.0.0-beta.2
2021

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/record_type_spreads_deep_sub.res:8:9-21
4+
5+
6 │
6+
7 │ let d: d = {
7+
8 │ x: Ok("this errors"),
8+
9 │ }
9+
10 │
10+
11+
This has type: string
12+
Somewhere wanted: int
13+
14+
You can convert string to int with Belt.Int.fromString.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Checks that deep subsitution works as intended
2+
type t<'a, 'b> = {x: result<'a, 'b>}
3+
type d = {
4+
...t<int, int>,
5+
}
6+
7+
let d: d = {
8+
x: Ok("this errors"),
9+
}

jscomp/ml/record_type_spread.ml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
module StringMap = Map.Make (String)
2+
3+
let t_equals t1 t2 = t1.Types.level = t2.Types.level && t1.id = t2.id
4+
5+
let substitute_types ~type_map (t : Types.type_expr) =
6+
if StringMap.is_empty type_map then t
7+
else
8+
let apply_substitution type_variable_name t =
9+
match StringMap.find_opt type_variable_name type_map with
10+
| None -> t
11+
| Some substituted_type -> substituted_type
12+
in
13+
let rec loop (t : Types.type_expr) =
14+
match t.desc with
15+
| Tlink t -> {t with desc = Tlink (loop t)}
16+
| Tvar (Some type_variable_name) ->
17+
apply_substitution type_variable_name t
18+
| Tvar None -> t
19+
| Tunivar _ -> t
20+
| Tconstr (path, args, _memo) ->
21+
{t with desc = Tconstr (path, args |> List.map loop, ref Types.Mnil)}
22+
| Tsubst t -> {t with desc = Tsubst (loop t)}
23+
| Tvariant rd -> {t with desc = Tvariant (row_desc rd)}
24+
| Tnil -> t
25+
| Tarrow (lbl, t1, t2, c) ->
26+
{t with desc = Tarrow (lbl, loop t1, loop t2, c)}
27+
| Ttuple tl -> {t with desc = Ttuple (tl |> List.map loop)}
28+
| Tobject (t, r) -> {t with desc = Tobject (loop t, r)}
29+
| Tfield (n, k, t1, t2) -> {t with desc = Tfield (n, k, loop t1, loop t2)}
30+
| Tpoly (t, []) -> loop t
31+
| Tpoly (t, tl) -> {t with desc = Tpoly (loop t, tl |> List.map loop)}
32+
| Tpackage (p, l, tl) ->
33+
{t with desc = Tpackage (p, l, tl |> List.map loop)}
34+
and row_desc (rd : Types.row_desc) =
35+
let row_fields =
36+
rd.row_fields |> List.map (fun (l, rf) -> (l, row_field rf))
37+
in
38+
let row_more = loop rd.row_more in
39+
let row_name =
40+
match rd.row_name with
41+
| None -> None
42+
| Some (p, tl) -> Some (p, tl |> List.map loop)
43+
in
44+
{rd with row_fields; row_more; row_name}
45+
and row_field (rf : Types.row_field) =
46+
match rf with
47+
| Rpresent None -> rf
48+
| Rpresent (Some t) -> Rpresent (Some (loop t))
49+
| Reither (b1, tl, b2, r) -> Reither (b1, tl |> List.map loop, b2, r)
50+
| Rabsent -> Rabsent
51+
in
52+
loop t
53+
54+
let substitute_type_vars (type_vars : (string * Types.type_expr) list)
55+
(typ : Types.type_expr) =
56+
let type_map =
57+
type_vars
58+
|> List.fold_left
59+
(fun acc (tvar_name, tvar_typ) -> StringMap.add tvar_name tvar_typ acc)
60+
StringMap.empty
61+
in
62+
substitute_types ~type_map typ
63+
64+
let has_type_spread (lbls : Typedtree.label_declaration list) =
65+
lbls
66+
|> List.exists (fun (l : Typedtree.label_declaration) ->
67+
match l with
68+
| {ld_name = {txt = "..."}} -> true
69+
| _ -> false)
70+
71+
let extract_type_vars (type_params : Types.type_expr list)
72+
(typ : Types.type_expr) =
73+
(* The type variables applied to the record spread itself. *)
74+
let applied_type_vars =
75+
match Ctype.repr typ with
76+
| {desc = Tpoly ({desc = Tconstr (_, tvars, _)}, _)} -> tvars
77+
| _ -> []
78+
in
79+
if List.length type_params = List.length applied_type_vars then
80+
(* Track which type param in the record we're spreading
81+
belongs to which type variable applied to the spread itself. *)
82+
let paired_type_vars = List.combine type_params applied_type_vars in
83+
paired_type_vars
84+
|> List.filter_map (fun (t, applied_tvar) ->
85+
match t.Types.desc with
86+
| Tvar (Some tname) -> Some (tname, applied_tvar)
87+
| _ -> None)
88+
else []

jscomp/ml/typedecl.ml

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -425,29 +425,38 @@ let transl_declaration ~typeRecordAsObject env sdecl id =
425425
else typ in
426426
{lbl with pld_type = typ }) in
427427
let lbls, lbls' = transl_labels env true lbls in
428-
let has_spread =
429-
lbls
430-
|> List.exists (fun l ->
431-
match l with
432-
| {ld_name = {txt = "..."}} -> true
433-
| _ -> false) in
434-
let lbls_opt = match has_spread with
428+
let lbls_opt = match Record_type_spread.has_type_spread lbls with
435429
| true ->
436430
let rec extract t = match t.desc with
437431
| Tpoly(t, []) -> extract t
438432
| _ -> Ctype.repr t in
439-
let mkLbl (l: Types.label_declaration) (ld_type: Typedtree.core_type) : Typedtree.label_declaration =
440-
{ ld_id = l.ld_id;
433+
let mkLbl (l: Types.label_declaration) (ld_type: Typedtree.core_type) (type_vars: (string * Types.type_expr) list) : Typedtree.label_declaration =
434+
{
435+
ld_id = l.ld_id;
441436
ld_name = {txt = Ident.name l.ld_id; loc = l.ld_loc};
442437
ld_mutable = l.ld_mutable;
443-
ld_type = {ld_type with ctyp_type = l.ld_type};
438+
ld_type = {ld_type with ctyp_type = Record_type_spread.substitute_type_vars type_vars l.ld_type};
444439
ld_loc = l.ld_loc;
445-
ld_attributes = l.ld_attributes; } in
440+
ld_attributes = l.ld_attributes;
441+
} in
446442
let rec process_lbls acc lbls lbls' = match lbls, lbls' with
447443
| {ld_name = {txt = "..."}; ld_type} :: rest, _ :: rest' ->
448444
(match Ctype.extract_concrete_typedecl env (extract ld_type.ctyp_type) with
449-
(_p0, _p, {type_kind=Type_record (fields, _repr)}) ->
450-
process_lbls (fst acc @ (fields |> List.map (fun l -> mkLbl l ld_type)), snd acc @ fields) rest rest'
445+
(_p0, _p, {type_kind=Type_record (fields, _repr); type_params}) ->
446+
let type_vars = Record_type_spread.extract_type_vars type_params ld_type.ctyp_type in
447+
process_lbls
448+
( fst acc
449+
@ (Ext_list.map fields (fun l ->
450+
mkLbl l ld_type type_vars))
451+
,
452+
snd acc
453+
@ (Ext_list.map fields (fun l ->
454+
{
455+
l with
456+
ld_type =
457+
Record_type_spread.substitute_type_vars type_vars l.ld_type;
458+
})) )
459+
rest rest'
451460
| _ -> assert false
452461
| exception _ -> None)
453462
| lbl::rest, lbl'::rest' -> process_lbls (fst acc @ [lbl], snd acc @ [lbl']) rest rest'

jscomp/test/record_type_spread.js

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jscomp/test/record_type_spread.res

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,53 @@ let getY = (v: y) => v.y
99
let getX = (v: y) => v.x
1010

1111
let v: y = {y: 3, x: 3}
12+
13+
type f<'a> = {
14+
a: string,
15+
b: 'a,
16+
c: option<'a>,
17+
d: option<result<'a, 'a>>,
18+
}
19+
20+
type d<'a> = {
21+
...f<'a>,
22+
}
23+
24+
let d: d<int> = {
25+
a: "",
26+
b: 1,
27+
c: None,
28+
d: Some(Ok(1)),
29+
}
30+
31+
type rn<'aaa> = {c: option<'aaa>}
32+
33+
type withRenamedTypeVariable<'bbb> = {
34+
...rn<'bbb>,
35+
}
36+
37+
let x: withRenamedTypeVariable<bool> = {
38+
c: Some(true),
39+
}
40+
41+
type rnAsString = {
42+
...rn<string>,
43+
}
44+
45+
let x: rnAsString = {
46+
c: Some("hello"),
47+
}
48+
49+
module DeepSub = {
50+
type t<'a, 'b> = {
51+
x: result<'a, 'b>,
52+
z: [#One | #Two('a) | #Three('b)],
53+
}
54+
type d = {
55+
...t<int, int>,
56+
}
57+
let d: d = {
58+
x: Ok(1),
59+
z: #Two(1),
60+
}
61+
}

0 commit comments

Comments
 (0)