Semantic Diff

#9140 Show signature mismatches as the actual and expected declaration, each under its location

4 commits

25e3dfbbe00cfabda4c97b27f2be28985c94300a..pr-9140

5de3a96a4 · cknitt + Claude · Oct 30, 2026, 11:40 UTC

Show signature mismatches as the actual and expected declaration, each under its location

Why

A signature mismatch printed every enclosing module in full, then the two declarations, then their locations as "file:line:" lines with a label on the next line that read like headings with nothing under them, and the reasons without a break. Which text was where had to be matched up by hand, and declarations that read the same looked like a declaration not including itself. The type checker's errors separate their parts with blank lines and indent the content under a lead-in.

Includemod.report_error now leaves out a module or module type comparison when an item inside it follows, so that only the items that differ are printed, each after a line naming where it is: "In module P:", "In module type P.T:", "In the result of functor P:", "In the parameter X of functor P:", or "At position ..." otherwise, once for items at the same position. Missing items are listed in the order of the expected signature, and in a functor's parameter, where inclusion is reversed, the implementation's side is still the actual declaration.

modifiedDiagnostic type declaration in an interface

// Foo.restype t = {a: int, b: int}// Foo.resitype t = {x: int, y: int}
diagnostic
Before
After
The implementation Foo.resThe implementation Foo.resdoes not match the interface foo.cmi:does not match the interface foo.cmi:Type declarations do not match: Actual declaration (Foo.res:1:1-25): type t = {a: int, b: int} type t = {a: int, b: int}is not included in Expected declaration (Foo.resi:1:1-25): type t = {x: int, y: int} type t = {x: int, y: int}Foo.resi:1:1-25: Expected declarationFoo.res:1:1-25: Actual declaration Fields number 1 have different names, a and x.Fields number 1 have different names, a and x.

modifiedDiagnostic value in a nested module

// Foo.resmodule Inner = {  let value = "not an int"}// Foo.resimodule Inner: {  let value: int}
diagnostic
Before
After
In module Inner:In module Inner:Modules do not match: { let value: string }is not included in { let value: int }In module Inner:Values do not match: let value: string is not included in let value: intFoo.resi:2:3-16: Expected declarationFoo.res:2:7-11: Actual declaration Actual declaration (Foo.res:2:7-11): let value: string Expected declaration (Foo.resi:2:3-16): let value: int

modifiedDiagnostic functor result

module type S = {  type t}module M: {  module F: (X: S) => S with type t = X.t} = {  module F = (X: S) => {    type t = int  }}
diagnostic
Before
After
Signature mismatch:Signature mismatch:Modules do not match: …In module F:Modules do not match: …At position module F: (X) => <here>Modules do not match: …At position module F: (X) => <here>Type declarations do not match: In the result of functor F: Actual declaration (functor_signature_mismatch.res:9:5-16): type t = int type t = intis not included in Expected declaration (functor_signature_mismatch.res:6:36): type t = X.t type t = X.tfunctor_signature_mismatch.res:6:36: Expected declarationfunctor_signature_mismatch.res:9:5-16: Actual declaration

modifiedDiagnostic functor parameter

module M: {  module F: (X: {let a: int}) => {}} = {  module F = (X: {let a: string}) => {}}
diagnostic
Before
After
Signature mismatch:Modules do not match:… the module, then the functor, each pair printed in fullAt position module F: (X: <here>) => ...Values do not match: let a: int is not included in let a: stringinclusion_layout_functor_parameter.res:12:7-19: Expected declarationinclusion_layout_functor_parameter.res:6:7-16: Actual declarationIn the parameter X of functor F: Actual declaration (inclusion_layout_functor_parameter.res:12:7-19): let a: string Expected declaration (inclusion_layout_functor_parameter.res:6:7-16): let a: int

modifiedDiagnostic missing values

module M: {  module N: {    let a: int    let b: string  }} = {  module N = {}}
diagnostic
Before
After
Signature mismatch:Modules do not match:… the module, then module N, each pair printed in full In module N:In module N:The value `b` is required but not providedinclusion_layout_missing_values.res:5:5-17: Expected declarationIn module N:The value `a` is required but not providedinclusion_layout_missing_values.res:4:5-14: Expected declaration The value `a` is required but not provided. Expected declaration (inclusion_layout_missing_values.res:4:5-14): let a: int The value `b` is required but not provided. Expected declaration (inclusion_layout_missing_values.res:5:5-17): let b: string

modifiedDiagnostic runtime representation reason

diagnostic
Before
After
Their runtime representations differ:Their runtime representations differ:the first declaration states values whose typeof is "object",the second declaration states values whose typeof is "string". The actual declaration states values whose typeof is "object". The expected declaration states values whose typeof is "string".

Subst.extension_constructor dropped the location of an extension constructor when writing a .cmi, unlike every other declaration. It keeps it now, so the expected side of an extension mismatch has a position, and reanalyze also records a reference to an exception declared in an interface.

modifiedDiagnostic extension constructor declared in an interface

// Foo.restype t += Boom(string)// Foo.resitype t += Boom(int)
diagnostic
Before
After
Foo.res: Expected declarationExpected declaration (Foo.resi:4:3-13): type t += Boom(int)

Review

  • Is anything dropped that was useful? The enclosing modules printed in full repeated the items below them; the path line names where the items are. A big part was printed as "..." for the same reason, so the size check is gone.
  • Why "actual" and "expected" and not "implementation" and "interface"? The same errors are reported for module type constraints and functor arguments within one file, where there is no interface.
764173726 · cknitt + Claude · Oct 30, 2026, 12:46 UTC

Label the two sides when runtime representations differ

Why

"The actual declaration states values whose typeof is "object"" reads clumsily: a declaration doesn't state values. The two lines now use the labels of the declaration blocks above them, followed by what each side states.

modifiedDiagnostic runtime representation reason

diagnostic
Before
After
Their runtime representations differ:Their runtime representations differ: The actual declaration states values whose typeof is "object". The expected declaration states values whose typeof is "string". Actual declaration: typeof "object" Expected declaration: typeof "string"

modifiedDiagnostic @notUndefined reason

diagnostic
Before
After
Their runtime representations differ:Their runtime representations differ: The expected declaration is @notUndefined, but the actual declaration can hold undefined. Actual declaration: can hold undefined Expected declaration: @notUndefined

modifiedMapping what a declaration states, Ast_untagged_variants.describe_declared_values

representation
Before
After
@instanceOf("Dog") ↦ instances of Dog@instanceOf("Dog") ↦ instances of Dog@instanceOf("Dog") of a subclass ↦ instances of Dog, a subclass of Animal@instanceOf("Dog") of a subclass ↦ instances of Dog, a subclass of Animalarrays ↦ arraysarrays ↦ arrays@typeOf("string") ↦ values whose typeof is "string"nothing ↦ no runtime class or typeof@typeOf("string") ↦ typeof "string"nothing ↦ no stated class or typeof

Review

  • Where do the labels come from? From the side names the caller passes, without their article: "the actual" and "declaration" make "Actual declaration"; the type definition errors of Typedecl get "Original definition" and "This definition".
  • Why no period? The lines are labels with a value, like the declaration labels, not sentences.
3a0228cb0 · cknitt + Claude · Oct 30, 2026, 13:18 UTC

Keep the module type comparison around a permutation of its fields

Why

Codex review of #9140: when two module types have the same fields in another order, the innermost symptom is the permutation, which shows no declarations, and leaving out the module type comparison around it left only "Illegal permutation of structure fields". A comparison is now left out only when a later symptom shows the declarations that differ.

The parser gave a module type declaration in a signature no location, unlike one in a structure, so the expected side printed without a position. It now spans from the name to the end, as in a structure.

modifiedDiagnostic fields of a module type in another order

module M: {  module type T = {    let a: int    let b: int  }} = {  module type T = {    let b: int    let a: int  }}
diagnostic
Before
After
Signature mismatch:Signature mismatch: Actual declaration (include_modtype_permutation.res:7:15-10:3): module type T = { let b: int let a: int } Expected declaration (include_modtype_permutation.res:2:15-5:3): module type T = { let a: int let b: int } In module type T:In module type T: Illegal permutation of structure fieldsIllegal permutation of structure fields
42eed6ab0 · cknitt + Claude · Oct 30, 2026, 13:34 UTC

Report the actual side of a module type found by comparing the other way

Why

Codex review of #9140: two module types must be equal, and check_modtype_equiv also includes the expected one in the actual one. An item only the actual module type has is found by that reverse check, but the error called it required and its declaration the expected one. This was mislabelled before too, and the comparison of the two module types printed above it was the only hint, which the new layout leaves out.

The reverse check now adds a Reverse position to the path. Like a functor parameter it swaps the sides, and it is not printed.

modifiedDiagnostic an item only the actual module type has

module M: {  module type T = {    let a: int  }} = {  module type T = {    let a: int    let b: int  }}
diagnostic
Before
After
In module type T:In module type T: The value `b` is required but not provided.The value `b` is only present in the actual declaration. Expected declaration (inclusion_layout_module_type_extra.res:10:5-14):Actual declaration (inclusion_layout_module_type_extra.res:10:5-14): let b: int let b: int