Skip to content

Commit cb6dd38

Browse files
committed
Fix printing of optional record fields in pattern matching errors
1 parent a39eb8e commit cb6dd38

7 files changed

+129
-5
lines changed

compiler/common/pattern_printer.ml

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,55 @@
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_field_state =
19+
| Field_normal (* Regular user patterns: `{b: Some(_)}`, `{b}`, `_`, etc. *)
20+
| Field_missing
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+
| Field_present_none
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[@warning "-4"] rec classify_optional_field_state pat =
32+
match pat.pat_desc with
33+
| Tpat_construct (lid, cstr, [])
34+
when is_generated_optional_constructor lid && cstr.cstr_name = "None" ->
35+
Field_missing
36+
| Tpat_construct (lid, cstr, [inner])
37+
when is_generated_optional_constructor lid && cstr.cstr_name = "Some" -> (
38+
match classify_optional_field_state inner with
39+
| Field_missing | Field_present_none -> Field_present_none
40+
| Field_normal -> Field_normal)
41+
| _ -> Field_normal
42+
43+
let none_pattern =
44+
mkpat (Ppat_construct (mknoloc (Longident.Lident "None"), None))
45+
46+
let[@warning "-4"] strip_synthetic_some pat =
47+
match pat.pat_desc with
48+
| Tpat_construct (lid, cstr, [inner])
49+
when is_generated_optional_constructor lid && cstr.cstr_name = "Some" ->
50+
inner
51+
| _ -> pat
52+
753
let untype typed =
854
let rec loop pat =
955
match pat.pat_desc with
@@ -30,12 +76,26 @@ let untype typed =
3076
let arg = Option.map loop p_opt in
3177
mkpat (Ppat_variant (label, arg))
3278
| Tpat_record (subpatterns, closed_flag) ->
33-
let fields =
34-
List.map
35-
(fun (_, lbl, p, opt) ->
36-
{lid = mknoloc (Longident.Lident lbl.lbl_name); x = loop p; opt})
37-
subpatterns
79+
let fields, saw_optional_rewrite =
80+
List.fold_right
81+
(fun (_, lbl, p, opt) (fields, saw_optional_rewrite) ->
82+
let state =
83+
if lbl.lbl_optional then classify_optional_field_state p
84+
else Field_normal
85+
in
86+
let opt, par_pat, rewrote_optional =
87+
match state with
88+
| Field_missing -> (true, none_pattern, true)
89+
| Field_present_none -> (opt, loop (strip_synthetic_some p), true)
90+
| Field_normal -> (opt, loop p, false)
91+
in
92+
let field =
93+
{lid = mknoloc (Longident.Lident lbl.lbl_name); x = par_pat; opt}
94+
in
95+
(field :: fields, saw_optional_rewrite || rewrote_optional))
96+
subpatterns ([], false)
3897
in
98+
let closed_flag = if saw_optional_rewrite then Closed else closed_flag in
3999
mkpat (Ppat_record (fields, closed_flag))
40100
| Tpat_array lst -> mkpat (Ppat_array (List.map loop lst))
41101
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_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_inline.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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
Warning number 8
3+
/.../fixtures/optional_record_field_missing_cases_nested.res:5:9-7:1
4+
5+
3 │ let a: t = Obj.magic()
6+
4 │
7+
5 │ let _ = switch a {
8+
6 │ | {b: Some(Some(_))} => ()
9+
7 │ }
10+
8 │
11+
12+
You forgot to handle a possible case here, for example:
13+
| {b: Some(None)} | {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 = {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 = 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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
type t = {b?: option<option<int>>}
2+
3+
let a: t = Obj.magic()
4+
5+
let _ = switch a {
6+
| {b: Some(Some(_))} => ()
7+
}

0 commit comments

Comments
 (0)