Semantic Diff

#9136 Keep @tag and runtime annotations when printing a type declaration

3 commits

acdf563eef100e5b8d34a9803830c81bd1771b76..pr-9136

08279c285 · cknitt + Claude · Oct 30, 2026, 07:14 UTC

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).

modifiedDiagnostic @tag mismatch

// Foo.res@tag("type")type t =  | @as("a") A  | B// Foo.resi@tag("kind")type t =  | @as("a") A  | B
diagnostic
Before
After
Actual declaration (Foo.res:2:1-4:5):Actual declaration (Foo.res:2:1-4:5): type t = @as("a") A | B @tag("type") type t = @as("a") A | B Expected declaration (Foo.resi:2:1-4:5):Expected declaration (Foo.resi:2:1-4:5): type t = @as("a") A | B @tag("kind") type t = @as("a") A | B Their @tag annotations differ.Their @tag annotations differ.

modifiedDiagnostic @notUndefined functor parameter

module Make = (  X: {    @notUndefined type t  },) => {  type u = X.t}module Applied = Make({  type t = option<int>})
diagnostic
Before
After
Actual declaration (not_undefined_functor_argument.res:10:3-22):Actual declaration (not_undefined_functor_argument.res:10:3-22): type t = option<int> type t = option<int> Expected declaration (not_undefined_functor_argument.res:3:19-24):Expected declaration (not_undefined_functor_argument.res:3:19-24): type t @notUndefined type t Their runtime representations differ:Their runtime representations differ: Actual declaration: can hold undefined Actual declaration: can hold undefined Expected declaration: @notUndefined Expected declaration: @notUndefined

modifiedFormat createInterface for an applied functor

module Tagged = (X: Type) => {  @tag("kind") type wrapped = Value({value: X.t}) | Empty({reason: string})}module IntTagged = Tagged({  type t = int})
rescript
Before the interface does not compile against the implementation
After
module IntTagged: {module IntTagged: { type wrapped = Value({value: int}) | Empty({reason: string}) @tag("kind") type wrapped = Value({value: int}) | Empty({reason: string})}}

modifiedRepresentation printed declaration annotations, Printtyp.tree_of_type_decl

representation
Before
After
otype_immediate: bool ↦ @immediateotype_unboxed: bool ↦ @unboxedotype_attributes: string list ↦ @immediate @unboxed @tag("…") @notUndefined | @instanceOf("…") | @typeOf("…")
sourcewhat 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.
  • 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.
922b66364 · cknitt + Claude · Oct 30, 2026, 15:50 UTC

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.

modifiedDiagnostic declarations that print the same

// Classes.res@instanceOf("Animal") type animal@instanceOf("Dog") type dog = private animal// Classes.resitype animal@instanceOf("Dog") type dog = private animal
diagnostic
Before
After
Actual declaration (Classes.res:2:20-44):Actual declaration (Classes.res:2:20-44): @instanceOf("Dog") type dog = private animal @instanceOf("Dog") type dog = private animal Expected declaration (Classes.resi:2:20-44):Expected declaration (Classes.resi:2:20-44): @instanceOf("Dog") type dog = private animal @instanceOf("Dog") type dog = private animal Their runtime representations differ:The declarations read the same, but a type they mention differs: Actual declaration: instances of Dog, a subclass of Animal Actual declaration: instances of Dog, a subclass of Animal Expected declaration: instances of Dog 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.
6176c4d28 · cknitt + Claude · Oct 30, 2026, 16:06 UTC

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.

modifiedDiagnostic @tag names with a quote, a backslash and a non-ASCII letter

module M: {  @tag("k\"ind") type t = A({x: int}) | B({y: int})} = {  @tag("typ\\é") type t = A({x: int}) | B({y: int})}
diagnostic
Before
After
Actual declaration (tag_name_escaped.res:6:18-51):Actual declaration (tag_name_escaped.res:6:18-51): @tag("typ\é") type t = A({x: int}) | B({y: int}) @tag("typ\\é") type t = A({x: int}) | B({y: int}) Expected declaration (tag_name_escaped.res:4:18-51):Expected declaration (tag_name_escaped.res:4:18-51): @tag("k"ind") type t = A({x: int}) | B({y: int}) @tag("k\"ind") type t = A({x: int}) | B({y: int})