Skip to content

Commit d389139

Browse files
cknittclaude
andauthored
Pass the origin of a synthesized match as an argument, not an attribute (#9128)
Why: The type checker rewrites a `let` destructuring a GADT constructor, and an optional parameter's default, into a match, and marked that match with the string attributes `#let_gadt_pattern` and `#optional_arg_default` so that it gets function-style diagnostics rather than a switch's (#9126). Both are made and read inside typecore and nothing after typing reads them. `type_expect` now takes `?match_context` and `type_let` takes `?optional_default`; only `let.unwrap`, which comes from the parsed `let?`, stays an attribute. The GADT rewrite also required the binding to have no attributes, so a doc comment or `@warning` in front of it was an error; its attributes now go on the case pattern, and `@warning` on a pattern now covers that pattern's unused variables. Typedtree modified (let destructuring a GADT constructor): Example: type rec any = Any('a, 'a => string): any let get = value => { let Any(x, f) = value f(x) } Before (typed): expression attribute "#let_gadt_pattern" [] Texp_match expression Texp_ident "value/1004" [ <case> pattern Tpat_construct "Any" … After (typed): expression Texp_match expression Texp_ident "value/1004" [ <case> pattern Tpat_construct "Any" … Diagnostic removed (attribute in front of a GADT let): Example: type rec any = Any('a, 'a => string): any let get = value => { /** Destructures the existential. */ let Any(x, f) = value f(x) } Before (diagnostic): This pattern introduces an existential type from a GADT constructor, which is not allowed here. Match on it inside a switch instead. After (diagnostic): compiles Diagnostic removed (unused variable of a pattern with @warning): Example: let silenced = o => switch o { | @warning("-27") Some(x) => 1 | None => 0 } Before (diagnostic): Warning number 27 unused variable x. After (diagnostic): no warning Review: Why not a typed origin field on `Texp_match`, as #9126 suggests? Nothing after typing reads the origin; the field would change the typed tree for no consumer. Review: Does `@warning("-8")` on a GADT let cover a non-exhaustive pattern? No: the exhaustiveness check runs for the whole match, outside the case pattern's warning scope, as for a switch case. Fixes: #9126 Claude-Session: https://claude.ai/code/session_01Kbf3gHnoR8Goys4AMxET4q Signed-off-by: Christoph Knittel <ck@cca.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent bfe3834 commit d389139

6 files changed

Lines changed: 143 additions & 67 deletions

File tree

‎changes/gadt-let-attributes.fix.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Accept an attribute or doc comment in front of a `let` that destructures a GADT constructor, and let `@warning` on a pattern cover that pattern's unused variables.

‎compiler/ml/typecore.ml‎

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1961,10 +1961,15 @@ let type_pattern ~lev env spat expected_ty =
19611961
reset_pattern true;
19621962
let new_env = ref env in
19631963
let pat = type_pat ~allow_existentials:true ~lev new_env spat expected_ty in
1964+
(* The unused-variable checks are set up here, after [type_pat] has left
1965+
the pattern's warning scope: enter it again so that [@warning] on the
1966+
pattern covers its variables. *)
19641967
let new_env, unpacks =
1965-
add_pattern_variables !new_env
1966-
~check:(fun s -> Warnings.Unused_var_strict s)
1967-
~check_as:(fun s -> Warnings.Unused_var s)
1968+
Builtin_attributes.warning_scope ~ppwarning:false spat.ppat_attributes
1969+
(fun () ->
1970+
add_pattern_variables !new_env
1971+
~check:(fun s -> Warnings.Unused_var_strict s)
1972+
~check_as:(fun s -> Warnings.Unused_var s))
19681973
in
19691974
(pat, new_env, get_ref (pattern_force_ref ()), unpacks)
19701975
@@ -2544,7 +2549,8 @@ let rec type_exp ?deprecated_context ~context ?recarg env sexp =
25442549
In the principal case, [type_expected'] may be at generic_level.
25452550
*)
25462551
2547-
and type_expect ~context ?deprecated_context ?recarg env sexp ty_expected =
2552+
and type_expect ~context ?deprecated_context ?recarg ?match_context env sexp
2553+
ty_expected =
25482554
(* Special errors for braced identifiers passed to records *)
25492555
let context =
25502556
match sexp.pexp_desc with
@@ -2555,14 +2561,19 @@ and type_expect ~context ?deprecated_context ?recarg env sexp ty_expected =
25552561
let previous_saved_types = Cmt_format.get_saved_types () in
25562562
let exp =
25572563
Builtin_attributes.warning_scope sexp.pexp_attributes (fun () ->
2558-
type_expect_ ?deprecated_context ~context ?recarg env sexp ty_expected)
2564+
type_expect_ ?deprecated_context ~context ?recarg ?match_context env
2565+
sexp ty_expected)
25592566
in
25602567
Cmt_format.set_saved_types
25612568
(Cmt_format.Partial_expression exp :: previous_saved_types);
25622569
exp
25632570
2564-
and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp
2565-
ty_expected =
2571+
(* [match_context] is set where the type checker itself rewrites source into
2572+
a match, and says what that match stands for in the source: it decides the
2573+
wording of the match's diagnostics, which would otherwise speak of a
2574+
switch. *)
2575+
and type_expect_ ?deprecated_context ~context ?(recarg = Rejected)
2576+
?(match_context : [`Function] option) env sexp ty_expected =
25662577
let loc = sexp.pexp_loc in
25672578
(* Record the expression type before unifying it with the expected type *)
25682579
let rue exp =
@@ -2636,16 +2647,28 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp
26362647
}
26372648
| Pexp_let
26382649
( Nonrecursive,
2639-
[{pvb_pat = spat; pvb_expr = sval; pvb_attributes = []}],
2650+
[
2651+
{
2652+
pvb_pat = spat;
2653+
pvb_expr = sval;
2654+
pvb_constraint = None;
2655+
pvb_attributes;
2656+
};
2657+
],
26402658
sbody )
26412659
when contains_gadt env spat ->
2642-
(* TODO: allow non-empty attributes? *)
2643-
type_expect ~context:None env
2660+
(* A let destructuring a GADT constructor is typed as a one-case match,
2661+
whose case can bind the constructor's existential types. The rest of
2662+
the block is that case's body, so the match gets function-style
2663+
diagnostics rather than a switch's. The binding's attributes go on the
2664+
case pattern, where they scope over the pattern's warnings. *)
2665+
let spat =
2666+
{spat with ppat_attributes = pvb_attributes @ spat.ppat_attributes}
2667+
in
2668+
type_expect ~context:None ~match_context:`Function env
26442669
{
26452670
sexp with
26462671
pexp_desc = Pexp_match (sval, [Ast_helper.Exp.case spat sbody]);
2647-
pexp_attributes =
2648-
(mknoloc "#let_gadt_pattern", PStr []) :: sexp.pexp_attributes;
26492672
}
26502673
ty_expected
26512674
| Pexp_let (rec_flag, spat_sexp_list, sbody) ->
@@ -2829,19 +2852,15 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp
28292852
(* Note: val_caselist = [] and exn_caselist = [], i.e. a fully
28302853
empty pattern matching can be generated by Camlp4 with its
28312854
revised syntax. Let's accept it for backward compatibility. *)
2832-
let has_attr name =
2833-
Ext_list.exists sexp.pexp_attributes (fun ({txt}, _) -> txt = name)
2834-
in
28352855
let call_context =
2836-
(* Optional-default lowering synthesizes a match expression, but from the
2837-
user's point of view this is still part of a function definition. A
2838-
[let] destructuring a GADT constructor is typed as a one-case match,
2839-
whose body is the rest of the block. Use function-style diagnostics
2840-
for both instead of reporting a phantom switch. *)
2841-
if has_attr "let.unwrap" then `LetUnwrap
2842-
else if has_attr "#optional_arg_default" || has_attr "#let_gadt_pattern"
2843-
then `Function
2844-
else `Switch
2856+
match match_context with
2857+
| Some (`Function as context) -> context
2858+
| None ->
2859+
if
2860+
Ext_list.exists sexp.pexp_attributes (fun ({txt}, _) ->
2861+
txt = "let.unwrap")
2862+
then `LetUnwrap
2863+
else `Switch
28452864
in
28462865
let val_cases, partial =
28472866
type_cases ~call_context env arg.exp_type ty_expected true loc
@@ -3905,7 +3924,6 @@ and type_function ~async loc attrs env ty_expected_
39053924
in
39063925
let smatch =
39073926
Exp.match_ ~loc:sloc
3908-
~attrs:[(mknoloc "#optional_arg_default", PStr [])]
39093927
(Exp.ident ~loc (mknoloc (Longident.Lident opt_name)))
39103928
scases
39113929
in
@@ -4058,7 +4076,8 @@ and type_function ~async loc attrs env ty_expected_
40584076
| Some vb ->
40594077
let let_env = ext_env in
40604078
let pat_exp_list, ext_env, let_unpacks =
4061-
type_let ~context:None ext_env Nonrecursive [vb] true
4079+
type_let ~context:None ~optional_default:true ext_env Nonrecursive
4080+
[vb] true
40624081
in
40634082
(* The pattern binds the option carrier under an unspellable name
40644083
([*opt_<label>*]), which typing needs so that user code can
@@ -4908,9 +4927,13 @@ and type_cases ~(call_context : [`LetUnwrap | `Switch | `Function | `Try]) env
49084927
49094928
(* Typing of let bindings *)
49104929
4930+
(* [optional_default] marks the let that binds an optional parameter's
4931+
default, which [type_function] synthesizes: its variables are parameters
4932+
from the user's point of view, so an unused one gets the strict (parameter)
4933+
warning, and its match on the option gets function-style diagnostics. *)
49114934
and type_let ~context ?(check = fun s -> Warnings.Unused_var s)
4912-
?(check_strict = fun s -> Warnings.Unused_var_strict s) env rec_flag
4913-
spat_sexp_list allow =
4935+
?(check_strict = fun s -> Warnings.Unused_var_strict s)
4936+
?(optional_default = false) env rec_flag spat_sexp_list allow =
49144937
let spat_sexp_list =
49154938
List.map
49164939
(fun (vb : Parsetree.value_binding) ->
@@ -4926,17 +4949,7 @@ and type_let ~context ?(check = fun s -> Warnings.Unused_var s)
49264949
spat_sexp_list
49274950
in
49284951
begin_def ();
4929-
let is_fake_let =
4930-
(* the synthetic let-declaration introduced for an optional parameter's
4931-
default: its variables are parameters from the user's point of view,
4932-
so an unused one gets the strict (parameter) warning *)
4933-
match spat_sexp_list with
4934-
| [{pvb_expr = {pexp_attributes}}] ->
4935-
Ext_list.exists pexp_attributes (fun ({txt}, _) ->
4936-
txt = "#optional_arg_default")
4937-
| _ -> false
4938-
in
4939-
let check = if is_fake_let then check_strict else check in
4952+
let check = if optional_default then check_strict else check in
49404953
49414954
let spatl =
49424955
List.map
@@ -5048,7 +5061,11 @@ and type_let ~context ?(check = fun s -> Warnings.Unused_var s)
50485061
let type_expression expected =
50495062
Builtin_attributes.warning_scope pvb_attributes (fun () ->
50505063
match pvb_constraint with
5051-
| None -> type_expect ~context exp_env sexp expected
5064+
| None ->
5065+
let match_context =
5066+
if optional_default then Some `Function else None
5067+
in
5068+
type_expect ~context ?match_context exp_env sexp expected
50525069
| Some {pvc_newtypes; pvc_type} ->
50535070
let constrained =
50545071
Ast_helper.Exp.constraint_ ~loc:pvb_loc sexp pvc_type
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
Warning number 27
3+
/.../fixtures/pattern_warning_scope.res:21:10
4+
5+
19 │ let unsilenced = o =>
6+
20 │ switch o {
7+
21 │ | Some(y) => 1
8+
22 │ | None => 0
9+
23 │ }
10+
11+
unused variable y.
12+
13+
Fix this by:
14+
- Deleting the variable if it's not used anymore.
15+
- Prepending the variable name with `_` (like `_y`) to ignore that the variable is unused.
16+
- Using the variable somewhere.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
type rec any = Any('a, 'a => string): any
2+
3+
// `@warning` on a let destructuring a GADT and on a case pattern covers the
4+
// pattern's variables; only `unsilenced` is reported.
5+
let get = value => {
6+
/** Destructures the existential. */
7+
@warning("-27")
8+
let Any(x, f) = value
9+
ignore(f)
10+
"s"
11+
}
12+
13+
let silenced = o =>
14+
switch o {
15+
| @warning("-27") Some(x) => 1
16+
| None => 0
17+
}
18+
19+
let unsilenced = o =>
20+
switch o {
21+
| Some(y) => 1
22+
| None => 0
23+
}

0 commit comments

Comments
 (0)