Skip to content

Commit 6ea63e5

Browse files
cknittclaudecristianoc
authored
Type a let destructuring a GADT without switch diagnostics (#9081)
* Type a let destructuring a GADT without switch diagnostics Why: A `let` whose pattern contains a GADT constructor is typed as a one-case match, and that match took the diagnostic context of a `switch` in the source: a type error in the rest of the block said "But this switch is expected to return" and added the hint about switch branches, although the source has no switch. The match now carries `#let_gadt_pattern`, and its cases, like those of the match for an optional argument default, get function-style diagnostics. 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 Texp_match expression Texp_ident "value/1004" [ <case> pattern Tpat_construct "Any" … expression Texp_apply … ] After (typed): expression attribute "#let_gadt_pattern" [] Texp_match expression Texp_ident "value/1004" [ <case> pattern Tpat_construct "Any" … expression Texp_apply … ] Decision modified (diagnostic context of a match's cases): Before (text): `call_context` in `Typecore.type_expect_` match with @let.unwrap ↦ LetUnwrap match with #optional_arg_default ↦ Function match typed for a let with a GADT pattern ↦ Switch any other match ↦ Switch After (text): match with @let.unwrap ↦ LetUnwrap match with #optional_arg_default ↦ Function match with #let_gadt_pattern ↦ Function any other match ↦ Switch Diagnostic modified (type error in the body of a let destructuring a GADT): Example: type rec any = Any('a, 'a => string): any let get = value => { let Any(x, _) = value x } Before (diagnostic): This has type: $a But this switch is expected to return: 'a $a is an existential type bound by the constructor Any. It is only known in the scope of the pattern that matches Any. All branches in a switch must return the same type. To fix this, change your branch to return the expected type. After (diagnostic): This has type: $a But it's expected to have type: 'a $a is an existential type bound by the constructor Any. It is only known in the scope of the pattern that matches Any. Fixes: #9069 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3 Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel <ck@cca.io> Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com> * Records show the changed operation before its consequences The records skill asked for a block for each object a commit changes in what the compiler does, and a record of #9081 met it with the diagnostic alone, leaving the attribute the type checker adds to the synthesized match in prose. A record now shows the representation, rule, transformation or decision that changes, at its own level, and then its consequences; a change the prose names is shown in a block, a change no form prints is shown as a schematic, and a script or review fix that changes behaviour gets blocks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com> --------- Signed-off-by: Christoph Knittel <ck@cca.io> Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cristiano Calcagno <cristianoc@users.noreply.github.com>
1 parent 9c4de0a commit 6ea63e5

5 files changed

Lines changed: 55 additions & 7 deletions

File tree

‎.agents/skills/records/SKILL.md‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,32 @@ The table lists the common kinds.
177177
## What a commit holds
178178

179179
Every commit says why it is made. A commit that changes what the compiler
180-
or a tool does gets one object block for each object it changes, with the
181-
smallest example that shows the change. A refactor, a test, a script or a
182-
review fix needs no block, only its subject and a line or two of prose; a
183-
review fix names the finding it answers.
180+
or a tool does shows each representation, rule, transformation or decision
181+
it changes, Before and After, at the level where it operates, then the
182+
representative consequences that show why it matters: the blocks go from
183+
the changed operation to what it affects, and one block serves both when
184+
the operation directly changes the output shown. Each change the prose
185+
names, such as an attribute the compiler adds or a case a decision takes,
186+
is also shown in a block. Each block has the smallest example that shows
187+
its change.
188+
189+
- A printed form shows the change when one exposes it. Otherwise a block
190+
shows a schematic, captioned with the function that implements it: an
191+
inference rule for a changed typing judgment, a clause matrix and its
192+
split for the match compiler, a mapping from inputs to results for a
193+
decision, keeping the premises, priorities and side conditions that
194+
matter.
195+
- Printing each form for the example before and after the commit finds
196+
the layers it changes; the record keeps the excerpts that show the
197+
changed operation and its distinct consequences.
198+
- When an error stops later forms from being printed, the internal forms
199+
come from a related example that compiles, the diagnostic from one that
200+
fails, and the prose says how they relate.
201+
202+
A commit whose changes are only organization, tests or documentation
203+
needs no block, only its subject and a line or two of prose; a script or
204+
a review fix that changes behaviour gets blocks like any other. A review
205+
fix names the finding it answers.
184206

185207
## Rendering
186208

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- A type error in the body after a `let` that destructures a GADT constructor no longer talks about switch branches.

‎compiler/ml/typecore.ml‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2644,6 +2644,8 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp
26442644
{
26452645
sexp with
26462646
pexp_desc = Pexp_match (sval, [Ast_helper.Exp.case spat sbody]);
2647+
pexp_attributes =
2648+
(mknoloc "#let_gadt_pattern", PStr []) :: sexp.pexp_attributes;
26472649
}
26482650
ty_expected
26492651
| Pexp_let (rec_flag, spat_sexp_list, sbody) ->
@@ -2832,10 +2834,13 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp
28322834
in
28332835
let call_context =
28342836
(* Optional-default lowering synthesizes a match expression, but from the
2835-
user's point of view this is still part of a function definition. Use
2836-
function-style diagnostics instead of reporting a phantom switch. *)
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. *)
28372841
if has_attr "let.unwrap" then `LetUnwrap
2838-
else if has_attr "#optional_arg_default" then `Function
2842+
else if has_attr "#optional_arg_default" || has_attr "#let_gadt_pattern"
2843+
then `Function
28392844
else `Switch
28402845
in
28412846
let val_cases, partial =
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/gadt_existential_let.res:5:3
4+
5+
3 │ let get = value => {
6+
4 │ let Any(x, _) = value
7+
5 │ x
8+
6 │ }
9+
7 │
10+
11+
This has type: $a
12+
But it's expected to have type: 'a
13+
$a is an existential type bound by the constructor Any.
14+
It is only known in the scope of the pattern that matches Any.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
type rec any = Any('a, 'a => string): any
2+
3+
let get = value => {
4+
let Any(x, _) = value
5+
x
6+
}

0 commit comments

Comments
 (0)