Skip to content

Commit df1bc3d

Browse files
authored
Variant type spreads (#6316)
* parse ... in variants * initial implementation of expanding variant type spreads * transfer attributes * comments and clarifications * ensure that variant spreads are compliant with the underlying variants runtime configuration, and get some basic error reporting going * proper error reporting for non-discoverable types * report errors on duplicate constructors in spreads * error on type parameters in variant type spreads * make inline records work when spreading variants
1 parent 9040d1e commit df1bc3d

24 files changed

+540
-23
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/variant_spread_duplicate_constructors.res:3:22
4+
5+
1 │ type a = One | Two
6+
2 │ type b = Two | Three
7+
3 │ type c = | ...a | ...b | Four
8+
4 │
9+
10+
Variant b has a constructor named Two, but a constructor named Two already exists in the variant it's spread into.
11+
You cannot spread variants with overlapping constructors.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/variant_spread_inline_records.res:4:16-30
4+
5+
2 │ type b = | ...a | Three
6+
3 │
7+
4 │ let b: b = One({name: "hello"})
8+
9+
Some required record fields are missing:
10+
age. If this is a component, add the missing props.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/variant_spread_recursive.res:1:65
4+
5+
1 │ type rec a = One | Two | Three and b = Four | Five and c = | ...a | ...b
6+
2 │
7+
8+
This type could not be found. It's only possible to spread variants that are known as the spread happens. This means for example that you can't spread variants in recursive definitions.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/variant_spread_tag_missing.res:2:15
4+
5+
1 │ @tag("kind") type a = One(int) | Two(string)
6+
2 │ type b = | ...a | Three(bool)
7+
3 │
8+
9+
The @tag attribute does not match for this variant and the variant where this is spread. Both variants must have the same @tag attribute configuration, or no @tag attribute at all.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/variant_spread_tag_value_mismatch.res:2:28
4+
5+
1 │ @tag("kind") type a = One(int) | Two(string)
6+
2 │ @tag("name") type b = | ...a | Three(bool)
7+
3 │
8+
9+
The @tag attribute does not match for this variant and the variant where this is spread. Both variants must have the same @tag attribute configuration, or no @tag attribute at all.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/variant_spread_type_parameters.res:2:15
4+
5+
1 │ type a<'a> = One | Two('a)
6+
2 │ type b = | ...a<int> | Three
7+
8+
Type parameters are not supported in variant type spreads.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/variant_spread_unboxed_mismatch.res:2:15
4+
5+
1 │ @unboxed type a = One(int) | Two(string)
6+
2 │ type b = | ...a | Three(bool)
7+
3 │
8+
9+
This variant is unboxed, but the variant where this is spread is not. Both variants unboxed configuration must match.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
type a = One | Two
2+
type b = Two | Three
3+
type c = | ...a | ...b | Four
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type a = One({name: string, age: int}) | Two
2+
type b = | ...a | Three
3+
4+
let b: b = One({name: "hello"})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
type rec a = One | Two | Three and b = Four | Five and c = | ...a | ...b
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@tag("kind") type a = One(int) | Two(string)
2+
type b = | ...a | Three(bool)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@tag("kind") type a = One(int) | Two(string)
2+
@tag("name") type b = | ...a | Three(bool)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
type a<'a> = One | Two('a)
2+
type b = | ...a<int> | Three
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@unboxed type a = One(int) | Two(string)
2+
type b = | ...a | Three(bool)

jscomp/ml/typedecl.ml

+134-17
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ type error =
5353
| Bad_unboxed_attribute of string
5454
| Boxed_and_unboxed
5555
| Nonrec_gadt
56+
| Variant_runtime_representation_mismatch of Variant_coercion.variant_error
57+
| Variant_spread_fail of Variant_type_spread.variant_type_spread_error
5658

5759
open Typedtree
5860

@@ -379,34 +381,109 @@ let transl_declaration ~typeRecordAsObject env sdecl id =
379381
let copy_tag_attr_from_decl attr =
380382
let tag_attrs = Ext_list.filter sdecl.ptype_attributes (fun ({txt}, _) -> txt = "tag" || txt = Ast_untagged_variants.untagged) in
381383
if tag_attrs = [] then attr else tag_attrs @ attr in
384+
let constructors_from_variant_spreads = Hashtbl.create 10 in
382385
let make_cstr scstr =
383386
let name = Ident.create scstr.pcd_name.txt in
384387
let targs, tret_type, args, ret_type, _cstr_params =
385388
make_constructor env (Path.Pident id) params
386389
scstr.pcd_args scstr.pcd_res
387390
in
388-
let tcstr =
389-
{ cd_id = name;
390-
cd_name = scstr.pcd_name;
391-
cd_args = targs;
392-
cd_res = tret_type;
393-
cd_loc = scstr.pcd_loc;
394-
cd_attributes = scstr.pcd_attributes |> copy_tag_attr_from_decl }
395-
in
396-
let cstr =
397-
{ Types.cd_id = name;
398-
cd_args = args;
399-
cd_res = ret_type;
400-
cd_loc = scstr.pcd_loc;
401-
cd_attributes = scstr.pcd_attributes |> copy_tag_attr_from_decl }
402-
in
391+
if String.starts_with scstr.pcd_name.txt ~prefix:"..." then (
392+
(* Any constructor starting with "..." represents a variant type spread, and
393+
will have the spread variant itself as a single argument.
394+
395+
We pull that variant type out, and then track the type of each of its
396+
constructors, so that we can replace our dummy constructors added before
397+
type checking with the realtypes for each constructor.
398+
*)
399+
(match args with
400+
| Cstr_tuple [spread_variant] -> (
401+
match Ctype.extract_concrete_typedecl env spread_variant with
402+
| (_, _, {type_kind=Type_variant constructors}) -> (
403+
constructors |> List.iter(fun (c: Types.constructor_declaration) ->
404+
Hashtbl.add constructors_from_variant_spreads c.cd_id.name c)
405+
)
406+
| _ -> ()
407+
)
408+
| _ -> ());
409+
None)
410+
else (
411+
(* Check if this constructor is from a variant spread. If so, we need to replace
412+
its type with the right type we've pulled from the type checked spread variant
413+
itself. *)
414+
let tcstr, cstr = match Hashtbl.find_opt constructors_from_variant_spreads (Ident.name name) with
415+
| Some cstr ->
416+
let tcstr =
417+
{
418+
cd_id = name;
419+
cd_name = scstr.pcd_name;
420+
cd_args =
421+
(match cstr.cd_args with
422+
| Cstr_tuple args ->
423+
Cstr_tuple
424+
(args
425+
|> List.map (fun texpr : Typedtree.core_type ->
426+
{
427+
ctyp_attributes = cstr.cd_attributes;
428+
ctyp_loc = cstr.cd_loc;
429+
ctyp_env = env;
430+
ctyp_type = texpr;
431+
ctyp_desc = Ttyp_any;
432+
(* This is fine because the type checker seems to only look at `ctyp_type` for type checking. *)
433+
}))
434+
| Cstr_record lbls ->
435+
Cstr_record
436+
(lbls
437+
|> List.map
438+
(fun (l : Types.label_declaration) : Typedtree.label_declaration
439+
->
440+
{
441+
ld_id = l.ld_id;
442+
ld_name = Location.mkloc (Ident.name l.ld_id) l.ld_loc;
443+
ld_mutable = l.ld_mutable;
444+
ld_type =
445+
{
446+
ctyp_desc = Ttyp_any;
447+
ctyp_type = l.ld_type;
448+
ctyp_env = env;
449+
ctyp_loc = l.ld_loc;
450+
ctyp_attributes = [];
451+
};
452+
ld_loc = l.ld_loc;
453+
ld_attributes = l.ld_attributes;
454+
})));
455+
cd_res = tret_type;
456+
(* This is also strictly wrong, but is fine because the type checker does not look at this field. *)
457+
cd_loc = scstr.pcd_loc;
458+
cd_attributes = scstr.pcd_attributes |> copy_tag_attr_from_decl;
459+
}
460+
in
403461
tcstr, cstr
462+
| None ->
463+
let tcstr =
464+
{ cd_id = name;
465+
cd_name = scstr.pcd_name;
466+
cd_args = targs;
467+
cd_res = tret_type;
468+
cd_loc = scstr.pcd_loc;
469+
cd_attributes = scstr.pcd_attributes |> copy_tag_attr_from_decl }
470+
in
471+
let cstr =
472+
{ Types.cd_id = name;
473+
cd_args = args;
474+
cd_res = ret_type;
475+
cd_loc = scstr.pcd_loc;
476+
cd_attributes = scstr.pcd_attributes |> copy_tag_attr_from_decl }
477+
in
478+
tcstr, cstr
479+
in Some (tcstr, cstr)
480+
)
404481
in
405482
let make_cstr scstr =
406483
Builtin_attributes.warning_scope scstr.pcd_attributes
407484
(fun () -> make_cstr scstr)
408485
in
409-
let tcstrs, cstrs = List.split (List.map make_cstr scstrs) in
486+
let tcstrs, cstrs = List.split (List.filter_map make_cstr scstrs) in
410487
let isUntaggedDef = Ast_untagged_variants.has_untagged sdecl.ptype_attributes in
411488
Ast_untagged_variants.check_well_formed ~env ~isUntaggedDef cstrs;
412489
Ttype_variant tcstrs, Type_variant cstrs, sdecl
@@ -1270,7 +1347,12 @@ let transl_type_decl env rec_flag sdecl_list =
12701347
{sdecl with
12711348
ptype_name; ptype_kind = Ptype_abstract; ptype_manifest = None})
12721349
fixed_types
1273-
@ sdecl_list
1350+
@ (try
1351+
sdecl_list |> Variant_type_spread.expand_variant_spreads env
1352+
with
1353+
| Variant_coercion.VariantConfigurationError ((VariantError {left_loc}) as err) -> raise(Error(left_loc, Variant_runtime_representation_mismatch err))
1354+
| Variant_type_spread.VariantTypeSpreadError (loc, err) -> raise(Error(loc, Variant_spread_fail err))
1355+
)
12741356
in
12751357

12761358
(* Create identifiers. *)
@@ -1324,6 +1406,7 @@ let transl_type_decl env rec_flag sdecl_list =
13241406
List.map2 transl_declaration sdecl_list (List.map id_slots id_list) in
13251407
let decls =
13261408
List.map (fun tdecl -> (tdecl.typ_id, tdecl.typ_type)) tdecls in
1409+
let sdecl_list = Variant_type_spread.expand_dummy_constructor_args sdecl_list decls in
13271410
current_slot := None;
13281411
(* Check for duplicates *)
13291412
check_duplicates sdecl_list;
@@ -2072,6 +2155,40 @@ let report_error ppf = function
20722155
| Nonrec_gadt ->
20732156
fprintf ppf
20742157
"@[GADT case syntax cannot be used in a 'nonrec' block.@]"
2158+
| Variant_runtime_representation_mismatch
2159+
(Variant_coercion.VariantError
2160+
{is_spread_context; error = Variant_coercion.Untagged {left_is_unboxed}})
2161+
->
2162+
let other_variant_text =
2163+
if is_spread_context then "the variant where this is spread"
2164+
else "the other variant"
2165+
in
2166+
fprintf ppf "@[%s.@]"
2167+
("This variant is "
2168+
^ (if left_is_unboxed then "unboxed" else "not unboxed")
2169+
^ ", but " ^ other_variant_text
2170+
^ " is not. Both variants unboxed configuration must match")
2171+
| Variant_runtime_representation_mismatch
2172+
(Variant_coercion.VariantError
2173+
{is_spread_context; error = Variant_coercion.TagName _}) ->
2174+
let other_variant_text =
2175+
if is_spread_context then "the variant where this is spread"
2176+
else "the other variant"
2177+
in
2178+
fprintf ppf "@[%s.@]"
2179+
("The @tag attribute does not match for this variant and "
2180+
^ other_variant_text
2181+
^ ". Both variants must have the same @tag attribute configuration, or no \
2182+
@tag attribute at all")
2183+
| Variant_spread_fail Variant_type_spread.CouldNotFindType ->
2184+
fprintf ppf "@[This type could not be found. It's only possible to spread variants that are known as the spread happens. This means for example that you can't spread variants in recursive definitions.@]"
2185+
| Variant_spread_fail Variant_type_spread.HasTypeParams ->
2186+
fprintf ppf "@[Type parameters are not supported in variant type spreads.@]"
2187+
| Variant_spread_fail Variant_type_spread.DuplicateConstructor
2188+
{variant_with_overlapping_constructor; overlapping_constructor_name} ->
2189+
fprintf ppf "@[Variant %s has a constructor named %s, but a constructor named %s already exists in the variant it's spread into.@ You cannot spread variants with overlapping constructors.@]"
2190+
variant_with_overlapping_constructor overlapping_constructor_name overlapping_constructor_name
2191+
20752192

20762193
let () =
20772194
Location.register_error_of_exn

jscomp/ml/variant_coercion.ml

+50
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ let variant_representation_matches (c1_attrs : Parsetree.attributes)
4040
| Some s1, Some s2 when s1 = s2 -> true
4141
| _ -> false
4242

43+
type variant_configuration_error =
44+
| Untagged of {left_is_unboxed: bool}
45+
| TagName of {left_tag: string option; right_tag: string option}
46+
47+
type variant_error =
48+
| VariantError of {
49+
left_loc: Location.t;
50+
right_loc: Location.t;
51+
error: variant_configuration_error;
52+
is_spread_context: bool;
53+
}
54+
55+
exception VariantConfigurationError of variant_error
56+
4357
let variant_configuration_can_be_coerced (a1 : Parsetree.attributes)
4458
(a2 : Parsetree.attributes) =
4559
let unboxed =
@@ -62,3 +76,39 @@ let variant_configuration_can_be_coerced (a1 : Parsetree.attributes)
6276
| _ -> false
6377
in
6478
if not tag then false else true
79+
80+
let variant_configuration_can_be_coerced_raises ~is_spread_context ~left_loc
81+
~right_loc ~(left_attributes : Parsetree.attributes)
82+
~(right_attributes : Parsetree.attributes) =
83+
(match
84+
( Ast_untagged_variants.process_untagged left_attributes,
85+
Ast_untagged_variants.process_untagged right_attributes )
86+
with
87+
| true, true | false, false -> ()
88+
| left, _right ->
89+
raise
90+
(VariantConfigurationError
91+
(VariantError
92+
{
93+
is_spread_context;
94+
left_loc;
95+
right_loc;
96+
error = Untagged {left_is_unboxed = left};
97+
})));
98+
99+
match
100+
( Ast_untagged_variants.process_tag_name left_attributes,
101+
Ast_untagged_variants.process_tag_name right_attributes )
102+
with
103+
| Some host_tag, Some spread_tag when host_tag = spread_tag -> ()
104+
| None, None -> ()
105+
| left_tag, right_tag ->
106+
raise
107+
(VariantConfigurationError
108+
(VariantError
109+
{
110+
is_spread_context;
111+
left_loc;
112+
right_loc;
113+
error = TagName {left_tag; right_tag};
114+
}))

0 commit comments

Comments
 (0)