Skip to content

Commit 60cf75a

Browse files
authored
Exploring variant-to-variant coercion (rescript-lang#6314)
* sketch out variant-to-variant coercion * reuse logic from ast_untagged_variants * reuse more logic * handle inline records in variant coercion * fix false positive in variant to primitive branch, and add tests * changelog * format * reuse more logic from ast_untagged * remove unused
1 parent 48e4372 commit 60cf75a

18 files changed

+237
-86
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
> - :nail_care: [Polish]
1212
1313
# 11.0.0-beta.4 (Unreleased)
14+
#### :rocket: New Feature
15+
- Variants: Allow coercing from variant to variant, where applicable. https://github.com/rescript-lang/rescript-compiler/pull/6314
1416

1517
# 11.0.0-beta.3
1618

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/variant_to_variant_coercion.res:6:10-15
4+
5+
4 │ let x: x = One(true)
6+
5 │
7+
6 │ let y = (x :> y)
8+
7 │
9+
10+
Type x is not a subtype of y
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/variant_to_variant_coercion_as.res:6:10-15
4+
5+
4 │ let x: x = One(true)
6+
5 │
7+
6 │ let y = (x :> y)
8+
7 │
9+
10+
Type x is not a subtype of y
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/variant_to_variant_coercion_tag.res:6:10-15
4+
5+
4 │ let x: x = One(true)
6+
5 │
7+
6 │ let y = (x :> y)
8+
7 │
9+
10+
Type x is not a subtype of y
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/variant_to_variant_coercion_unboxed.res:6:10-15
4+
5+
4 │ let x: x = One(true)
6+
5 │
7+
6 │ let y = (x :> y)
8+
7 │
9+
10+
Type x is not a subtype of y
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type x = One(bool) | Two
2+
type y = One(string) | Two
3+
4+
let x: x = One(true)
5+
6+
let y = (x :> y)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type x = | @as("one") One(bool) | Two(string)
2+
type y = One(bool) | Two(string)
3+
4+
let x: x = One(true)
5+
6+
let y = (x :> y)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@tag("kind") type x = One(bool) | Two(string)
2+
type y = One(bool) | Two(string)
3+
4+
let x: x = One(true)
5+
6+
let y = (x :> y)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@unboxed type x = One(bool) | Two
2+
type y = One(bool) | Two
3+
4+
let x: x = One(true)
5+
6+
let y = (x :> y)

jscomp/core/matching_polyfill.ml

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
* along with this program; if not, write to the Free Software
2323
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *)
2424

25+
let () = Ast_untagged_variants.extract_concrete_typedecl := Ctype.extract_concrete_typedecl
26+
2527
let names_from_construct_pattern (pat : Typedtree.pattern) =
2628
let rec resolve_path n (path : Path.t) =
2729
match Env.find_type path pat.pat_env with

jscomp/ml/ast_uncurried.ml

+1-6
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,7 @@ let coreTypeIsUncurriedFun (typ : Parsetree.core_type) =
6969
true
7070
| _ -> false
7171

72-
let typeIsUncurriedFun (typ : Types.type_expr) =
73-
match typ.desc with
74-
| Tconstr (Pident {name = "function$"}, [{desc = Tarrow _}; _], _) ->
75-
true
76-
| _ -> false
77-
72+
let typeIsUncurriedFun = Ast_uncurried_utils.typeIsUncurriedFun
7873

7974
let typeExtractUncurriedFun (typ : Parsetree.core_type) =
8075
match typ.ptyp_desc with

jscomp/ml/ast_uncurried_utils.ml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
let typeIsUncurriedFun (typ : Types.type_expr) =
2+
match typ.desc with
3+
| Tconstr (Pident {name = "function$"}, [{desc = Tarrow _}; _], _) ->
4+
true
5+
| _ -> false

jscomp/ml/ast_untagged_variants.ml

+6-2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ let process_untagged (attrs : Parsetree.attributes) =
7777
| _ -> ());
7878
!st
7979

80+
let extract_concrete_typedecl: (Env.t ->
81+
Types.type_expr ->
82+
Path.t * Path.t * Types.type_declaration) ref = ref (Obj.magic ())
83+
8084
let process_tag_type (attrs : Parsetree.attributes) =
8185
let st : tag_type option ref = ref None in
8286
Ext_list.iter attrs (fun ({txt; loc}, payload) ->
@@ -137,7 +141,7 @@ let get_block_type ~env (cstr : Types.constructor_declaration) :
137141
when Path.same path Predef.path_array ->
138142
Some ArrayType
139143
| true, Cstr_tuple [({desc = Tconstr _} as t)]
140-
when Ast_uncurried.typeIsUncurriedFun t ->
144+
when Ast_uncurried_utils.typeIsUncurriedFun t ->
141145
Some FunctionType
142146
| true, Cstr_tuple [{desc = Tarrow _}] -> Some FunctionType
143147
| true, Cstr_tuple [{desc = Tconstr (path, _, _)}]
@@ -148,7 +152,7 @@ let get_block_type ~env (cstr : Types.constructor_declaration) :
148152
Some ObjectType
149153
| true, Cstr_tuple [ty] -> (
150154
let default = Some UnknownType in
151-
match Ctype.extract_concrete_typedecl env ty with
155+
match !extract_concrete_typedecl env ty with
152156
| _, _, {type_kind = Type_record (_, Record_unboxed _)} -> default
153157
| _, _, {type_kind = Type_record (_, _)} -> Some ObjectType
154158
| _ -> default

jscomp/ml/ctype.ml

+44-29
Original file line numberDiff line numberDiff line change
@@ -3952,18 +3952,55 @@ let rec subtype_rec env trace t1 t2 cstrs =
39523952
| (Tconstr(p1, _, _), _) when generic_private_abbrev env p1 ->
39533953
subtype_rec env trace (expand_abbrev_opt env t1) t2 cstrs
39543954
| (Tconstr(_, [], _), Tconstr(path, [], _)) when Variant_coercion.can_coerce_path path &&
3955-
extract_concrete_typedecl env t1 |> Variant_coercion.is_variant_typedecl |> Option.is_some
3956-
->
3957-
(* type coercion for variants *)
3958-
(match Variant_coercion.is_variant_typedecl (extract_concrete_typedecl env t1) with
3955+
extract_concrete_typedecl env t1 |> Variant_coercion.can_try_coerce_variant_to_primitive |> Option.is_some
3956+
->
3957+
(* type coercion for variants to primitives *)
3958+
(match Variant_coercion.can_try_coerce_variant_to_primitive (extract_concrete_typedecl env t1) with
39593959
| Some constructors ->
39603960
if constructors |> Variant_coercion.can_coerce_variant ~path then
39613961
cstrs
39623962
else
39633963
(trace, t1, t2, !univar_pairs)::cstrs
39643964
| None -> (trace, t1, t2, !univar_pairs)::cstrs)
3965-
| (Tconstr(_, [], _), Tconstr(_, [], _)) -> (* type coercion for records *)
3965+
| (Tconstr(_, [], _), Tconstr(_, [], _)) -> (* type coercion for variants and records *)
39663966
(match extract_concrete_typedecl env t1, extract_concrete_typedecl env t2 with
3967+
| (_, _, {type_kind=Type_variant (c1); type_attributes=t1attrs}), (_, _, {type_kind=Type_variant (c2); type_attributes=t2attrs}) ->
3968+
if
3969+
Variant_coercion.variant_configuration_can_be_coerced t1attrs t2attrs = false
3970+
then
3971+
(trace, t1, t2, !univar_pairs)::cstrs
3972+
else
3973+
let c1_len = List.length c1 in
3974+
if c1_len > List.length c2 then (trace, t1, t2, !univar_pairs)::cstrs
3975+
else
3976+
let constructor_map = Hashtbl.create c1_len in
3977+
c2
3978+
|> List.iter (fun (c : Types.constructor_declaration) ->
3979+
Hashtbl.add constructor_map (Ident.name c.cd_id) c);
3980+
if c1 |> List.for_all (fun (c : Types.constructor_declaration) ->
3981+
match (c, Hashtbl.find_opt constructor_map (Ident.name c.cd_id)) with
3982+
| ( {Types.cd_args = Cstr_record fields1; cd_attributes=c1_attributes},
3983+
Some {Types.cd_args = Cstr_record fields2; cd_attributes=c2_attributes} ) ->
3984+
if Variant_coercion.variant_representation_matches c1_attributes c2_attributes then
3985+
let violation, tl1, tl2 = Record_coercion.check_record_fields fields1 fields2 in
3986+
if violation then false
3987+
else
3988+
begin try
3989+
let lst = subtype_list env trace tl1 tl2 cstrs in
3990+
List.length lst = List.length cstrs
3991+
with | _ -> false end
3992+
else false
3993+
| ( {Types.cd_args = Cstr_tuple tl1; cd_attributes=c1_attributes},
3994+
Some {Types.cd_args = Cstr_tuple tl2; cd_attributes=c2_attributes} ) ->
3995+
if Variant_coercion.variant_representation_matches c1_attributes c2_attributes then
3996+
begin try
3997+
let lst = subtype_list env trace tl1 tl2 cstrs in
3998+
List.length lst = List.length cstrs
3999+
with | _ -> false end
4000+
else false
4001+
| _ -> false)
4002+
then cstrs
4003+
else (trace, t1, t2, !univar_pairs)::cstrs
39674004
| (_, _, {type_kind=Type_record (fields1, repr1)}), (_, _, {type_kind=Type_record (fields2, repr2)}) ->
39684005
let same_repr = match repr1, repr2 with
39694006
| (Record_regular | Record_optional_labels _), (Record_regular | Record_optional_labels _) ->
@@ -3973,30 +4010,8 @@ let rec subtype_rec env trace t1 t2 cstrs =
39734010
| Record_extension, Record_extension -> true
39744011
| _ -> false in
39754012
if same_repr then
3976-
let field_is_optional id repr = match repr with
3977-
| Record_optional_labels lbls -> List.mem (Ident.name id) lbls
3978-
| _ -> false in
3979-
let violation = ref false in
3980-
let label_decl_sub (acc1, acc2) ld2 =
3981-
match Ext_list.find_first fields1 (fun ld1 -> ld1.ld_id.name = ld2.ld_id.name) with
3982-
| Some ld1 ->
3983-
if field_is_optional ld1.ld_id repr1 <> (field_is_optional ld2.ld_id repr2) then
3984-
(* optional field can't be modified *)
3985-
violation := true;
3986-
let get_as (({txt}, payload) : Parsetree.attribute) =
3987-
if txt = "as" then Ast_payload.is_single_string payload
3988-
else None in
3989-
let get_as_name ld = match Ext_list.filter_map ld.ld_attributes get_as with
3990-
| [] -> ld.ld_id.name
3991-
| (s,_)::_ -> s in
3992-
if get_as_name ld1 <> get_as_name ld2 then violation := true;
3993-
ld1.ld_type :: acc1, ld2.ld_type :: acc2
3994-
| None ->
3995-
(* field must be present *)
3996-
violation := true;
3997-
(acc1, acc2) in
3998-
let tl1, tl2 = List.fold_left label_decl_sub ([], []) fields2 in
3999-
if !violation
4013+
let violation, tl1, tl2 = Record_coercion.check_record_fields ~repr1 ~repr2 fields1 fields2 in
4014+
if violation
40004015
then (trace, t1, t2, !univar_pairs)::cstrs
40014016
else
40024017
subtype_list env trace tl1 tl2 cstrs

jscomp/ml/record_coercion.ml

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
let check_record_fields ?repr1 ?repr2 (fields1 : Types.label_declaration list)
2+
(fields2 : Types.label_declaration list) =
3+
let field_is_optional id repr =
4+
match repr with
5+
| Some (Types.Record_optional_labels lbls) -> List.mem (Ident.name id) lbls
6+
| _ -> false
7+
in
8+
let violation = ref false in
9+
let label_decl_sub (acc1, acc2) (ld2 : Types.label_declaration) =
10+
match
11+
Ext_list.find_first fields1 (fun ld1 -> ld1.ld_id.name = ld2.ld_id.name)
12+
with
13+
| Some ld1 ->
14+
if field_is_optional ld1.ld_id repr1 <> field_is_optional ld2.ld_id repr2
15+
then (* optional field can't be modified *)
16+
violation := true;
17+
let get_as (({txt}, payload) : Parsetree.attribute) =
18+
if txt = "as" then Ast_payload.is_single_string payload else None
19+
in
20+
let get_as_name (ld : Types.label_declaration) =
21+
match Ext_list.filter_map ld.ld_attributes get_as with
22+
| [] -> ld.ld_id.name
23+
| (s, _) :: _ -> s
24+
in
25+
if get_as_name ld1 <> get_as_name ld2 then violation := true;
26+
(ld1.ld_type :: acc1, ld2.ld_type :: acc2)
27+
| None ->
28+
(* field must be present *)
29+
violation := true;
30+
(acc1, acc2)
31+
in
32+
let tl1, tl2 = List.fold_left label_decl_sub ([], []) fields2 in
33+
(!violation, tl1, tl2)

jscomp/ml/variant_coercion.ml

+52-49
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,64 @@
1-
let find_as_attribute_payload (attributes : Parsetree.attribute list) =
2-
attributes
3-
|> List.find_map (fun (attr : Parsetree.attribute) ->
4-
match attr with
5-
| {txt = "as"}, payload -> Some payload
6-
| _ -> None)
7-
81
(* TODO: Improve error messages? Say why we can't coerce. *)
92

10-
let check_constructors (constructors : Types.constructor_declaration list) check
11-
=
12-
List.for_all
13-
(fun (c : Types.constructor_declaration) ->
14-
check c.cd_args (find_as_attribute_payload c.cd_attributes))
15-
constructors
16-
17-
let can_coerce_to_string (constructors : Types.constructor_declaration list) =
18-
check_constructors constructors (fun args payload ->
19-
match (args, payload) with
20-
| Cstr_tuple [], None -> true
21-
| Cstr_tuple [], Some payload
22-
when Ast_payload.is_single_string payload |> Option.is_some ->
23-
true
24-
| _ -> false)
25-
26-
let can_coerce_to_int (constructors : Types.constructor_declaration list) =
27-
check_constructors constructors (fun args payload ->
28-
match (args, payload) with
29-
| Cstr_tuple [], Some payload
30-
when Ast_payload.is_single_int payload |> Option.is_some ->
31-
true
32-
| _ -> false)
33-
34-
let can_coerce_to_float (constructors : Types.constructor_declaration list) =
35-
check_constructors constructors (fun args payload ->
36-
match (args, payload) with
37-
| Cstr_tuple [], Some payload
38-
when Ast_payload.is_single_float payload |> Option.is_some ->
39-
true
40-
| _ -> false)
41-
3+
(* Right now we only allow coercing to primitives string/int/float *)
424
let can_coerce_path (path : Path.t) =
435
Path.same path Predef.path_string
446
|| Path.same path Predef.path_int
457
|| Path.same path Predef.path_float
468

479
let can_coerce_variant ~(path : Path.t)
4810
(constructors : Types.constructor_declaration list) =
49-
if Path.same path Predef.path_string && can_coerce_to_string constructors then
50-
true
51-
else if Path.same path Predef.path_int && can_coerce_to_int constructors then
52-
true
53-
else if Path.same path Predef.path_float && can_coerce_to_float constructors
54-
then true
55-
else false
11+
constructors
12+
|> List.for_all (fun (c : Types.constructor_declaration) ->
13+
let args = c.cd_args in
14+
let payload = Ast_untagged_variants.process_tag_type c.cd_attributes in
15+
match args with
16+
| Cstr_tuple [] -> (
17+
match payload with
18+
| None | Some (String _) -> Path.same path Predef.path_string
19+
| Some (Int _) -> Path.same path Predef.path_int
20+
| Some (Float _) -> Path.same path Predef.path_float
21+
| Some (Null | Undefined | Bool _ | Untagged _) -> false)
22+
| _ -> false)
5623

57-
let is_variant_typedecl
58-
((_, _, typedecl) : Path.t * Path.t * Types.type_declaration) =
24+
let can_try_coerce_variant_to_primitive
25+
((_, p, typedecl) : Path.t * Path.t * Types.type_declaration) =
5926
match typedecl with
60-
| {type_kind = Type_variant constructors} -> Some constructors
27+
| {type_kind = Type_variant constructors; type_params = []}
28+
when Path.name p <> "bool" ->
29+
(* bool is represented as a variant internally, so we need to account for that *)
30+
Some constructors
6131
| _ -> None
32+
33+
let variant_representation_matches (c1_attrs : Parsetree.attributes)
34+
(c2_attrs : Parsetree.attributes) =
35+
match
36+
( Ast_untagged_variants.process_tag_type c1_attrs,
37+
Ast_untagged_variants.process_tag_type c2_attrs )
38+
with
39+
| None, None -> true
40+
| Some s1, Some s2 when s1 = s2 -> true
41+
| _ -> false
42+
43+
let variant_configuration_can_be_coerced (a1 : Parsetree.attributes)
44+
(a2 : Parsetree.attributes) =
45+
let unboxed =
46+
match
47+
( Ast_untagged_variants.process_untagged a1,
48+
Ast_untagged_variants.process_untagged a2 )
49+
with
50+
| true, true | false, false -> true
51+
| _ -> false
52+
in
53+
if not unboxed then false
54+
else
55+
let tag =
56+
match
57+
( Ast_untagged_variants.process_tag_name a1,
58+
Ast_untagged_variants.process_tag_name a2 )
59+
with
60+
| Some tag1, Some tag2 when tag1 = tag2 -> true
61+
| None, None -> true
62+
| _ -> false
63+
in
64+
if not tag then false else true

0 commit comments

Comments
 (0)