Skip to content

Commit b23f95b

Browse files
cknittclaude
andauthored
Keep @tag and runtime annotations when printing a type declaration (#9136)
* Keep @tag and runtime annotations when printing a type declaration Why: The printer of type declarations kept only @Immediate and @unboxed. @tag, @notundefined, @instanceof and @typeof were dropped, so a signature mismatch about them printed two declarations that looked the same, hover hid them, and createInterface wrote an interface for an applied functor that does not match its implementation (#9134). Diagnostic modified (@tag mismatch): Example: // Foo.res @tag("type") type t = | @as("a") A | B // Foo.resi @tag("kind") type t = | @as("a") A | B Before (diagnostic): Actual declaration (Foo.res:2:1-4:5): type t = @as("a") A | B Expected declaration (Foo.resi:2:1-4:5): type t = @as("a") A | B Their @tag annotations differ. After (diagnostic): Actual declaration (Foo.res:2:1-4:5): @tag("type") type t = @as("a") A | B Expected declaration (Foo.resi:2:1-4:5): @tag("kind") type t = @as("a") A | B Their @tag annotations differ. Diagnostic modified (@notundefined functor parameter): Example: module Make = ( X: { @notundefined type t }, ) => { type u = X.t } module Applied = Make({ type t = option<int> }) Before (diagnostic): Actual declaration (not_undefined_functor_argument.res:10:3-22): type t = option<int> Expected declaration (not_undefined_functor_argument.res:3:19-24): type t Their runtime representations differ: Actual declaration: can hold undefined Expected declaration: @notundefined After (diagnostic): Actual declaration (not_undefined_functor_argument.res:10:3-22): type t = option<int> Expected declaration (not_undefined_functor_argument.res:3:19-24): @notundefined type t Their runtime representations differ: Actual declaration: can hold undefined Expected declaration: @notundefined Format modified (createInterface for an applied functor): Example: module Tagged = (X: Type) => { @tag("kind") type wrapped = Value({value: X.t}) | Empty({reason: string}) } module IntTagged = Tagged({ type t = int }) Before (rescript): the interface does not compile against the implementation module IntTagged: { type wrapped = Value({value: int}) | Empty({reason: string}) } After (rescript): module IntTagged: { @tag("kind") type wrapped = Value({value: int}) | Empty({reason: string}) } Representation modified (printed declaration annotations, Printtyp.tree_of_type_decl): Before (representation): otype_immediate: bool ↦ @Immediate otype_unboxed: bool ↦ @unboxed After (representation): otype_attributes: string list ↦ @Immediate @unboxed @tag("…") @notundefined | @instanceof("…") | @typeof("…") source: what the declaration itself states, not what it inherits from an abbreviated type Review: Why read the attributes rather than the declaration's runtime shape? The shape of an abbreviation can come from the type it abbreviates; the printed declaration should show what it states itself, as the source does. Review: Can printing raise for a malformed annotation? No. The annotations are parsed by the functions that typing uses, which raise on malformed payloads; those were reported when the declaration was typed, and the printer then leaves the annotation out. Fixes: #9134 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kbf3gLpf7RN1MiRcubPgRn Signed-off-by: Christoph Knittel <ck@cca.io> * Say when declarations that differ read the same Why: With the annotations printed, two declarations can still read the same and not match, when a type they mention differs between the two sides, e.g. a base class that the interface leaves abstract. The error then showed the same text twice, which reads as a bug in the compiler. The reasons now start with a note saying so, which replaces the lead-in of a difference in runtime representation. Diagnostic modified (declarations that print the same): Example: // Classes.res @instanceof("Animal") type animal @instanceof("Dog") type dog = private animal // Classes.resi type animal @instanceof("Dog") type dog = private animal Before (diagnostic): Actual declaration (Classes.res:2:20-44): @instanceof("Dog") type dog = private animal Expected declaration (Classes.resi:2:20-44): @instanceof("Dog") type dog = private animal Their runtime representations differ: Actual declaration: instances of Dog, a subclass of Animal Expected declaration: instances of Dog After (diagnostic): Actual declaration (Classes.res:2:20-44): @instanceof("Dog") type dog = private animal Expected declaration (Classes.resi:2:20-44): @instanceof("Dog") type dog = private animal The declarations read the same, but a type they mention differs: Actual declaration: instances of Dog, a subclass of Animal Expected declaration: instances of Dog Review: Why compare the printed text? It is what the reader sees, and the case to catch is exactly two declarations that look the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kbf3gLpf7RN1MiRcubPgRn Signed-off-by: Christoph Knittel <ck@cca.io> * Escape a @tag name when printing it Codex review of #9136: the printed @tag put the decoded name between quotes as it is, so a tag with a quote printed as invalid source and a backslash changed its value when reparsed, also in interfaces written by createInterface. It is now encoded as a string literal, which keeps non-ASCII letters as they are. Diagnostic modified (@tag names with a quote, a backslash and a non-ASCII letter): Example: module M: { @tag("k\"ind") type t = A({x: int}) | B({y: int}) } = { @tag("typ\\é") type t = A({x: int}) | B({y: int}) } Before (diagnostic): Actual declaration (tag_name_escaped.res:6:18-51): @tag("typ\é") type t = A({x: int}) | B({y: int}) Expected declaration (tag_name_escaped.res:4:18-51): @tag("k"ind") type t = A({x: int}) | B({y: int}) After (diagnostic): Actual declaration (tag_name_escaped.res:6:18-51): @tag("typ\\é") type t = A({x: int}) | B({y: int}) Expected declaration (tag_name_escaped.res:4:18-51): @tag("k\"ind") type t = A({x: int}) | B({y: int}) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kbf3gLpf7RN1MiRcubPgRn 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 acdf563 commit b23f95b

23 files changed

Lines changed: 151 additions & 44 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Keep `@tag`, `@notUndefined`, `@instanceOf` and `@typeOf` when printing a type declaration, in error messages, hover and generated interfaces.

‎compiler/ml/includecore.ml‎

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,9 @@ let label side decl =
201201
in
202202
String.capitalize_ascii (side ^ " " ^ decl)
203203

204-
let report_type_mismatch0 first second decl ppf err =
204+
(* [lead_in] introduces the two lines of a difference in runtime
205+
representation; without it they stand under a lead-in printed before *)
206+
let report_type_mismatch0 ?(lead_in = true) first second decl ppf err =
205207
let pr fmt = Format.fprintf ppf fmt in
206208
match err with
207209
| Arity -> pr "They have different arities"
@@ -243,24 +245,41 @@ let report_type_mismatch0 first second decl ppf err =
243245
| Variant_representation s ->
244246
pr "The internal representations for case %s are not equal" (Ident.name s)
245247
| Not_undefined ->
246-
pr
247-
"@[<v 2>Their runtime representations differ:@,\
248-
%s: can hold undefined@,\
249-
%s: @notUndefined@]"
250-
(label first decl) (label second decl)
248+
if lead_in then pr "@[<v 2>Their runtime representations differ:@,";
249+
pr "%s: can hold undefined@,%s: @notUndefined" (label first decl)
250+
(label second decl);
251+
if lead_in then pr "@]"
251252
| Runtime_annotation (declared1, declared2) ->
252-
pr "@[<v 2>Their runtime representations differ:@,%s: %s@,%s: %s@]"
253-
(label first decl) declared1 (label second decl) declared2
253+
if lead_in then pr "@[<v 2>Their runtime representations differ:@,";
254+
pr "%s: %s@,%s: %s" (label first decl) declared1 (label second decl)
255+
declared2;
256+
if lead_in then pr "@]"
254257

255-
let report_type_mismatch first second decl ppf =
256-
List.iter (fun err ->
257-
match err with
258-
| Manifest -> ()
259-
(* Its lines are labels, not sentences *)
260-
| Runtime_annotation _ | Not_undefined ->
261-
Format.fprintf ppf "@ %a" (report_type_mismatch0 first second decl) err
262-
| _ ->
263-
Format.fprintf ppf "@ %a." (report_type_mismatch0 first second decl) err)
258+
(* Each reason on a line after a break. [read_the_same] puts them under a note
259+
that the declarations print the same, which then replaces the lead-in of a
260+
difference in runtime representation. *)
261+
let report_type_mismatch ?(read_the_same = false) first second decl ppf errs =
262+
let report ppf =
263+
List.iter (fun err ->
264+
match err with
265+
| Manifest -> ()
266+
(* Its lines are labels, not sentences *)
267+
| Runtime_annotation _ | Not_undefined ->
268+
Format.fprintf ppf "@ %a"
269+
(report_type_mismatch0 ~lead_in:(not read_the_same) first second
270+
decl)
271+
err
272+
| _ ->
273+
Format.fprintf ppf "@ %a."
274+
(report_type_mismatch0 first second decl)
275+
err)
276+
in
277+
if read_the_same then
278+
Format.fprintf ppf
279+
"@ @[<v 2>The declarations read the same, but a type they mention \
280+
differs:%a@]"
281+
report errs
282+
else report ppf errs
264283

265284
let rec compare_constructor_arguments env cstr params1 params2 arg1 arg2 =
266285
match (arg1, arg2) with

‎compiler/ml/includecore.mli‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,10 @@ val extension_constructors :
6767
Env.t -> Ident.t -> extension_constructor -> extension_constructor -> bool
6868

6969
val report_type_mismatch :
70-
string -> string -> string -> Format.formatter -> type_mismatch list -> unit
70+
?read_the_same:bool ->
71+
string ->
72+
string ->
73+
string ->
74+
Format.formatter ->
75+
type_mismatch list ->
76+
unit

‎compiler/ml/includemod.ml‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,8 +583,16 @@ let include_symptom ~swapped env ppf = function
583583
if swapped then ("the expected", "the actual")
584584
else ("the actual", "the expected")
585585
in
586+
(* Declarations can print the same and still differ, when a type they
587+
mention differs between the two sides, e.g. a class the interface
588+
leaves abstract. The reasons then say that first. *)
589+
let read_the_same =
590+
Format.asprintf "%a" (type_declaration id) d1
591+
= Format.asprintf "%a" (type_declaration id) d2
592+
in
586593
fprintf ppf "@,%a"
587-
(Includecore.report_type_mismatch first second "declaration")
594+
(Includecore.report_type_mismatch ~read_the_same first second
595+
"declaration")
588596
errs
589597
| Extension_constructors (id, x1, x2) ->
590598
show_declarations ~swapped (extension_constructor id) ppf

‎compiler/ml/outcometree.ml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ and out_type_decl = {
7070
otype_params: (string * (bool * bool)) list;
7171
otype_type: out_type;
7272
otype_private: Asttypes.private_flag;
73-
otype_immediate: bool;
74-
otype_unboxed: bool;
73+
otype_attributes: string list;
74+
(** The annotations that change what the declaration means at run time,
75+
as they are written, e.g. [@unboxed] or [@tag("kind")] *)
7576
otype_cstrs: (out_type * out_type) list;
7677
}
7778
and out_extension_constructor = {

‎compiler/ml/printtyp.ml‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -892,14 +892,35 @@ and tree_of_type_decl id decl =
892892
decl.type_private,
893893
decl.type_representation = Unboxed )
894894
in
895-
let immediate = Builtin_attributes.immediate decl.type_attributes in
895+
let attributes =
896+
let flag set attribute = if set then [attribute] else [] in
897+
(* A malformed annotation was reported when the declaration was typed *)
898+
let stated process =
899+
try process decl.type_attributes
900+
with Ast_untagged_variants.Error _ -> None
901+
in
902+
List.concat
903+
[
904+
flag (Builtin_attributes.immediate decl.type_attributes) "@immediate";
905+
flag unboxed "@unboxed";
906+
(match stated Ast_untagged_variants.process_tag_name with
907+
| Some name ->
908+
[Printf.sprintf "@tag(\"%s\")" (String_literal.encode_js_string name)]
909+
| None -> []);
910+
(match
911+
stated (Ast_untagged_variants.process_runtime_annotation ?loc:None)
912+
with
913+
| Some annotation ->
914+
[Ast_untagged_variants.runtime_annotation_to_string annotation]
915+
| None -> []);
916+
]
917+
in
896918
{
897919
otype_name = name;
898920
otype_params = args;
899921
otype_type = ty;
900922
otype_private = priv;
901-
otype_immediate = immediate;
902-
otype_unboxed = unboxed;
923+
otype_attributes = attributes;
903924
otype_cstrs = constraints;
904925
}
905926

‎compiler/syntax/src/res_outcome_printer.ml‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -661,11 +661,10 @@ let rec print_out_sig_item_doc ?(print_name_as_is = false)
661661
| Osig_type (out_type_decl, out_rec_status) ->
662662
(* TODO: manifest ? *)
663663
let attrs =
664-
match (out_type_decl.otype_immediate, out_type_decl.otype_unboxed) with
665-
| false, false -> Doc.nil
666-
| true, false -> Doc.concat [Doc.text "@immediate"; Doc.line]
667-
| false, true -> Doc.concat [Doc.text "@unboxed"; Doc.line]
668-
| true, true -> Doc.concat [Doc.text "@immediate @unboxed"; Doc.line]
664+
match out_type_decl.otype_attributes with
665+
| [] -> Doc.nil
666+
| attributes ->
667+
Doc.concat [Doc.text (String.concat " " attributes); Doc.line]
669668
in
670669
let kw =
671670
Doc.text

‎tests/analysis_tests/tests/src/CreateInterfaceFunctor.res‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,12 @@ module WithWire = (X: {}) => {
3232
}
3333

3434
module Applied = WithWire({})
35+
36+
// Types of an applied functor keep their @tag
37+
module type Type = {
38+
type t
39+
}
40+
41+
module Tagged = (X: Type) => {
42+
@tag("kind") type wrapped = Value({value: X.t}) | Empty({reason: string})
43+
}

‎tests/analysis_tests/tests/src/CreateInterfaceFunctorUse.res‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ include CreateInterfaceFunctor.MakeSet({
99
type t = float
1010
let compare = (a: float, b) => a < b ? -1 : a > b ? 1 : 0
1111
})
12+
13+
module IntTagged = CreateInterfaceFunctor.Tagged({
14+
type t = int
15+
})

‎tests/analysis_tests/tests/src/expected/Completion.res.txt‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)