Skip to content

Commit 83bfd76

Browse files
committed
record fields mismatch
1 parent 73ff695 commit 83bfd76

14 files changed

+283
-26
lines changed

compiler/ml/ctype.ml

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ type subtype_context =
9494
left_type_kind: type_kind;
9595
right_type_kind: type_kind;
9696
}
97+
| Record_fields_mismatch of {
98+
left_record_name: Path.t;
99+
right_record_name: Path.t;
100+
issues: Record_coercion.record_field_subtype_violation list;
101+
}
97102

98103
exception
99104
Subtype of
@@ -3762,9 +3767,9 @@ let rec subtype_rec env trace t1 t2 cstrs =
37623767
c2
37633768
|> List.iter (fun (c : Types.constructor_declaration) ->
37643769
Hashtbl.add constructor_map (Ident.name c.cd_id) c);
3765-
if
3770+
let field_subtype_violations =
37663771
c1
3767-
|> List.for_all (fun (c : Types.constructor_declaration) ->
3772+
|> List.filter_map (fun (c : Types.constructor_declaration) ->
37683773
match
37693774
( c,
37703775
Hashtbl.find_opt constructor_map (Ident.name c.cd_id)
@@ -3783,17 +3788,18 @@ let rec subtype_rec env trace t1 t2 cstrs =
37833788
Variant_coercion.variant_representation_matches
37843789
c1_attributes c2_attributes
37853790
then
3786-
(* TODO(subtype-errors) Inline record coercion check, piggy back on record coercion check *)
3787-
let violation, tl1, tl2 =
3791+
let violations, tl1, tl2 =
37883792
Record_coercion.check_record_fields fields1 fields2
37893793
in
3790-
if violation then false
3791-
else
3794+
match violations with
3795+
| [] -> (
37923796
try
37933797
let lst = subtype_list env trace tl1 tl2 cstrs in
3794-
List.length lst = List.length cstrs
3795-
with _ -> false
3796-
else false
3798+
if List.length lst = List.length cstrs then None
3799+
else Some [ (* TODO(subtype-errors) *) ]
3800+
with _ -> Some [ (* TODO(subtype-errors) *) ])
3801+
| violations -> Some violations
3802+
else Some [ (* TODO(subtype-errors) *) ]
37973803
| ( {
37983804
Types.cd_args = Cstr_tuple tl1;
37993805
cd_attributes = c1_attributes;
@@ -3809,14 +3815,16 @@ let rec subtype_rec env trace t1 t2 cstrs =
38093815
then
38103816
try
38113817
let lst = subtype_list env trace tl1 tl2 cstrs in
3812-
List.length lst = List.length cstrs
3813-
with _ -> false
3814-
else false
3815-
| _ -> false)
3816-
then cstrs
3818+
if List.length lst = List.length cstrs then None
3819+
else Some [ (* TODO(subtype-errors) *) ]
3820+
with _ -> Some [ (* TODO(subtype-errors) *) ]
3821+
else Some [ (* TODO(subtype-errors) *) ]
3822+
| _ -> Some [ (* TODO(subtype-errors) *) ])
3823+
in
3824+
if field_subtype_violations = [] then cstrs
38173825
else (trace, t1, t2, !univar_pairs, None) :: cstrs)
3818-
| ( (_, _, {type_kind = Type_record (fields1, repr1)}),
3819-
(_, _, {type_kind = Type_record (fields2, repr2)}) ) ->
3826+
| ( (p1, _, {type_kind = Type_record (fields1, repr1)}),
3827+
(p2, _, {type_kind = Type_record (fields2, repr2)}) ) ->
38203828
(* TODO(subtype-errors) Record representation *)
38213829
let same_repr =
38223830
match (repr1, repr2) with
@@ -3828,10 +3836,22 @@ let rec subtype_rec env trace t1 t2 cstrs =
38283836
| _ -> false
38293837
in
38303838
if same_repr then
3831-
let violation, tl1, tl2 =
3839+
let violations, tl1, tl2 =
38323840
Record_coercion.check_record_fields fields1 fields2
38333841
in
3834-
if violation then (trace, t1, t2, !univar_pairs, None) :: cstrs
3842+
if violations <> [] then
3843+
( trace,
3844+
t1,
3845+
t2,
3846+
!univar_pairs,
3847+
Some
3848+
(Record_fields_mismatch
3849+
{
3850+
left_record_name = p1;
3851+
right_record_name = p2;
3852+
issues = violations;
3853+
}) )
3854+
:: cstrs
38353855
else subtype_list env trace tl1 tl2 cstrs
38363856
else (trace, t1, t2, !univar_pairs, None) :: cstrs
38373857
| (p1, _, {type_kind = tk1}), (p2, _, {type_kind = tk2}) ->
@@ -3862,6 +3882,8 @@ let rec subtype_rec env trace t1 t2 cstrs =
38623882
| Tvariant {row_closed = true; row_fields}, Tconstr (_, [], _)
38633883
when extract_concrete_typedecl_opt env t2
38643884
|> Variant_coercion.type_is_variant -> (
3885+
(* TODO(subtype-errors) Polyvariant to variant *)
3886+
(* TODO(subtype-errors) Add Variant to polyvariant while we're at it? *)
38653887
match extract_concrete_typedecl env t2 with
38663888
| _, _, {type_kind = Type_variant variant_constructors; type_attributes}
38673889
-> (

compiler/ml/ctype.mli

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ type subtype_context =
4343
left_type_kind: type_kind;
4444
right_type_kind: type_kind;
4545
}
46+
| Record_fields_mismatch of {
47+
left_record_name: Path.t;
48+
right_record_name: Path.t;
49+
issues: Record_coercion.record_field_subtype_violation list;
50+
}
4651

4752
exception Unify of (type_expr * type_expr) list
4853
exception Tags of label * label

compiler/ml/printtyp.ml

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1642,6 +1642,51 @@ let print_variant_configuration_issue ppf
16421642
(Path.name right_variant_name)
16431643
(Path.name left_variant_name)
16441644

1645+
let print_record_field_subtype_violation ppf
1646+
(issue : Record_coercion.record_field_subtype_violation) ~left_record_name
1647+
~right_record_name =
1648+
match issue with
1649+
| Optional_mismatch {label; left_optional; right_optional} -> (
1650+
fprintf ppf "The field @{<info>%s@} " label;
1651+
match (left_optional, right_optional) with
1652+
| true, false ->
1653+
fprintf ppf
1654+
"is optional in record @{<info>%s@}, but is not optional in record \
1655+
@{<info>%s@}"
1656+
(Path.name left_record_name)
1657+
(Path.name right_record_name)
1658+
| false, true ->
1659+
fprintf ppf
1660+
"is not optional in record @{<info>%s@}, but is optional in record \
1661+
@{<info>%s@}"
1662+
(Path.name left_record_name)
1663+
(Path.name right_record_name)
1664+
| _ -> failwith "Invalid optional mismatch")
1665+
| Field_runtime_name_mismatch {label; left_as; right_as} ->
1666+
fprintf ppf "Field @{<info>%s@} runtime representation" label;
1667+
(match left_as with
1668+
| Some as_name ->
1669+
fprintf ppf
1670+
" is configured to be @{<info>\"%s\"@} (via the @as attribute)" as_name
1671+
| None -> fprintf ppf " is @{<info>\"%s\"@}" label);
1672+
fprintf ppf " in record @{<info>%s@}, but in record @{<info>%s@}"
1673+
(Path.name right_record_name)
1674+
(Path.name left_record_name);
1675+
(match right_as with
1676+
| Some as_name ->
1677+
fprintf ppf
1678+
" it is configured to be @{<info>\"%s\"@} (via the @as attribute)."
1679+
as_name
1680+
| None -> fprintf ppf " it is @{<info>\"%s\"@}." label);
1681+
fprintf ppf " Runtime representations must match."
1682+
| Field_missing {label} ->
1683+
fprintf ppf
1684+
"The field @{<info>%s@} is missing in record @{<info>%s@}, but present \
1685+
in record @{<info>%s@}"
1686+
label
1687+
(Path.name right_record_name)
1688+
(Path.name left_record_name)
1689+
16451690
let report_subtyping_error ppf env tr1 txt1 tr2 ctx =
16461691
wrap_printing_env env (fun () ->
16471692
reset ();
@@ -1703,7 +1748,20 @@ let report_subtyping_error ppf env tr1 txt1 tr2 ctx =
17031748
fprintf ppf "@ - @{<info>%s@} is %s" (Path.name left_typename)
17041749
(type_kind_to_string left_type_kind);
17051750
fprintf ppf "@ - @{<info>%s@} is %s" (Path.name right_typename)
1706-
(type_kind_to_string right_type_kind))
1751+
(type_kind_to_string right_type_kind)
1752+
| Record_fields_mismatch {left_record_name; right_record_name; issues}
1753+
->
1754+
fprintf ppf
1755+
"@ The record @{<info>%s@} cannot be coerced to the record \
1756+
@{<info>%s@} because:"
1757+
(Path.name left_record_name)
1758+
(Path.name right_record_name);
1759+
List.iter
1760+
(fun issue ->
1761+
fprintf ppf "@ - ";
1762+
print_record_field_subtype_violation ppf issue ~left_record_name
1763+
~right_record_name)
1764+
issues)
17071765
| None -> ())
17081766

17091767
let report_ambiguous_type_error ppf env (tp0, tp0') tpl txt1 txt2 txt3 =

compiler/ml/record_coercion.ml

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,60 @@
1+
type record_field_subtype_violation =
2+
| Optional_mismatch of {
3+
label: string;
4+
left_optional: bool;
5+
right_optional: bool;
6+
}
7+
| Field_runtime_name_mismatch of {
8+
label: string;
9+
left_as: string option;
10+
right_as: string option;
11+
}
12+
| Field_missing of {label: string}
13+
114
let check_record_fields (fields1 : Types.label_declaration list)
215
(fields2 : Types.label_declaration list) =
3-
let violation = ref false in
16+
let violations = ref [] in
17+
let add_violation v = violations := v :: !violations in
418
let label_decl_sub (acc1, acc2) (ld2 : Types.label_declaration) =
519
match
620
Ext_list.find_first fields1 (fun ld1 -> ld1.ld_id.name = ld2.ld_id.name)
721
with
822
| Some ld1 ->
923
if ld1.ld_optional <> ld2.ld_optional then
1024
(* optional field can't be modified *)
11-
violation := true;
25+
add_violation
26+
(Optional_mismatch
27+
{
28+
label = ld1.ld_id.name;
29+
left_optional = ld1.ld_optional;
30+
right_optional = ld2.ld_optional;
31+
});
1232
let get_as (({txt}, payload) : Parsetree.attribute) =
1333
if txt = "as" then Ast_payload.is_single_string payload else None
1434
in
1535
let get_as_name (ld : Types.label_declaration) =
1636
match Ext_list.filter_map ld.ld_attributes get_as with
17-
| [] -> ld.ld_id.name
18-
| (s, _) :: _ -> s
37+
| [] -> None
38+
| (s, _) :: _ -> Some s
1939
in
20-
if get_as_name ld1 <> get_as_name ld2 then violation := true;
40+
let get_label_runtime_name (ld : Types.label_declaration) =
41+
match get_as_name ld with
42+
| None -> ld.ld_id.name
43+
| Some s -> s
44+
in
45+
if get_label_runtime_name ld1 <> get_label_runtime_name ld2 then
46+
add_violation
47+
(Field_runtime_name_mismatch
48+
{
49+
label = ld1.ld_id.name;
50+
left_as = get_as_name ld1;
51+
right_as = get_as_name ld2;
52+
});
2153
(ld1.ld_type :: acc1, ld2.ld_type :: acc2)
2254
| None ->
2355
(* field must be present *)
24-
violation := true;
56+
add_violation (Field_missing {label = ld2.ld_id.name});
2557
(acc1, acc2)
2658
in
2759
let tl1, tl2 = List.fold_left label_decl_sub ([], []) fields2 in
28-
(!violation, tl1, tl2)
60+
(!violations, tl1, tl2)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/record_coercion_as_payload_mismatch_both.res:15:10-15
4+
5+
13 │ }
6+
14 │
7+
15 │ let y = (x :> y)
8+
16 │
9+
10+
Type x is not a subtype of y
11+
12+
The record x cannot be coerced to the record y because:
13+
- Field x runtime representation is configured to be "z" (via the @as attribute) in record y, but in record x it is configured to be "w" (via the @as attribute). Runtime representations must match.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/record_coercion_as_payload_mismatch_left.res:15:10-15
4+
5+
13 │ }
6+
14 │
7+
15 │ let y = (x :> y)
8+
16 │
9+
10+
Type x is not a subtype of y
11+
12+
The record x cannot be coerced to the record y because:
13+
- Field x runtime representation is configured to be "z" (via the @as attribute) in record y, but in record x it is "x". Runtime representations must match.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/record_coercion_as_payload_mismatch_right.res:15:10-15
4+
5+
13 │ }
6+
14 │
7+
15 │ let y = (x :> y)
8+
16 │
9+
10+
Type x is not a subtype of y
11+
12+
The record x cannot be coerced to the record y because:
13+
- Field x runtime representation is "x" in record y, but in record x it is configured to be "z" (via the @as attribute). Runtime representations must match.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/record_coercion_missing_field.res:16:10-15
4+
5+
14 │ }
6+
15 │
7+
16 │ let y = (x :> y)
8+
17 │
9+
10+
Type x is not a subtype of y
11+
12+
The record x cannot be coerced to the record y because:
13+
- The field z is missing in record y, but present in record x
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/record_coercion_optional_mismatch.res:14:10-15
4+
5+
12 │ }
6+
13 │
7+
14 │ let y = (x :> y)
8+
15 │
9+
10+
Type x is not a subtype of y
11+
12+
The record x cannot be coerced to the record y because:
13+
- The field x is optional in record x, but is not optional in record y
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
type x = {
2+
@as("z") x: int,
3+
y: int,
4+
}
5+
type y = {
6+
@as("w") x: int,
7+
y: int,
8+
}
9+
10+
let x: x = {
11+
x: 1,
12+
y: 1,
13+
}
14+
15+
let y = (x :> y)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
type x = {
2+
@as("z") x: int,
3+
y: int,
4+
}
5+
type y = {
6+
x: int,
7+
y: int,
8+
}
9+
10+
let x: x = {
11+
x: 1,
12+
y: 1,
13+
}
14+
15+
let y = (x :> y)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
type x = {
2+
x: int,
3+
y: int,
4+
}
5+
type y = {
6+
@as("z")x: int,
7+
y: int,
8+
}
9+
10+
let x: x = {
11+
x: 1,
12+
y: 1,
13+
}
14+
15+
let y = (x :> y)

0 commit comments

Comments
 (0)