Skip to content

Commit d711614

Browse files
authored
Support record type spreads in inline records (#7859)
* support inline record type spreads * refactor for reuse * format * refactor * clearer comments * changelog * fix disallowing mixing record and object spreads with fields of the other kind * output * cover another case * format
1 parent 497f233 commit d711614

16 files changed

+356
-145
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
- Fix code generation for emojis in polyvars and labels. https://github.com/rescript-lang/rescript/pull/7853
2626
- Add `reset` to `experimental_features` to correctly reset playground. https://github.com/rescript-lang/rescript/pull/7868
2727
- Fix crash with `@get` on external of type `unit => 'a`. https://github.com/rescript-lang/rescript/pull/7866
28+
- Fix record type spreads in inline records. https://github.com/rescript-lang/rescript/pull/7859
2829

2930
#### :memo: Documentation
3031

compiler/ml/record_type_spread.ml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,52 @@ let extract_type_vars (type_params : Types.type_expr list)
8989
| Tvar (Some tname) -> Some (tname, applied_tvar)
9090
| _ -> None)
9191
else []
92+
93+
let expand_labels_with_type_spreads (env : Env.t)
94+
(lbls : Typedtree.label_declaration list)
95+
(lbls' : Types.label_declaration list) =
96+
match has_type_spread lbls with
97+
| false -> Some (lbls, lbls')
98+
| true ->
99+
let rec extract (t : Types.type_expr) =
100+
match t.desc with
101+
| Tpoly (t, []) -> extract t
102+
| _ -> Ctype.repr t
103+
in
104+
let mk_lbl (l : Types.label_declaration) (ld_type : Typedtree.core_type)
105+
(type_vars : (string * Types.type_expr) list) :
106+
Typedtree.label_declaration =
107+
{
108+
ld_id = l.ld_id;
109+
ld_name = {txt = Ident.name l.ld_id; loc = l.ld_loc};
110+
ld_mutable = l.ld_mutable;
111+
ld_optional = l.ld_optional;
112+
ld_type =
113+
{ld_type with ctyp_type = substitute_type_vars type_vars l.ld_type};
114+
ld_loc = l.ld_loc;
115+
ld_attributes = l.ld_attributes;
116+
}
117+
in
118+
let rec process_lbls acc (lbls : Typedtree.label_declaration list)
119+
(lbls' : Types.label_declaration list) =
120+
match (lbls, lbls') with
121+
| {ld_name = {txt = "..."}; ld_type} :: rest, _ :: rest' -> (
122+
match
123+
Ctype.extract_concrete_typedecl env (extract ld_type.ctyp_type)
124+
with
125+
| _p0, _p, {type_kind = Type_record (fields, _repr); type_params} ->
126+
let type_vars = extract_type_vars type_params ld_type.ctyp_type in
127+
process_lbls
128+
( fst acc @ Ext_list.map fields (fun l -> mk_lbl l ld_type type_vars),
129+
snd acc
130+
@ Ext_list.map fields (fun l ->
131+
{l with ld_type = substitute_type_vars type_vars l.ld_type})
132+
)
133+
rest rest'
134+
| _ -> None
135+
| exception _ -> None)
136+
| lbl :: rest, lbl' :: rest' ->
137+
process_lbls (fst acc @ [lbl], snd acc @ [lbl']) rest rest'
138+
| _ -> Some acc
139+
in
140+
process_lbls ([], []) lbls lbls'

compiler/ml/typedecl.ml

Lines changed: 96 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type error =
2828
| Repeated_parameter
2929
| Duplicate_constructor of string
3030
| Duplicate_label of string * string option
31+
| Object_spread_with_record_field of string
3132
| Recursive_abbrev of string
3233
| Cycle_in_def of string * type_expr
3334
| Definition_mismatch of type_expr * Includecore.type_mismatch list
@@ -255,13 +256,61 @@ let transl_labels ?record_name env closed lbls =
255256
in
256257
(lbls, lbls')
257258

259+
let first_non_spread_field (lbls_ : Parsetree.label_declaration list) =
260+
List.find_map
261+
(fun (ld : Parsetree.label_declaration) ->
262+
if ld.pld_name.txt <> "..." then Some ld else None)
263+
lbls_
264+
258265
let transl_constructor_arguments env closed = function
259266
| Pcstr_tuple l ->
260267
let l = List.map (transl_simple_type env closed) l in
261268
(Types.Cstr_tuple (List.map (fun t -> t.ctyp_type) l), Cstr_tuple l)
262-
| Pcstr_record l ->
269+
| Pcstr_record l -> (
263270
let lbls, lbls' = transl_labels env closed l in
264-
(Types.Cstr_record lbls', Cstr_record lbls)
271+
let expanded =
272+
Record_type_spread.expand_labels_with_type_spreads env lbls lbls'
273+
in
274+
match expanded with
275+
| Some (lbls, lbls') -> (Types.Cstr_record lbls', Cstr_record lbls)
276+
| None -> (
277+
match l with
278+
| [{pld_name = {txt = "..."}; pld_type = spread_typ; _}] ->
279+
(* Ambiguous `{...t}`: if only spread present and it doesn't resolve to a
280+
record type, treat it as an object-typed tuple argument. *)
281+
let obj_ty =
282+
Ast_helper.Typ.object_ ~loc:spread_typ.ptyp_loc
283+
[Parsetree.Oinherit spread_typ]
284+
Asttypes.Closed
285+
in
286+
let cty = transl_simple_type env closed obj_ty in
287+
(Types.Cstr_tuple [cty.ctyp_type], Cstr_tuple [cty])
288+
| _ -> (
289+
(* Could not resolve spread to a record type, but additional record
290+
fields are present. Mirror declaration logic and reject mixing
291+
object-type spreads with record fields. *)
292+
match first_non_spread_field l with
293+
| Some ld ->
294+
raise
295+
(Error (ld.pld_loc, Object_spread_with_record_field ld.pld_name.txt))
296+
| None -> (
297+
(* Be defensive: treat as an object-typed tuple if somehow only spreads
298+
are present but not caught by the single-spread case. *)
299+
let fields =
300+
Ext_list.filter_map l (fun ld ->
301+
match ld.pld_name.txt with
302+
| "..." -> Some (Parsetree.Oinherit ld.pld_type)
303+
| _ -> None)
304+
in
305+
match fields with
306+
| [] -> (Types.Cstr_record lbls', Cstr_record lbls)
307+
| _ ->
308+
let obj_ty =
309+
Ast_helper.Typ.object_ ~loc:(List.hd l).pld_loc fields
310+
Asttypes.Closed
311+
in
312+
let cty = transl_simple_type env closed obj_ty in
313+
(Types.Cstr_tuple [cty.ctyp_type], Cstr_tuple [cty])))))
265314

266315
let make_constructor env type_path type_params sargs sret_type =
267316
match sret_type with
@@ -582,64 +631,7 @@ let transl_declaration ~type_record_as_object ~untagged_wfc env sdecl id =
582631
transl_labels ~record_name:sdecl.ptype_name.txt env true lbls
583632
in
584633
let lbls_opt =
585-
match Record_type_spread.has_type_spread lbls with
586-
| true ->
587-
let rec extract t =
588-
match t.desc with
589-
| Tpoly (t, []) -> extract t
590-
| _ -> Ctype.repr t
591-
in
592-
let mk_lbl (l : Types.label_declaration)
593-
(ld_type : Typedtree.core_type)
594-
(type_vars : (string * Types.type_expr) list) :
595-
Typedtree.label_declaration =
596-
{
597-
ld_id = l.ld_id;
598-
ld_name = {txt = Ident.name l.ld_id; loc = l.ld_loc};
599-
ld_mutable = l.ld_mutable;
600-
ld_optional = l.ld_optional;
601-
ld_type =
602-
{
603-
ld_type with
604-
ctyp_type =
605-
Record_type_spread.substitute_type_vars type_vars l.ld_type;
606-
};
607-
ld_loc = l.ld_loc;
608-
ld_attributes = l.ld_attributes;
609-
}
610-
in
611-
let rec process_lbls acc lbls lbls' =
612-
match (lbls, lbls') with
613-
| {ld_name = {txt = "..."}; ld_type} :: rest, _ :: rest' -> (
614-
match
615-
Ctype.extract_concrete_typedecl env (extract ld_type.ctyp_type)
616-
with
617-
| _p0, _p, {type_kind = Type_record (fields, _repr); type_params}
618-
->
619-
let type_vars =
620-
Record_type_spread.extract_type_vars type_params
621-
ld_type.ctyp_type
622-
in
623-
process_lbls
624-
( fst acc
625-
@ Ext_list.map fields (fun l -> mk_lbl l ld_type type_vars),
626-
snd acc
627-
@ Ext_list.map fields (fun l ->
628-
{
629-
l with
630-
ld_type =
631-
Record_type_spread.substitute_type_vars type_vars
632-
l.ld_type;
633-
}) )
634-
rest rest'
635-
| _ -> assert false
636-
| exception _ -> None)
637-
| lbl :: rest, lbl' :: rest' ->
638-
process_lbls (fst acc @ [lbl], snd acc @ [lbl']) rest rest'
639-
| _ -> Some acc
640-
in
641-
process_lbls ([], []) lbls lbls'
642-
| false -> Some (lbls, lbls')
634+
Record_type_spread.expand_labels_with_type_spreads env lbls lbls'
643635
in
644636
let rec check_duplicates loc (lbls : Typedtree.label_declaration list)
645637
seen =
@@ -663,24 +655,38 @@ let transl_declaration ~type_record_as_object ~untagged_wfc env sdecl id =
663655
else if optional then Record_regular
664656
else Record_regular ),
665657
sdecl )
666-
| None ->
667-
(* Could not find record type decl for ...t: assume t is an object type and this is syntax ambiguity *)
668-
type_record_as_object := true;
669-
let fields =
670-
Ext_list.map lbls_ (fun ld ->
671-
match ld.pld_name.txt with
672-
| "..." -> Parsetree.Oinherit ld.pld_type
673-
| _ -> Otag (ld.pld_name, ld.pld_attributes, ld.pld_type))
674-
in
675-
let sdecl =
676-
{
677-
sdecl with
678-
ptype_kind = Ptype_abstract;
679-
ptype_manifest =
680-
Some (Ast_helper.Typ.object_ ~loc:sdecl.ptype_loc fields Closed);
681-
}
682-
in
683-
(Ttype_abstract, Type_abstract, sdecl))
658+
| None -> (
659+
(* Could not find record type decl for ...t. This happens when the spread
660+
target is not a record type (e.g. an object type). If additional
661+
fields are present in the record, this mixes a record field with an
662+
object-type spread and should be rejected. If only the spread exists,
663+
reinterpret as an object type for backwards compatibility. *)
664+
(* TODO: We really really need to make this "spread that needs to be resolved"
665+
concept 1st class in the AST or similar. This is quite hacky and fragile as
666+
is.*)
667+
match first_non_spread_field lbls_ with
668+
| Some ld ->
669+
(* Error on the first record field mixed with an object spread. *)
670+
raise
671+
(Error (ld.pld_loc, Object_spread_with_record_field ld.pld_name.txt))
672+
| None ->
673+
(* Only a spread present: treat as object type (syntax ambiguity). *)
674+
type_record_as_object := true;
675+
let fields =
676+
Ext_list.map lbls_ (fun ld ->
677+
match ld.pld_name.txt with
678+
| "..." -> Parsetree.Oinherit ld.pld_type
679+
| _ -> Otag (ld.pld_name, ld.pld_attributes, ld.pld_type))
680+
in
681+
let sdecl =
682+
{
683+
sdecl with
684+
ptype_kind = Ptype_abstract;
685+
ptype_manifest =
686+
Some (Ast_helper.Typ.object_ ~loc:sdecl.ptype_loc fields Closed);
687+
}
688+
in
689+
(Ttype_abstract, Type_abstract, sdecl)))
684690
| Ptype_open -> (Ttype_open, Type_open, sdecl)
685691
in
686692
let tman, man =
@@ -818,6 +824,12 @@ let check_constraints ~type_record_as_object env sdecl (_, decl) =
818824
styl tyl
819825
| Cstr_record tyl, Pcstr_record styl ->
820826
check_constraints_labels env visited tyl styl
827+
| ( Cstr_tuple [ty],
828+
Pcstr_record [{pld_name = {txt = "..."}; pld_type; _}] ) ->
829+
(* Ambiguous `{...t}` parsed as record with a single spread; typer may
830+
reinterpret as an object tuple argument. Accept this and check the
831+
single tuple arg against the source location of the spread type. *)
832+
check_constraints_rec env pld_type.ptyp_loc visited ty
821833
| _ -> assert false);
822834
match (pcd_res, cd_res) with
823835
| Some sr, Some r -> check_constraints_rec env sr.ptyp_loc visited r
@@ -2110,6 +2122,12 @@ let report_error ppf = function
21102122
"The field @{<info>%s@} is defined several times in this record. Fields \
21112123
can only be added once to a record."
21122124
s
2125+
| Object_spread_with_record_field field_name ->
2126+
fprintf ppf
2127+
"@[You cannot mix a record field with an object type spread.@\n\
2128+
Remove the record field or change it to an object field (e.g. \"%s\": \
2129+
...).@]"
2130+
field_name
21132131
| Invalid_attribute msg -> fprintf ppf "%s" msg
21142132
| Duplicate_label (s, Some record_name) ->
21152133
fprintf ppf

0 commit comments

Comments
 (0)