@@ -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+
258265let 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
266315let 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