diff --git a/CHANGELOG.md b/CHANGELOG.md index 22769c4fd6..255f8b8463 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Fix code generation for emojis in polyvars and labels. https://github.com/rescript-lang/rescript/pull/7853 - Add `reset` to `experimental_features` to correctly reset playground. https://github.com/rescript-lang/rescript/pull/7868 - Fix crash with `@get` on external of type `unit => 'a`. https://github.com/rescript-lang/rescript/pull/7866 +- Fix record type spreads in inline records. https://github.com/rescript-lang/rescript/pull/7859 #### :memo: Documentation diff --git a/compiler/ml/record_type_spread.ml b/compiler/ml/record_type_spread.ml index 80dcbe9d5d..bf7b286dd2 100644 --- a/compiler/ml/record_type_spread.ml +++ b/compiler/ml/record_type_spread.ml @@ -89,3 +89,52 @@ let extract_type_vars (type_params : Types.type_expr list) | Tvar (Some tname) -> Some (tname, applied_tvar) | _ -> None) else [] + +let expand_labels_with_type_spreads (env : Env.t) + (lbls : Typedtree.label_declaration list) + (lbls' : Types.label_declaration list) = + match has_type_spread lbls with + | false -> Some (lbls, lbls') + | true -> + let rec extract (t : Types.type_expr) = + match t.desc with + | Tpoly (t, []) -> extract t + | _ -> Ctype.repr t + in + let mk_lbl (l : Types.label_declaration) (ld_type : Typedtree.core_type) + (type_vars : (string * Types.type_expr) list) : + Typedtree.label_declaration = + { + ld_id = l.ld_id; + ld_name = {txt = Ident.name l.ld_id; loc = l.ld_loc}; + ld_mutable = l.ld_mutable; + ld_optional = l.ld_optional; + ld_type = + {ld_type with ctyp_type = substitute_type_vars type_vars l.ld_type}; + ld_loc = l.ld_loc; + ld_attributes = l.ld_attributes; + } + in + let rec process_lbls acc (lbls : Typedtree.label_declaration list) + (lbls' : Types.label_declaration list) = + match (lbls, lbls') with + | {ld_name = {txt = "..."}; ld_type} :: rest, _ :: rest' -> ( + match + Ctype.extract_concrete_typedecl env (extract ld_type.ctyp_type) + with + | _p0, _p, {type_kind = Type_record (fields, _repr); type_params} -> + let type_vars = extract_type_vars type_params ld_type.ctyp_type in + process_lbls + ( fst acc @ Ext_list.map fields (fun l -> mk_lbl l ld_type type_vars), + snd acc + @ Ext_list.map fields (fun l -> + {l with ld_type = substitute_type_vars type_vars l.ld_type}) + ) + rest rest' + | _ -> None + | exception _ -> None) + | lbl :: rest, lbl' :: rest' -> + process_lbls (fst acc @ [lbl], snd acc @ [lbl']) rest rest' + | _ -> Some acc + in + process_lbls ([], []) lbls lbls' diff --git a/compiler/ml/typedecl.ml b/compiler/ml/typedecl.ml index 360354600c..da35de5288 100644 --- a/compiler/ml/typedecl.ml +++ b/compiler/ml/typedecl.ml @@ -28,6 +28,7 @@ type error = | Repeated_parameter | Duplicate_constructor of string | Duplicate_label of string * string option + | Object_spread_with_record_field of string | Recursive_abbrev of string | Cycle_in_def of string * type_expr | Definition_mismatch of type_expr * Includecore.type_mismatch list @@ -255,13 +256,61 @@ let transl_labels ?record_name env closed lbls = in (lbls, lbls') +let first_non_spread_field (lbls_ : Parsetree.label_declaration list) = + List.find_map + (fun (ld : Parsetree.label_declaration) -> + if ld.pld_name.txt <> "..." then Some ld else None) + lbls_ + let transl_constructor_arguments env closed = function | Pcstr_tuple l -> let l = List.map (transl_simple_type env closed) l in (Types.Cstr_tuple (List.map (fun t -> t.ctyp_type) l), Cstr_tuple l) - | Pcstr_record l -> + | Pcstr_record l -> ( let lbls, lbls' = transl_labels env closed l in - (Types.Cstr_record lbls', Cstr_record lbls) + let expanded = + Record_type_spread.expand_labels_with_type_spreads env lbls lbls' + in + match expanded with + | Some (lbls, lbls') -> (Types.Cstr_record lbls', Cstr_record lbls) + | None -> ( + match l with + | [{pld_name = {txt = "..."}; pld_type = spread_typ; _}] -> + (* Ambiguous `{...t}`: if only spread present and it doesn't resolve to a + record type, treat it as an object-typed tuple argument. *) + let obj_ty = + Ast_helper.Typ.object_ ~loc:spread_typ.ptyp_loc + [Parsetree.Oinherit spread_typ] + Asttypes.Closed + in + let cty = transl_simple_type env closed obj_ty in + (Types.Cstr_tuple [cty.ctyp_type], Cstr_tuple [cty]) + | _ -> ( + (* Could not resolve spread to a record type, but additional record + fields are present. Mirror declaration logic and reject mixing + object-type spreads with record fields. *) + match first_non_spread_field l with + | Some ld -> + raise + (Error (ld.pld_loc, Object_spread_with_record_field ld.pld_name.txt)) + | None -> ( + (* Be defensive: treat as an object-typed tuple if somehow only spreads + are present but not caught by the single-spread case. *) + let fields = + Ext_list.filter_map l (fun ld -> + match ld.pld_name.txt with + | "..." -> Some (Parsetree.Oinherit ld.pld_type) + | _ -> None) + in + match fields with + | [] -> (Types.Cstr_record lbls', Cstr_record lbls) + | _ -> + let obj_ty = + Ast_helper.Typ.object_ ~loc:(List.hd l).pld_loc fields + Asttypes.Closed + in + let cty = transl_simple_type env closed obj_ty in + (Types.Cstr_tuple [cty.ctyp_type], Cstr_tuple [cty]))))) let make_constructor env type_path type_params sargs sret_type = match sret_type with @@ -582,64 +631,7 @@ let transl_declaration ~type_record_as_object ~untagged_wfc env sdecl id = transl_labels ~record_name:sdecl.ptype_name.txt env true lbls in let lbls_opt = - match Record_type_spread.has_type_spread lbls with - | true -> - let rec extract t = - match t.desc with - | Tpoly (t, []) -> extract t - | _ -> Ctype.repr t - in - let mk_lbl (l : Types.label_declaration) - (ld_type : Typedtree.core_type) - (type_vars : (string * Types.type_expr) list) : - Typedtree.label_declaration = - { - ld_id = l.ld_id; - ld_name = {txt = Ident.name l.ld_id; loc = l.ld_loc}; - ld_mutable = l.ld_mutable; - ld_optional = l.ld_optional; - ld_type = - { - ld_type with - ctyp_type = - Record_type_spread.substitute_type_vars type_vars l.ld_type; - }; - ld_loc = l.ld_loc; - ld_attributes = l.ld_attributes; - } - in - let rec process_lbls acc lbls lbls' = - match (lbls, lbls') with - | {ld_name = {txt = "..."}; ld_type} :: rest, _ :: rest' -> ( - match - Ctype.extract_concrete_typedecl env (extract ld_type.ctyp_type) - with - | _p0, _p, {type_kind = Type_record (fields, _repr); type_params} - -> - let type_vars = - Record_type_spread.extract_type_vars type_params - ld_type.ctyp_type - in - process_lbls - ( fst acc - @ Ext_list.map fields (fun l -> mk_lbl l ld_type type_vars), - snd acc - @ Ext_list.map fields (fun l -> - { - l with - ld_type = - Record_type_spread.substitute_type_vars type_vars - l.ld_type; - }) ) - rest rest' - | _ -> assert false - | exception _ -> None) - | lbl :: rest, lbl' :: rest' -> - process_lbls (fst acc @ [lbl], snd acc @ [lbl']) rest rest' - | _ -> Some acc - in - process_lbls ([], []) lbls lbls' - | false -> Some (lbls, lbls') + Record_type_spread.expand_labels_with_type_spreads env lbls lbls' in let rec check_duplicates loc (lbls : Typedtree.label_declaration list) seen = @@ -663,24 +655,38 @@ let transl_declaration ~type_record_as_object ~untagged_wfc env sdecl id = else if optional then Record_regular else Record_regular ), sdecl ) - | None -> - (* Could not find record type decl for ...t: assume t is an object type and this is syntax ambiguity *) - type_record_as_object := true; - let fields = - Ext_list.map lbls_ (fun ld -> - match ld.pld_name.txt with - | "..." -> Parsetree.Oinherit ld.pld_type - | _ -> Otag (ld.pld_name, ld.pld_attributes, ld.pld_type)) - in - let sdecl = - { - sdecl with - ptype_kind = Ptype_abstract; - ptype_manifest = - Some (Ast_helper.Typ.object_ ~loc:sdecl.ptype_loc fields Closed); - } - in - (Ttype_abstract, Type_abstract, sdecl)) + | None -> ( + (* Could not find record type decl for ...t. This happens when the spread + target is not a record type (e.g. an object type). If additional + fields are present in the record, this mixes a record field with an + object-type spread and should be rejected. If only the spread exists, + reinterpret as an object type for backwards compatibility. *) + (* TODO: We really really need to make this "spread that needs to be resolved" + concept 1st class in the AST or similar. This is quite hacky and fragile as + is.*) + match first_non_spread_field lbls_ with + | Some ld -> + (* Error on the first record field mixed with an object spread. *) + raise + (Error (ld.pld_loc, Object_spread_with_record_field ld.pld_name.txt)) + | None -> + (* Only a spread present: treat as object type (syntax ambiguity). *) + type_record_as_object := true; + let fields = + Ext_list.map lbls_ (fun ld -> + match ld.pld_name.txt with + | "..." -> Parsetree.Oinherit ld.pld_type + | _ -> Otag (ld.pld_name, ld.pld_attributes, ld.pld_type)) + in + let sdecl = + { + sdecl with + ptype_kind = Ptype_abstract; + ptype_manifest = + Some (Ast_helper.Typ.object_ ~loc:sdecl.ptype_loc fields Closed); + } + in + (Ttype_abstract, Type_abstract, sdecl))) | Ptype_open -> (Ttype_open, Type_open, sdecl) in let tman, man = @@ -818,6 +824,12 @@ let check_constraints ~type_record_as_object env sdecl (_, decl) = styl tyl | Cstr_record tyl, Pcstr_record styl -> check_constraints_labels env visited tyl styl + | ( Cstr_tuple [ty], + Pcstr_record [{pld_name = {txt = "..."}; pld_type; _}] ) -> + (* Ambiguous `{...t}` parsed as record with a single spread; typer may + reinterpret as an object tuple argument. Accept this and check the + single tuple arg against the source location of the spread type. *) + check_constraints_rec env pld_type.ptyp_loc visited ty | _ -> assert false); match (pcd_res, cd_res) with | Some sr, Some r -> check_constraints_rec env sr.ptyp_loc visited r @@ -2110,6 +2122,12 @@ let report_error ppf = function "The field @{%s@} is defined several times in this record. Fields \ can only be added once to a record." s + | Object_spread_with_record_field field_name -> + fprintf ppf + "@[You cannot mix a record field with an object type spread.@\n\ + Remove the record field or change it to an object field (e.g. \"%s\": \ + ...).@]" + field_name | Invalid_attribute msg -> fprintf ppf "%s" msg | Duplicate_label (s, Some record_name) -> fprintf ppf diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index 805874b161..6d203583cd 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -149,10 +149,6 @@ module ErrorMessages = struct let string_interpolation_in_pattern = "String interpolation is not supported in pattern matching." - let spread_in_record_declaration = - "A record type declaration doesn't support the ... spread. Only an object \ - (with quoted field names) does." - let object_quoted_field_name name = "An object type declaration needs quoted field names. Did you mean \"" ^ name ^ "\"?" @@ -4977,46 +4973,57 @@ and parse_constr_decl_args p = in Parser.expect Rparen p; Parsetree.Pcstr_tuple (typ :: more_args) - | DotDotDot -> + | DotDotDot -> ( let dotdotdot_start = p.start_pos in let dotdotdot_end = p.end_pos in - (* start of object type spreading, e.g. `User({...a, "u": int})` *) + (* start of spread, e.g. `User({...a, "u": int})` *) Parser.next p; - let typ = parse_typ_expr p in - let () = - match p.token with - | Rbrace -> - (* {...x}, spread without extra fields *) - Parser.next p - | _ -> Parser.expect Comma p - in - let () = - match p.token with - | Lident _ -> - Parser.err ~start_pos:dotdotdot_start ~end_pos:dotdotdot_end p - (Diagnostics.message ErrorMessages.spread_in_record_declaration) - | _ -> () - in - let fields = - Parsetree.Oinherit typ - :: parse_comma_delimited_region - ~grammar:Grammar.StringFieldDeclarations ~closing:Rbrace - ~f:parse_string_field_declaration p - in - Parser.expect Rbrace p; - let loc = mk_loc start_pos p.prev_end_pos in - let typ = - Ast_helper.Typ.object_ ~loc fields Asttypes.Closed - |> parse_type_alias p - in - let typ = parse_arrow_type_rest ~es6_arrow:true ~start_pos typ p in - Parser.optional p Comma |> ignore; - let more_args = - parse_comma_delimited_region ~grammar:Grammar.TypExprList - ~closing:Rparen ~f:parse_typ_expr_region p - in - Parser.expect Rparen p; - Parsetree.Pcstr_tuple (typ :: more_args) + let spread_typ = parse_typ_expr p in + match p.token with + | Rbrace -> + (* {...x}, spread without extra fields *) + Parser.next p; + let spread_field_name = + Location.mkloc "..." (mk_loc dotdotdot_start dotdotdot_end) + in + let spread_field_loc = + mk_loc start_pos spread_typ.ptyp_loc.loc_end + in + let spread_field = + Ast_helper.Type.field ~attrs:[] ~loc:spread_field_loc + ~mut:Asttypes.Immutable spread_field_name spread_typ + in + Parser.optional p Comma |> ignore; + Parser.expect Rparen p; + Parsetree.Pcstr_record [spread_field] + | _ -> ( + let res = + parse_spread_tail_classified ~start_pos ~spread_typ + ~grammar:Grammar.FieldDeclarations p + in + match res with + | `Record fields -> + let spread_field_name = + Location.mkloc "..." (mk_loc dotdotdot_start dotdotdot_end) + in + let spread_field_loc = + mk_loc start_pos spread_typ.ptyp_loc.loc_end + in + let spread_field = + Ast_helper.Type.field ~attrs:[] ~loc:spread_field_loc + ~mut:Asttypes.Immutable spread_field_name spread_typ + in + Parser.optional p Comma |> ignore; + Parser.expect Rparen p; + Parsetree.Pcstr_record (spread_field :: fields) + | `Object typ -> + Parser.optional p Comma |> ignore; + let more_args = + parse_comma_delimited_region ~grammar:Grammar.TypExprList + ~closing:Rparen ~f:parse_typ_expr_region p + in + Parser.expect Rparen p; + Parsetree.Pcstr_tuple (typ :: more_args))) | _ -> ( let attrs = parse_attributes p in match p.Parser.token with @@ -5435,6 +5442,47 @@ and parse_type_equation_or_constr_decl p = (* TODO: is this a good idea? *) (None, Asttypes.Public, Parsetree.Ptype_abstract) +and parse_spread_tail_classified ?current_type_name_path ?inline_types_context + ~start_pos ~spread_typ ~grammar p = + match p.token with + | Rbrace -> + (* `{...t}` no extra fields: treat as record without tail fields *) + Parser.next p; + `Record [] + | _ -> + Parser.expect Comma p; + let found_object_field = ref false in + let (fields : Parsetree.label_declaration list) = + parse_comma_delimited_region ~grammar ~closing:Rbrace + ~f: + (parse_field_declaration_region ?current_type_name_path + ?inline_types_context ~found_object_field) + p + in + Parser.expect Rbrace p; + if !found_object_field then + (* Object-style: build an object type that inherits the spread *) + let obj_fields = + let convert (ld : Parsetree.label_declaration) = + let ({Parsetree.pld_name; pld_type; pld_attributes; _} + : Parsetree.label_declaration) = + ld + in + match pld_name.txt with + | "..." -> Parsetree.Oinherit pld_type + | _ -> Otag (pld_name, pld_attributes, pld_type) + in + Parsetree.Oinherit spread_typ :: List.map convert fields + in + let loc = mk_loc start_pos p.prev_end_pos in + let typ = + Ast_helper.Typ.object_ ~loc obj_fields Asttypes.Closed + |> parse_type_alias p + in + let typ = parse_arrow_type_rest ~es6_arrow:true ~start_pos typ p in + `Object typ + else `Record fields + and parse_record_or_object_decl ?current_type_name_path ?inline_types_context p = let start_pos = p.Parser.start_pos in diff --git a/tests/build_tests/super_errors/expected/mix_object_record_spread.res.expected b/tests/build_tests/super_errors/expected/mix_object_record_spread.res.expected new file mode 100644 index 0000000000..ebc7705603 --- /dev/null +++ b/tests/build_tests/super_errors/expected/mix_object_record_spread.res.expected @@ -0,0 +1,12 @@ + + We've found a bug for you! + /.../fixtures/mix_object_record_spread.res:5:3-15 + + 3 │ type props = { + 4 │ ...baseProps, + 5 │ label: string, + 6 │ } + 7 │ + + You cannot mix a record field with an object type spread. + Remove the record field or change it to an object field (e.g. "label": ...). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/mix_object_record_spread_constructor.res.expected b/tests/build_tests/super_errors/expected/mix_object_record_spread_constructor.res.expected new file mode 100644 index 0000000000..87759f9d10 --- /dev/null +++ b/tests/build_tests/super_errors/expected/mix_object_record_spread_constructor.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/mix_object_record_spread_constructor.res:3:21-33 + + 1 │ type obj = {"name": string} + 2 │ + 3 │ type t = V({...obj, label: string}) + 4 │ + + You cannot mix a record field with an object type spread. + Remove the record field or change it to an object field (e.g. "label": ...). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/mix_record_object_spread.res.expected b/tests/build_tests/super_errors/expected/mix_record_object_spread.res.expected new file mode 100644 index 0000000000..a3354bd70f --- /dev/null +++ b/tests/build_tests/super_errors/expected/mix_record_object_spread.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/mix_record_object_spread.res:3:18-26 + + 1 │ type baseProps = {name: string} + 2 │ + 3 │ type props = {...baseProps, "label": string} + 4 │ + + The type baseProps is not an object type \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/mix_object_record_spread.res b/tests/build_tests/super_errors/fixtures/mix_object_record_spread.res new file mode 100644 index 0000000000..2ec5288929 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/mix_object_record_spread.res @@ -0,0 +1,11 @@ +type baseProps = {"name": string} + +type props = { + ...baseProps, + label: string, +} + +let label: props = { + "name": "hello", + "label": "label", +} diff --git a/tests/build_tests/super_errors/fixtures/mix_object_record_spread_constructor.res b/tests/build_tests/super_errors/fixtures/mix_object_record_spread_constructor.res new file mode 100644 index 0000000000..910c48feca --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/mix_object_record_spread_constructor.res @@ -0,0 +1,3 @@ +type obj = {"name": string} + +type t = V({...obj, label: string}) diff --git a/tests/build_tests/super_errors/fixtures/mix_record_object_spread.res b/tests/build_tests/super_errors/fixtures/mix_record_object_spread.res new file mode 100644 index 0000000000..3b4c53e5ed --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/mix_record_object_spread.res @@ -0,0 +1,3 @@ +type baseProps = {name: string} + +type props = {...baseProps, "label": string} diff --git a/tests/syntax_tests/data/parsing/errors/other/expected/spread.res.txt b/tests/syntax_tests/data/parsing/errors/other/expected/spread.res.txt index adcec38799..2b33d97dbc 100644 --- a/tests/syntax_tests/data/parsing/errors/other/expected/spread.res.txt +++ b/tests/syntax_tests/data/parsing/errors/other/expected/spread.res.txt @@ -54,18 +54,6 @@ Solution: you need to pull out each field you want explicitly. List pattern matches only supports one `...` spread, at the end. Explanation: a list spread at the tail is efficient, but a spread in the middle would create new lists; out of performance concern, our pattern matching currently guarantees to never create new intermediate data. - - Syntax error! - syntax_tests/data/parsing/errors/other/spread.res:9:20 - - 7 │ - 8 │ type t = {...a} - 9 │ type t = Foo({...a}) - 10 │ type t = option - 11 │ - - I'm not sure what to parse here when looking at ")". - let [|arr;_|] = [|1;2;3|] let record = { x with y } let { x; y } = myRecord @@ -73,5 +61,6 @@ let x::y = myList type nonrec t = { ...: a } type nonrec t = - | Foo of < a > + | Foo of { + ...: a } type nonrec t = (foo, < x > ) option \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/errors/typexpr/expected/objectSpread.res.txt b/tests/syntax_tests/data/parsing/errors/typexpr/expected/objectSpread.res.txt index bd46eeae3c..99c4b518a8 100644 --- a/tests/syntax_tests/data/parsing/errors/typexpr/expected/objectSpread.res.txt +++ b/tests/syntax_tests/data/parsing/errors/typexpr/expected/objectSpread.res.txt @@ -1,16 +1,4 @@ - Syntax error! - syntax_tests/data/parsing/errors/typexpr/objectSpread.res:5:16-18 - - 3 │ type u = private {...a, u: int} - 4 │ - 5 │ type x = Type({...a, u: int}) - 6 │ - 7 │ type u = {...a, "u": int, v: int} - - A record type declaration doesn't support the ... spread. Only an object (with quoted field names) does. - - Syntax error! syntax_tests/data/parsing/errors/typexpr/objectSpread.res:9:14 @@ -28,6 +16,8 @@ type nonrec u = private { ...: a ; u: int } type nonrec x = - | Type of < a ;u: int > + | Type of { + ...: a ; + u: int } type nonrec u = < a ;u: int ;v: int > let f [arity:1](x : < a: int ;b: int > ) = () \ No newline at end of file diff --git a/tests/tests/src/object_type_spreads.mjs b/tests/tests/src/object_type_spreads.mjs new file mode 100644 index 0000000000..f95e52ba13 --- /dev/null +++ b/tests/tests/src/object_type_spreads.mjs @@ -0,0 +1,22 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + + +let a = { + TAG: "One", + _0: { + one: "1", + two: 2 + } +}; + +let b = { + TAG: "Two", + one: "1", + two: 2 +}; + +export { + a, + b, +} +/* No side effect */ diff --git a/tests/tests/src/object_type_spreads.res b/tests/tests/src/object_type_spreads.res new file mode 100644 index 0000000000..a55f543429 --- /dev/null +++ b/tests/tests/src/object_type_spreads.res @@ -0,0 +1,11 @@ +type a = {"one": string, "two": int} + +type b = { + one: string, + two: int, +} + +type variant = One({...a}) | Two({...b}) + +let a = One({"one": "1", "two": 2}) +let b = Two({one: "1", two: 2}) diff --git a/tests/tests/src/record_type_spread.mjs b/tests/tests/src/record_type_spread.mjs index e13971b417..8e2c6ecadd 100644 --- a/tests/tests/src/record_type_spread.mjs +++ b/tests/tests/src/record_type_spread.mjs @@ -41,6 +41,30 @@ let x = { c: "hello" }; +let v1 = { + TAG: "One", + a: "", + b: 1, + c: undefined, + d: undefined +}; + +let v2 = { + TAG: "Two", + a: "", + b: "1", + c: undefined, + d: undefined +}; + +let v3 = { + TAG: "Three", + a: "", + b: true, + c: undefined, + d: undefined +}; + export { getY, getX, @@ -48,5 +72,8 @@ export { d, x, DeepSub, + v1, + v2, + v3, } /* No side effect */ diff --git a/tests/tests/src/record_type_spread.res b/tests/tests/src/record_type_spread.res index ee373fb1ca..50c963bbb8 100644 --- a/tests/tests/src/record_type_spread.res +++ b/tests/tests/src/record_type_spread.res @@ -59,3 +59,9 @@ module DeepSub = { z: #Two(1), } } + +type variant<'f> = One({...f}) | Two({...f}) | Three({...f<'f>}) + +let v1 = One({a: "", b: 1, c: None, d: None}) +let v2 = Two({a: "", b: "1", c: None, d: None}) +let v3 = Three({a: "", b: true, c: None, d: None})