Skip to content

Commit 9c4de0a

Browse files
cknittclaude
andauthored
Show existential types as $a and name their constructor (#9080)
* Show existential types as $a and name their constructor Why: An existential type of a GADT constructor is an abstract type named `$Any_'a` internally, and that name was printed as is in errors and hover. It is now printed `$a`, as in OCaml 5, and the escape error says which constructor binds it instead of calling it a type constructor. Diagnostic modified (existential escape): Example: type rec any = Any('a): any let get = (Any(x)) => x Before (diagnostic): This has type: $Any_'a But it's expected to have type: 'a The type constructor $Any_'a would escape its scope 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. Review: Why decode the name instead of recording the constructor? The name already holds both parts, so printing needs no table that would have to be reset between compilations in one process. Fixes: #9068 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> * Give distinct existential types distinct names Codex review on #9080: printing `$C_'a` as `$a` made existentials of different constructors that bind the same variable print alike, so a mismatch between them read `$a` vs `$a`. Existentials are now named `$a` before the compiler makes the name unique, as OCaml 5 does, so the second one is `$a1`; the constructor binding each name is recorded for the escape explanation instead of being decoded from the name. Diagnostic modified (existentials of two constructors): Example: type rec any = First('a): any | Second('a): any let same = (First(x), Second(y)) => x == y Before (diagnostic): This has type: $a But it's being compared to something of type: $a After (diagnostic): This has type: $a1 But it's being compared to something of type: $a 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> * Key existential constructors by identifier Codex review on #9080: the constructor of each existential was recorded by its name, but the counter that makes names unique is reset for each binding, so a nested module's `$a` could overwrite the constructor of an outer `$a`. It is recorded by the identifier's stamp now. 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: Christoph Knittel <ck@cca.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9ceeb96 commit 9c4de0a

12 files changed

Lines changed: 111 additions & 6 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Existential types of GADT constructors are shown as `$a` instead of `$Any_'a`, and an escaping one is explained by naming the constructor that binds it.

‎compiler/ml/ctype.ml‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -933,19 +933,35 @@ let new_declaration newtype manifest =
933933
type_inlined_types = [];
934934
}
935935

936+
(* The constructor binding each existential type created by
937+
[instance_constructor], by the stamp of its identifier, for explaining an
938+
escaping one. Names are not unique: their counter is reset per binding. *)
939+
let existential_constructors_key =
940+
Request_local.new_key (fun () -> Hashtbl.create 8)
941+
let existential_constructor id =
942+
Hashtbl.find_opt
943+
(Domain.DLS.get existential_constructors_key)
944+
(Ident.binding_time id)
945+
936946
let instance_constructor ?in_pattern cstr =
937947
with_copy_session (fun () ->
938948
(match in_pattern with
939949
| None -> ()
940950
| Some (env, newtype_lev) ->
941951
let process existential =
942952
let decl = new_declaration (Some (newtype_lev, newtype_lev)) None in
953+
(* [$a] for the variable ['a], numbered when the name is taken,
954+
so that distinct existentials print differently *)
943955
let name =
944956
match repr existential with
945-
| {desc = Tvar (Some name)} -> "$" ^ cstr.cstr_name ^ "_'" ^ name
957+
| {desc = Tvar (Some name)} -> "$" ^ name
946958
| _ -> "$" ^ cstr.cstr_name
947959
in
948-
let path = Path.Pident (Ident.create (get_new_abstract_name name)) in
960+
let id = Ident.create (get_new_abstract_name name) in
961+
Hashtbl.replace
962+
(Domain.DLS.get existential_constructors_key)
963+
(Ident.binding_time id) cstr.cstr_name;
964+
let path = Path.Pident id in
949965
let new_env = Env.add_local_type path decl !env in
950966
env := new_env;
951967
let to_unify = newty (Tconstr (path, [], ref Mnil)) in

‎compiler/ml/ctype.mli‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ val instance_constructor :
175175
type_expr list * type_expr
176176
(* Same, for a constructor *)
177177

178+
val existential_constructor : Ident.t -> string option
179+
(* The constructor that binds this existential type, created by
180+
[instance_constructor] in the current request *)
181+
178182
val instance_parameterized_type :
179183
?keep_names:bool -> type_expr list -> type_expr -> type_expr list * type_expr
180184
val instance_declaration : type_declaration -> type_declaration

‎compiler/ml/printtyp.ml‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,16 +1300,31 @@ let rec mismatch = function
13001300
| [] -> None
13011301
| _ -> assert false
13021302

1303+
let escape_explanation ppf p =
1304+
match p with
1305+
| Pident id -> (
1306+
match Ctype.existential_constructor id with
1307+
| Some constructor ->
1308+
fprintf ppf
1309+
"@,\
1310+
@[%s is an existential type bound by the constructor %s.@ It is only \
1311+
known in the scope of the pattern that matches %s.@]"
1312+
(Ident.name id) constructor constructor
1313+
| None ->
1314+
fprintf ppf "@,@[The type constructor@;<1 2>%a@ would escape its scope@]"
1315+
path p)
1316+
| _ ->
1317+
fprintf ppf "@,@[The type constructor@;<1 2>%a@ would escape its scope@]"
1318+
path p
1319+
13031320
let explanation unif t3 t4 ppf =
13041321
match (t3.desc, t4.desc) with
13051322
| Ttuple [], Tvar _ | Tvar _, Ttuple [] ->
13061323
fprintf ppf "@,Self type cannot escape its class"
13071324
| Tconstr (p, _, _), Tvar _ when unif && t4.level < Path.binding_time p ->
1308-
fprintf ppf "@,@[The type constructor@;<1 2>%a@ would escape its scope@]"
1309-
path p
1325+
escape_explanation ppf p
13101326
| Tvar _, Tconstr (p, _, _) when unif && t3.level < Path.binding_time p ->
1311-
fprintf ppf "@,@[The type constructor@;<1 2>%a@ would escape its scope@]"
1312-
path p
1327+
escape_explanation ppf p
13131328
| Tvar _, Tunivar _ | Tunivar _, Tvar _ ->
13141329
fprintf ppf "@,The universal variable %a would escape its scope" type_expr
13151330
(if is_Tunivar t3 then t3 else t4)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type rec any = Any(array<'a>, 'a => string): any
2+
3+
let showAll = (Any(xs, f)) => xs->Array.map(f)
4+
// ^hov
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Hover src/HoverExistential.res 2:19
2+
{
3+
"contents": { "kind": "markdown", "value": "```rescript\narray<$a>\n```" }
4+
}
5+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/gadt_existential_escape.res:3:23
4+
5+
1 │ type rec any = Any('a): any
6+
2 │
7+
3 │ let get = (Any(x)) => x
8+
4 │
9+
10+
This has type: $a
11+
But it's expected to have type: 'a
12+
$a is an existential type bound by the constructor Any.
13+
It is only known in the scope of the pattern that matches Any.
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_nested_binding.res:12:5
4+
5+
10 ┆ }
6+
11 ┆ }
7+
12 ┆ x
8+
13 ┆ | Second(_) => assert(false)
9+
14 ┆ }
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 First.
14+
It is only known in the scope of the pattern that matches First.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/gadt_existential_same_name.res:3:42
4+
5+
1 │ type rec any = First('a): any | Second('a): any
6+
2 │
7+
3 │ let same = (First(x), Second(y)) => x == y
8+
4 │
9+
10+
This has type: $a1
11+
But it's being compared to something of type: $a
12+
13+
You can only compare things of the same type.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
type rec any = Any('a): any
2+
3+
let get = (Any(x)) => x

0 commit comments

Comments
 (0)