Skip to content

Commit d0c6eb2

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

8 files changed

+132
-2
lines changed

compiler/common/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
(action
66
(run %{bin:cppo} %{env:CPPO_FLAGS=} %{input-file})))
77
(flags
8-
(:standard -w +a-9-40-42))
8+
(:standard -w +a-4-9-40-42))
99
(libraries syntax))

compiler/common/pattern_printer.ml

Lines changed: 67 additions & 1 deletion
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_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+
| Missing_field | Present_missing_value -> Present_missing_value
40+
| Not_special -> Not_special)
41+
| _ -> Not_special
42+
43+
let none_pattern =
44+
mkpat (Ppat_construct (mknoloc (Longident.Lident "None"), None))
45+
46+
let 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,32 @@ let untype typed =
3076
let arg = Option.map loop p_opt in
3177
mkpat (Ppat_variant (label, arg))
3278
| Tpat_record (subpatterns, closed_flag) ->
79+
let special_case_seen = ref false in
3380
let fields =
3481
List.map
3582
(fun (_, lbl, p, opt) ->
36-
{lid = mknoloc (Longident.Lident lbl.lbl_name); x = loop p; opt})
83+
let classification =
84+
if lbl.lbl_optional then classify_optional_absence p
85+
else Not_special
86+
in
87+
(match classification with
88+
| Missing_field | Present_missing_value -> special_case_seen := true
89+
| Not_special -> ());
90+
let opt =
91+
match classification with
92+
| Missing_field -> true
93+
| Present_missing_value | Not_special -> opt
94+
in
95+
let par_pat =
96+
match classification with
97+
| Missing_field -> none_pattern
98+
| Present_missing_value -> loop (strip_synthetic_some p)
99+
| Not_special -> loop p
100+
in
101+
{lid = mknoloc (Longident.Lident lbl.lbl_name); x = par_pat; opt})
37102
subpatterns
38103
in
104+
let closed_flag = if !special_case_seen then Closed else closed_flag in
39105
mkpat (Ppat_record (fields, closed_flag))
40106
| Tpat_array lst -> mkpat (Ppat_array (List.map loop lst))
41107
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)