@@ -28,6 +28,7 @@ type error =
28
28
| Repeated_parameter
29
29
| Duplicate_constructor of string
30
30
| Duplicate_label of string * string option
31
+ | Object_spread_with_record_field of string
31
32
| Recursive_abbrev of string
32
33
| Cycle_in_def of string * type_expr
33
34
| Definition_mismatch of type_expr * Includecore .type_mismatch list
@@ -255,13 +256,61 @@ let transl_labels ?record_name env closed lbls =
255
256
in
256
257
(lbls, lbls')
257
258
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
+
258
265
let transl_constructor_arguments env closed = function
259
266
| Pcstr_tuple l ->
260
267
let l = List. map (transl_simple_type env closed) l in
261
268
(Types. Cstr_tuple (List. map (fun t -> t.ctyp_type) l), Cstr_tuple l)
262
- | Pcstr_record l ->
269
+ | Pcstr_record l -> (
263
270
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])))))
265
314
266
315
let make_constructor env type_path type_params sargs sret_type =
267
316
match sret_type with
@@ -582,64 +631,7 @@ let transl_declaration ~type_record_as_object ~untagged_wfc env sdecl id =
582
631
transl_labels ~record_name: sdecl.ptype_name.txt env true lbls
583
632
in
584
633
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'
643
635
in
644
636
let rec check_duplicates loc (lbls : Typedtree.label_declaration list )
645
637
seen =
@@ -663,24 +655,38 @@ let transl_declaration ~type_record_as_object ~untagged_wfc env sdecl id =
663
655
else if optional then Record_regular
664
656
else Record_regular ),
665
657
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)))
684
690
| Ptype_open -> (Ttype_open , Type_open , sdecl)
685
691
in
686
692
let tman, man =
@@ -818,6 +824,12 @@ let check_constraints ~type_record_as_object env sdecl (_, decl) =
818
824
styl tyl
819
825
| Cstr_record tyl , Pcstr_record styl ->
820
826
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
821
833
| _ -> assert false );
822
834
match (pcd_res, cd_res) with
823
835
| Some sr , Some r -> check_constraints_rec env sr.ptyp_loc visited r
@@ -2110,6 +2122,12 @@ let report_error ppf = function
2110
2122
" The field @{<info>%s@} is defined several times in this record. Fields \
2111
2123
can only be added once to a record."
2112
2124
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
2113
2131
| Invalid_attribute msg -> fprintf ppf " %s" msg
2114
2132
| Duplicate_label (s , Some record_name ) ->
2115
2133
fprintf ppf
0 commit comments