Semantic Diff

#9039 Compare constructor representations in interfaces and coercions alike

1 commit

d48d577bf67854c0023aa07a2a5a219d015d1f9f..pr-9039

594ff2811 · cknitt + Claude · Oct 25, 2026, 06:54 UTC

Compare constructor representations in interfaces and coercions alike

Why

An interface or a coercion accepted a tagged constructor for an @unboxed one: inclusion compared only the tags of tagged constructors, and the coercion check only the declared @as (#9038). The coercion error also said that the wrong variant was unboxed.

Variant_runtime.same_representation compares two constructors: constant with the same literal, tagged with the same tag, or both untagged. Interface inclusion and the coercion check use it. Type declarations in messages print the @unboxed of an untagged constructor of a type that is not @unboxed itself, and an interface mismatch names the constructor that is @unboxed in only one declaration. The predefined unknown, an @unboxed variant, now has the untagged layout a declaration of it gets, so @unboxed type rec t = unknown = Unknown(_): t still agrees with it.

addedDiagnostic interface with an @unboxed constructor

module M: {  type t = | @unboxed A(int) | B({x: int})} = {  type t = | A(int) | B({x: int})}
diagnostic
Before
After
none; clients read {TAG: "A", _0: 3} as B Type declarations do not match: type t = A(int) | B({x: int})is not included in type t = @unboxed A(int) | B({x: int})Their internal representations differ:case A is @unboxed only in the second declaration.

addedDiagnostic coercion between representations

type a = | @unboxed A(int) | B({x: int})type b = | A(int) | B({x: int}) | Clet x: b = (A(3): a :> b)
diagnostic
Before
After
none; the coerced 3 matches CType a is not a subtype of b

modifiedDiagnostic unboxed configurations

@unboxed type x = One(bool) | Twotype y = One(bool) | Two
diagnostic
Before
After
- Variant x is not unboxed.- Variant y is unboxed.- Variant x is unboxed.- Variant y is not unboxed.

Fixes #9038