Skip to content

Commit f086ea6

Browse files
committed
Fix printing of optional record fields in error messages
1 parent a39eb8e commit f086ea6

7 files changed

+125
-1
lines changed

compiler/common/pattern_printer.ml

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,48 @@
11
open Types
22
open Typedtree
33
open Parsetree
4+
open Asttypes
45

56
let mkpat desc = Ast_helper.Pat.mk desc
67

8+
let is_generated_optional_constructor (lid : Longident.t Location.loc) =
9+
match lid.txt with
10+
| Longident.Lident name ->
11+
String.length name >= 2 && name.[0] = '#' && name.[1] = '$'
12+
| _ -> false
13+
14+
(* Optional fields become “option-of-option” internally: the outer layer is
15+
added by the compiler to track presence, while the inner layer is the user’s
16+
payload. When printing counterexamples we only need to know which of these
17+
situations we saw. *)
18+
type optional_absence =
19+
| Not_special (* Regular user patterns: `{b: Some(_)}`, `{b}`, `_`, etc. *)
20+
| Missing_field
21+
(* The outer constructor was the synthetic `#$None…`, i.e. the field was
22+
not provided at all. This is what should print as `{b: ?None}`. *)
23+
| Present_missing_value
24+
(* The outer constructor was the synthetic `#$Some…` but its payload was
25+
still the synthetic `None`. That means the field exists but the user’s
26+
inner option is `None`, so we should print `{b: None}`. *)
27+
28+
(* Optional record fields are lowered into an extra option layer; we re-infer
29+
whether we’re looking at a missing field vs. a present-but-`None` value so
30+
we can render useful surface syntax in error messages. *)
31+
let rec classify_optional_absence pat =
32+
match pat.pat_desc with
33+
| Tpat_construct (lid, cstr, [])
34+
when is_generated_optional_constructor lid && cstr.cstr_name = "None" ->
35+
Missing_field
36+
| Tpat_construct (lid, cstr, [inner])
37+
when is_generated_optional_constructor lid && cstr.cstr_name = "Some" -> (
38+
match classify_optional_absence inner with
39+
| Not_special -> Not_special
40+
| _ -> Present_missing_value)
41+
| _ -> Not_special
42+
43+
let none_pattern =
44+
mkpat (Ppat_construct (mknoloc (Longident.Lident "None"), None))
45+
746
let untype typed =
847
let rec loop pat =
948
match pat.pat_desc with
@@ -30,12 +69,31 @@ let untype typed =
3069
let arg = Option.map loop p_opt in
3170
mkpat (Ppat_variant (label, arg))
3271
| Tpat_record (subpatterns, closed_flag) ->
72+
let special_case_seen = ref false in
3373
let fields =
3474
List.map
3575
(fun (_, lbl, p, opt) ->
36-
{lid = mknoloc (Longident.Lident lbl.lbl_name); x = loop p; opt})
76+
let classification =
77+
if lbl.lbl_optional then classify_optional_absence p
78+
else Not_special
79+
in
80+
(match classification with
81+
| Missing_field | Present_missing_value -> special_case_seen := true
82+
| Not_special -> ());
83+
let opt =
84+
match classification with
85+
| Missing_field -> true
86+
| Present_missing_value | Not_special -> opt
87+
in
88+
let par_pat =
89+
match classification with
90+
| Missing_field | Present_missing_value -> none_pattern
91+
| Not_special -> loop p
92+
in
93+
{lid = mknoloc (Longident.Lident lbl.lbl_name); x = par_pat; opt})
3794
subpatterns
3895
in
96+
let closed_flag = if !special_case_seen then Closed else closed_flag in
3997
mkpat (Ppat_record (fields, closed_flag))
4098
| Tpat_array lst -> mkpat (Ppat_array (List.map loop lst))
4199
in
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
Warning number 8
3+
/.../fixtures/optional_inline_record_field_missing_case.res:5:9-8:1
4+
5+
3 │ let v: t = Obj.magic()
6+
4 │
7+
5 │ let _ = switch v {
8+
6 │ | A({b: None}) => ()
9+
7 │ | A({b: Some(_)}) => ()
10+
8 │ }
11+
9 │
12+
13+
You forgot to handle a possible case here, for example:
14+
| A({b: ?None})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
Warning number 8
3+
/.../fixtures/optional_record_field_missing_case.res:5:9-8:1
4+
5+
3 │ let a: t = Obj.magic()
6+
4 │
7+
5 │ let _ = switch a {
8+
6 │ | {b: None} => ()
9+
7 │ | {b: Some(_)} => ()
10+
8 │ }
11+
9 │
12+
13+
You forgot to handle a possible case here, for example:
14+
| {b: ?None}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
Warning number 8
3+
/.../fixtures/optional_record_field_missing_case_nested.res:5:9-8:1
4+
5+
3 │ let a: t = Obj.magic()
6+
4 │
7+
5 │ let _ = switch a {
8+
6 │ | {b: Some(Some(_))} => ()
9+
7 │ | {b: Some(None)} => ()
10+
8 │ }
11+
9 │
12+
13+
You forgot to handle a possible case here, for example:
14+
| {b: None} | {b: ?None}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
type t = A({b?: option<int>})
2+
3+
let v: t = Obj.magic()
4+
5+
let _ = switch v {
6+
| A({b: None}) => ()
7+
| A({b: Some(_)}) => ()
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
type t = {b?: option<int>}
2+
3+
let a: t = Obj.magic()
4+
5+
let _ = switch a {
6+
| {b: None} => ()
7+
| {b: Some(_)} => ()
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
type t = {b?: option<option<int>>}
2+
3+
let a: t = Obj.magic()
4+
5+
let _ = switch a {
6+
| {b: Some(Some(_))} => ()
7+
| {b: Some(None)} => ()
8+
}

0 commit comments

Comments
 (0)