Skip to content

Resolve untagged declaration shapes and literals once - #9017

Open
cknitt wants to merge 13 commits into
untagged-dispatchfrom
untagged-declarations
Open

cknitt wants to merge 13 commits into
untagged-dispatchfrom
untagged-declarations

Conversation

@cknitt

@cknitt cknitt commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Stacked on #9015. Declaration-side follow-ups from the architecture reviews of the untagged variant work, one commit each:

  1. Builtin types state their shapes.
    • Predef declares int, float, bigint, string, bool, dict, array and promise with the type_shape an annotation would give them. The list of builtin paths in get_block_type_from_typ is gone.
    • @typeOf("number") type count = int is now a restatement.
    • @typeOf("string") type t = int is now reported as a contradiction.
    • An interface's @typeOf("number") type t accepts type t = int.
  2. The literal of a constructor without @as is resolved once.
    • The layout stores it: the name, or 0 for []. Every reader takes it as is, with no None case each.
    • Variant_runtime.matchable_tag and to_matchable_tag are gone.
    • Messages that talk about the declared @as read the constructor's annotation.
    • As a consequence, A and @as("A") A agree across an interface and in a coercion, and constant folding compares any two constructors by runtime value.
    • No JavaScript output changes.
  3. @notUndefined on type abbreviations.
    • It follows the rule of @instanceOf/@typeOf: it can refine a type whose representation the compiler doesn't see, or restate that a type can't be undefined. A restatement keeps the payload kind, so @notUndefined type count = int is still a number case.
    • Interface inclusion and the declaration now share one check, Typeopt.undefinedness. For that, get_unboxed_type_representation moves from Typedecl to Ctype.
  4. Constant folding compares literals by runtime value in every branch of Lambda.switch. A coercion now accepts @as(1.) for @as(1.0) or @as(1), and a folded match on the coerced constant used to pick the wrong arm (found by Codex).
  5. One check per level of a declared shape. Interface inclusion compares classes and kinds and @notUndefined in Includecore.compare_runtime_annotations; Includemod.not_undefined_mismatch is gone. check_invariant moves from Ast_untagged_variants to Variant_layout, its only caller.

The spec (compiler/ml/UntaggedVariants.md) is updated in rules 2, 4, 5 and 14. Rule 5, now "Declared runtime shapes", covers @notUndefined, @typeOf and @instanceOf together, with their errors and interface checks; the former rule 13 on @notUndefined is merged into it, and the rule on @tag and interfaces becomes rule 13.

New tests:

  • untagged_builtin_alias_test.res: an annotation restating int, and an interface stating @typeOf("number") over int.
  • as_constructor_name_test.res: coercion and interface inclusion between A and @as("A") A, and folded matches of coerced constants.
  • not_undefined_abbreviation_test.res: a refined abstract type isn't boxed in option, and a restated builtin keeps its kind.
  • ounit_lambda_constant_tests.ml: constructors compare by runtime value when folded.
  • Fixtures:
    • runtime_annotation_contradicts_builtin.res;
    • not_undefined_on_option.res;
    • not_undefined_on_functor_parameter_type.res;
    • invalid_attribute_not_undefined.res, now a record, since @notUndefined type t = int is valid.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t

@cknitt
cknitt added this pull request to stack #9016 October 24, 2026 10:46
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Changelog

💅 Polish

  • Allow @notUndefined on type abbreviations, and @typeOf restating a builtin type's kind, as in @typeOf("number") type count = int; a constructor A and @as("A") A now agree across interfaces and coercions.

Semantic Diff

13 commits · Full page

Let builtin types state their runtime shapes

601022f · cknitt + Claude · Oct 24, 2026, 10:30 UTC

Why

Payload kinds had two sources: the shapes declarations state, and a list of builtin paths in get_block_type_from_typ. Builtins were outside the generic checks, so an annotation could not restate a builtin's kind and an interface could not state it over a builtin implementation.

The declarations of int, float, bigint, string, bool, dict, array and promise in Predef state their shapes, and only tuples and function types are classified structurally. @typeOf("number") type count = int is a restatement, @typeOf("string") type t = int a contradiction, and an interface's @typeOf("number") type t accepts type t = int.

Diagnostic modified (annotation over a builtin)

@typeOf("string") type t = int

diagnostic

-@typeOf on a type abbreviation must describe an abstract type, or a type with the same annotation. The abbreviated type already has a runtime representation the compiler knows, or can be undefined.
+This annotation, @typeOf("string"), contradicts the abbreviated type, whose values are recognized by @typeOf("number").

Diagnostic modified (contradicting annotation)

@typeOf("symbol") type sym
@typeOf("number") type t = sym

diagnostic

-This annotation, @typeOf("number"), contradicts the abbreviated type's annotation, @typeOf("symbol").
+This annotation, @typeOf("number"), contradicts the abbreviated type, whose values are recognized by @typeOf("symbol").
Resolve the literal of a constructor without @as once

59ec477 · cknitt + Claude · Oct 24, 2026, 10:40 UTC

Why

"A constructor without @as is its name" was re-derived wherever a tag was read: in the matching facts, constant folding, code generation, the duplicate-literal check, coercions and gentype, each with its own None case, and [] special-cased in several of them.

The layout stores every constructor's literal, the name when there is no @as and 0 for []. Readers take it as is, and matching compares tags without the widening to an optional literal. Messages that name the declared @as read the constructor's annotation. Since the literal is what the value is, A and @as("A") A now agree across an interface and in a coercion, and constant folding compares any two constructors by their runtime values.

Representation modified (constructor tag in the layout)

type t = A | @as("b") B

layout

-A -> {name: "A", literal: None}
-B -> {name: "B", literal: Some (String "b")}
+A -> {name: "A", literal: String "A"}
+B -> {name: "B", literal: String "b"}

Review

  • Does this change generated JavaScript? No snapshot changed: the resolved literal is the value every reader computed before.
Allow @notUndefined on type abbreviations

c3c1c9d · cknitt + Claude · Oct 24, 2026, 10:46 UTC

Why

@instanceOf and @typeOf can refine or restate the type an abbreviation stands for, while @notUndefined was rejected on every abbreviation, even of an abstract type it could refine.

@notUndefined on an abbreviation follows the rule of the other annotations: it can refine a type whose representation the compiler does not see, or restate that the type cannot be undefined, keeping a payload kind the type has. The check whether a type can be undefined is shared with interface inclusion (Typeopt.undefinedness); get_unboxed_type_representation moves to Ctype so that Typedecl can call it.

Diagnostic modified (@notUndefined on an abbreviation)

@notUndefined
type t = option<int>

diagnostic

-@notUndefined can only be used on abstract types
+@notUndefined on a type abbreviation must describe an abstract type, or a type with the same annotation. The abbreviated type already has a runtime representation the compiler knows, or can be undefined.

Diagnostic modified (@notUndefined on a record)

@notUndefined
type t = {x: int}

diagnostic

-@notUndefined can only be used on abstract types
+@notUndefined can only be used on abstract types and type abbreviations, whose runtime representation it describes.

JS modified (refined abstract type)

type opaque
@notUndefined type refined = opaque
let wrap = (x: refined) => Some(x)

js

-error
+function wrap(x) {
+  return x;
+}
Fold matches of constructors by their runtime literals

34dd9df · cknitt + Claude · Oct 24, 2026, 12:02 UTC

Why

A coercion accepts constructors whose literals are spelled differently but are the same value, such as @as(1.) for @as(1.0), while a match on a constant constructor was folded by comparing tags structurally, so the coerced constant missed its arm.

Constant constructors and tagged blocks are now found by the runtime value of their literal, as the untagged dispatch already does.

JS modified (folded match of a coerced constant)

type source = | @as(1.) One
type target = | @as(1.0) One | Two | Three | Four
let isOne = (x: target) =>
  switch x {
  | One => true
  | _ => false
  }
let r = isOne((One: source :> target))

js

-let r = false;
+let r = true;
Fold matches of numeric constants by their runtime value

6978c6d · cknitt + Claude · Oct 24, 2026, 16:46 UTC

Why

A constant constructor represented by an int is lowered to an int, and a match on it was folded by comparing with the int tags of the matched variant only. A coercion accepts @as(1) for @as(1.), so the coerced constant missed its arm.

The int is compared with every constant's literal by value.

JS modified (folded match of a coerced int constant)

type source = | @as(1) One
type target = | @as(1.) One | Two | Three | Four
let isOne = (x: target) =>
  switch x {
  | One => true
  | _ => false
  }
let r = isOne((One: source) :> target)

js

-let r = false;
+let r = true;
Move the variant invariant check next to the layout

565d96c · cknitt + Claude · Oct 24, 2026, 16:23 UTC

check_invariant checks a layout, and Variant_layout, which derives the layout, is its only caller; Ast_untagged_variants keeps the attributes and errors. Its doc comment, which described another function, now says what it checks.

Compare every runtime shape claim of an interface in one place

f851d1a · cknitt + Claude · Oct 24, 2026, 16:25 UTC

Interface inclusion compared classes and kinds in Includecore and @notUndefined in Includemod, the two levels of one declared shape. Includecore.compare_runtime_annotations now checks both; the messages are unchanged.

Merge the @notUndefined rule into the rule on declared shapes

d14444b · cknitt + Claude · Oct 25, 2026, 05:11 UTC

@notUndefined, @typeOf and @instanceOf declare levels of one stored shape, checked on abbreviations by one entry point and across interfaces by one comparer. Rule 5 now describes the three together, with their errors and interface checks; rule 13 is gone and the rule on @tag and interfaces becomes rule 13.

Classify payload types once for every reader of runtime shapes

63b3da2 · cknitt + Claude · Oct 25, 2026, 14:05 UTC

Why

Untagged payloads, annotations on abbreviations and interfaces each classified types their own way, so the latter two rejected annotations that only restate what a function type or a record is.

Variant_layout.payload_kind gives the declared shape of a type, or the kind its structure gives: a tuple is an array, a function type a function, a record an object. get_block_type, check_manifest_annotation and Includecore.compare_runtime_annotations all use it.

Typing rules changed (an annotation restating a structural kind):

@typeOf("function") type callback = int => int
module M: { @typeOf("object") type point } = { type point = {py: int} }

rule · Before: an annotation on an abbreviation agrees with the type's · After: both compare with the payload kind of rule 4, which also

BeforeAfter
$$\begin{array}{l}\color{red}{\mathrm{declared}\ \mathrm{shape};\mathrm{an}\ \mathrm{implementation}\ \mathrm{states}\ \mathrm{the}\ \mathrm{shape}\ \mathrm{it}\ \mathrm{declares}\ \mathrm{or}} \\ \color{red}{\mathrm{abbreviates}.}\end{array}$$ $$\begin{array}{l}\color{green}{\mathrm{covers}\ \mathrm{tuples},\mathrm{function}\ \mathrm{types}\ \mathrm{and}\ \mathrm{records}.}\end{array}$$

Diagnostic changed (an abbreviation of a function type):

@typeOf("string") type cb = int => int

diagnostic

-@typeOf on a type abbreviation must describe an abstract type, or a type
-with the same annotation. ...
+This annotation, @typeOf("string"), contradicts the abbreviated type,
+whose values are recognized by @typeOf("function").
Read the three runtime annotations on one path

50e1ecb · cknitt + Claude · Oct 25, 2026, 14:09 UTC

Why

@notUndefined was parsed, checked on declarations and checked on abbreviations apart from @instanceOf and @typeOf, so each step had two versions, and a type could carry it beside the others.

process_runtime_annotation returns Not_undefined as well, so one parse feeds declaration_shape, the check for represented types and Typedecl.check_manifest_annotation, which now checks all three on an abbreviation. Typeopt.abstractness takes a type and is what undefinedness falls back to.

Diagnostic changed (two of the three annotations):

@notUndefined @typeOf("string") type t

diagnostic

-none
+A type can have only one of @notUndefined, @instanceOf and @typeOf.
+@instanceOf and @typeOf already imply @notUndefined.
Count untagged payload kinds in one table

33b9f36 · cknitt + Claude · Oct 25, 2026, 14:11 UTC

Why

check_invariant kept a counter per payload kind and rechecked all of them after every case, with an error variant per kind.

The kinds seen so far are one table, classes keyed by name, and a second case of a kind is reported where it is added. AtMostOneOfKind replaces the eight AtMostOne* errors and keeps their messages.

See through private abbreviations and JavaScript object types

da8baf1 · cknitt + Claude · Oct 25, 2026, 15:00 UTC

Why

The structural classifier did not expand private abbreviations, so private r of a record was unknown while private string was a string, and it called JavaScript object types unknown although their typeof is always "object". Contradictions with an array type printed it as @typeOf("object").

payload_kind expands private abbreviations and classifies {"x": int} as an object. An annotation contradicting an array type names Array.isArray, and @instanceOf over an array type has an error of its own.

Typing rules changed (payload kinds of rule 4):

type r = {x: int}
type pr = private r
@unboxed type t = A(pr) | B({"y": int}) | S(string)

rule

BeforeAfter
$$\begin{array}{l}\color{red}{\texttt{pr}\ \mathrm{and}\ \texttt{\{"y": int\}}\ \mathrm{are}\ \mathrm{unknown}\ \mathrm{payloads}}\end{array}$$ $$\begin{array}{l}\color{green}{\mathrm{both}\ \mathrm{are}\ \mathrm{object}\ \mathrm{payloads},\mathrm{so}\ \mathrm{only}\ \mathrm{one}\ \mathrm{of}\ \mathrm{them}\ \mathrm{may}\ \mathrm{be}\ a\ \mathrm{case}}\end{array}$$

Diagnostic changed (an annotation over a tuple):

@typeOf("object") type t = private (int, int)

diagnostic

 This annotation, @typeOf("object"), contradicts the abbreviated type,
-whose values are recognized by @typeOf("object").
+whose values are recognized by Array.isArray.

Diagnostic added (a class over an array type)

@instanceOf("MyArr") type myArr = private array<int>

diagnostic

-This annotation, @instanceOf("MyArr"), contradicts the abbreviated type,
-whose values are recognized by @typeOf("object").
+@instanceOf("MyArr") cannot describe the values of an array type: they
+are recognized by Array.isArray, and subclasses of Array are not
+supported.
Regenerate the callable-instance test for the typeof dispatch

828bbc4 · cknitt + Claude · Oct 25, 2026, 16:29 UTC

Why

The test added to the bottom of the stack is compiled by this pull request's dispatch, which checks the function arm against the classes.

@codecov

codecov Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.74359% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.31%. Comparing base (141e433) to head (828bbc4).

Files with missing lines Patch % Lines
compiler/ml/lambda.ml 76.92% 6 Missing ⚠️
compiler/ml/ctype.ml 76.47% 4 Missing ⚠️
compiler/ml/typedecl.ml 87.50% 4 Missing ⚠️
compiler/core/lam_compile.ml 71.42% 2 Missing ⚠️
compiler/ml/ast_untagged_variants.ml 90.00% 2 Missing ⚠️
compiler/ml/typeopt.ml 89.47% 2 Missing ⚠️
compiler/ml/variant_layout.ml 96.66% 2 Missing ⚠️
compiler/gentype/translate_type_declarations.ml 91.66% 1 Missing ⚠️
compiler/ml/error_message_utils.ml 50.00% 1 Missing ⚠️
Additional details and impacted files
@@                  Coverage Diff                  @@
##           untagged-dispatch    #9017      +/-   ##
=====================================================
- Coverage              81.32%   81.31%   -0.01%     
=====================================================
  Files                    456      456              
  Lines                  64181    64135      -46     
=====================================================
- Hits                   52193    52151      -42     
+ Misses                 11988    11984       -4     
Files with missing lines Coverage Δ
compiler/core/js_dump.ml 93.11% <100.00%> (-0.02%) ⬇️
compiler/core/lam_compile_const.ml 96.42% <100.00%> (-0.13%) ⬇️
compiler/ml/includecore.ml 79.64% <100.00%> (+0.58%) ⬆️
compiler/ml/includemod.ml 75.88% <ø> (-0.43%) ⬇️
compiler/ml/matching.ml 78.14% <100.00%> (ø)
compiler/ml/predef.ml 100.00% <100.00%> (ø)
compiler/ml/printtyp.ml 70.52% <100.00%> (-0.04%) ⬇️
compiler/ml/variant_coercion.ml 91.95% <100.00%> (-0.44%) ⬇️
compiler/ml/variant_runtime.ml 89.55% <100.00%> (-1.88%) ⬇️
tests/ounit_tests/ounit_lambda_constant_tests.ml 94.87% <100.00%> (+0.27%) ⬆️
... and 9 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cknitt
cknitt marked this pull request as ready for review October 24, 2026 11:47
@cknitt

cknitt commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-25T16:47:19.815705Z c7b1bb5 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a16ad0b18f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compiler/ml/typedecl.ml Outdated
Comment thread compiler/ml/variant_coercion.ml
@pkg-pr-new

pkg-pr-new Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@9017

@rescript/belt

npm i https://pkg.pr.new/@rescript/belt@9017

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@9017

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@9017

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@9017

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@9017

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@9017

commit: 828bbc4

@cknitt

cknitt commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 899176311f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread compiler/ml/lambda.ml Outdated
@cknitt

cknitt commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 7cb6fa9c15

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cknitt
cknitt force-pushed the untagged-declarations branch from 7cb6fa9 to f42bd93 Compare October 24, 2026 20:20
@cknitt

cknitt commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: f42bd93abe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cknitt

cknitt commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

@codex review

@cknitt
cknitt removed this pull request from stack #9016 October 25, 2026 05:37
@cknitt
cknitt added this pull request to stack #9033 October 25, 2026 05:37
@cknitt
cknitt force-pushed the untagged-declarations branch from 36bd191 to b43a9a5 Compare October 25, 2026 13:18
@cknitt

cknitt commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 55c756b307

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cknitt
cknitt force-pushed the untagged-declarations branch from 55c756b to 8d7b161 Compare October 25, 2026 15:08
@cknitt
cknitt force-pushed the untagged-declarations branch from 8d7b161 to 6f87e5b Compare October 25, 2026 15:18
@cknitt

cknitt commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 6f87e5b40f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@cknitt
cknitt force-pushed the untagged-declarations branch from 6f87e5b to c7b1bb5 Compare October 25, 2026 16:30
@cknitt

cknitt commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: c7b1bb5d71

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

cknitt and others added 13 commits October 25, 2026 16:58
Why:
    Payload kinds had two sources: the shapes declarations state, and a list
    of builtin paths in get_block_type_from_typ. Builtins were outside the
    generic checks, so an annotation could not restate a builtin's kind and
    an interface could not state it over a builtin implementation.

The declarations of `int`, `float`, `bigint`, `string`, `bool`, `dict`,
`array` and `promise` in `Predef` state their shapes, and only tuples and
function types are classified structurally. `@typeOf("number") type count =
int` is a restatement, `@typeOf("string") type t = int` a contradiction, and
an interface's `@typeOf("number") type t` accepts `type t = int`.

Diagnostic modified (annotation over a builtin):
Example:
    @typeof("string") type t = int
Before (diagnostic):
    @typeof on a type abbreviation must describe an abstract type, or a type with the same annotation. The abbreviated type already has a runtime representation the compiler knows, or can be undefined.
After (diagnostic):
    This annotation, @typeof("string"), contradicts the abbreviated type, whose values are recognized by @typeof("number").

Diagnostic modified (contradicting annotation):
Example:
    @typeof("symbol") type sym
    @typeof("number") type t = sym
Before (diagnostic):
    This annotation, @typeof("number"), contradicts the abbreviated type's annotation, @typeof("symbol").
After (diagnostic):
    This annotation, @typeof("number"), contradicts the abbreviated type, whose values are recognized by @typeof("symbol").

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Why:
    "A constructor without @as is its name" was re-derived wherever a tag
    was read: in the matching facts, constant folding, code generation, the
    duplicate-literal check, coercions and gentype, each with its own
    `None` case, and `[]` special-cased in several of them.

The layout stores every constructor's literal, the name when there is no
`@as` and `0` for `[]`. Readers take it as is, and matching compares tags
without the widening to an optional literal. Messages that name the declared
`@as` read the constructor's annotation. Since the literal is what the value
is, `A` and `@as("A") A` now agree across an interface and in a coercion, and
constant folding compares any two constructors by their runtime values.

Representation modified (constructor tag in the layout):
Example:
    type t = A | @as("b") B
Before (layout):
    A -> {name: "A", literal: None}
    B -> {name: "B", literal: Some (String "b")}
After (layout):
    A -> {name: "A", literal: String "A"}
    B -> {name: "B", literal: String "b"}

Review: Does this change generated JavaScript? No snapshot changed: the
resolved literal is the value every reader computed before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Why:
    `@instanceOf` and `@typeOf` can refine or restate the type an
    abbreviation stands for, while `@notUndefined` was rejected on every
    abbreviation, even of an abstract type it could refine.

`@notUndefined` on an abbreviation follows the rule of the other
annotations: it can refine a type whose representation the compiler does
not see, or restate that the type cannot be undefined, keeping a payload
kind the type has. The check whether a type can be undefined is shared with
interface inclusion (`Typeopt.undefinedness`); `get_unboxed_type_representation`
moves to `Ctype` so that `Typedecl` can call it.

Diagnostic modified (@notundefined on an abbreviation):
Example:
    @notundefined
    type t = option<int>
Before (diagnostic):
    @notundefined can only be used on abstract types
After (diagnostic):
    @notundefined on a type abbreviation must describe an abstract type, or a type with the same annotation. The abbreviated type already has a runtime representation the compiler knows, or can be undefined.

Diagnostic modified (@notundefined on a record):
Example:
    @notundefined
    type t = {x: int}
Before (diagnostic):
    @notundefined can only be used on abstract types
After (diagnostic):
    @notundefined can only be used on abstract types and type abbreviations, whose runtime representation it describes.

JS modified (refined abstract type):
Example:
    type opaque
    @notundefined type refined = opaque
    let wrap = (x: refined) => Some(x)
Before (js): error
After (js):
    function wrap(x) {
      return x;
    }

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Why:
    A coercion accepts constructors whose literals are spelled differently
    but are the same value, such as `@as(1.)` for `@as(1.0)`, while a match
    on a constant constructor was folded by comparing tags structurally, so
    the coerced constant missed its arm.

Constant constructors and tagged blocks are now found by the runtime value
of their literal, as the untagged dispatch already does.

JS modified (folded match of a coerced constant):
Example:
    type source = | @as(1.) One
    type target = | @as(1.0) One | Two | Three | Four
    let isOne = (x: target) =>
      switch x {
      | One => true
      | _ => false
      }
    let r = isOne((One: source :> target))
Before (js): let r = false;
After (js): let r = true;

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Why:
    A constant constructor represented by an int is lowered to an int, and
    a match on it was folded by comparing with the int tags of the matched
    variant only. A coercion accepts `@as(1)` for `@as(1.)`, so the coerced
    constant missed its arm.

The int is compared with every constant's literal by value.

JS modified (folded match of a coerced int constant):
Example:
    type source = | @as(1) One
    type target = | @as(1.) One | Two | Three | Four
    let isOne = (x: target) =>
      switch x {
      | One => true
      | _ => false
      }
    let r = isOne((One: source) :> target)
Before (js): let r = false;
After (js): let r = true;

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
check_invariant checks a layout, and Variant_layout, which derives the
layout, is its only caller; Ast_untagged_variants keeps the attributes
and errors. Its doc comment, which described another function, now says
what it checks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Interface inclusion compared classes and kinds in Includecore and
@notundefined in Includemod, the two levels of one declared shape.
Includecore.compare_runtime_annotations now checks both; the messages are
unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
`@notUndefined`, `@typeOf` and `@instanceOf` declare levels of one stored
shape, checked on abbreviations by one entry point and across interfaces
by one comparer. Rule 5 now describes the three together, with their
errors and interface checks; rule 13 is gone and the rule on `@tag` and
interfaces becomes rule 13.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Why:
    Untagged payloads, annotations on abbreviations and interfaces each
    classified types their own way, so the latter two rejected annotations
    that only restate what a function type or a record is.

`Variant_layout.payload_kind` gives the declared shape of a type, or the
kind its structure gives: a tuple is an array, a function type a function, a
record an object. `get_block_type`, `check_manifest_annotation` and
`Includecore.compare_runtime_annotations` all use it.

Typing rules changed (an annotation restating a structural kind):
Example:
    @typeof("function") type callback = int => int
    module M: { @typeof("object") type point } = { type point = {py: int} }
Before (rule): an annotation on an abbreviation agrees with the type's
    declared shape; an implementation states the shape it declares or
    abbreviates.
After (rule): both compare with the payload kind of rule 4, which also
    covers tuples, function types and records.

Diagnostic changed (an abbreviation of a function type):
Example:
    @typeof("string") type cb = int => int
Before (diagnostic):
    @typeof on a type abbreviation must describe an abstract type, or a type
    with the same annotation. ...
After (diagnostic):
    This annotation, @typeof("string"), contradicts the abbreviated type,
    whose values are recognized by @typeof("function").

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Why:
    `@notUndefined` was parsed, checked on declarations and checked on
    abbreviations apart from `@instanceOf` and `@typeOf`, so each step had
    two versions, and a type could carry it beside the others.

`process_runtime_annotation` returns `Not_undefined` as well, so one parse
feeds `declaration_shape`, the check for represented types and
`Typedecl.check_manifest_annotation`, which now checks all three on an
abbreviation. `Typeopt.abstractness` takes a type and is what
`undefinedness` falls back to.

Diagnostic changed (two of the three annotations):
Example:
    @notundefined @typeof("string") type t
Before (diagnostic): none
After (diagnostic):
    A type can have only one of @notundefined, @instanceof and @typeof.
    @instanceof and @typeof already imply @notundefined.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Why:
    `check_invariant` kept a counter per payload kind and rechecked all of
    them after every case, with an error variant per kind.

The kinds seen so far are one table, classes keyed by name, and a second
case of a kind is reported where it is added. `AtMostOneOfKind` replaces the
eight `AtMostOne*` errors and keeps their messages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Why:
    The structural classifier did not expand private abbreviations, so
    `private r` of a record was unknown while `private string` was a string,
    and it called JavaScript object types unknown although their typeof is
    always "object". Contradictions with an array type printed it as
    @typeof("object").

`payload_kind` expands private abbreviations and classifies `{"x": int}` as
an object. An annotation contradicting an array type names Array.isArray,
and @instanceof over an array type has an error of its own.

Typing rules changed (payload kinds of rule 4):
Example:
    type r = {x: int}
    type pr = private r
    @unboxed type t = A(pr) | B({"y": int}) | S(string)
Before (rule): `pr` and `{"y": int}` are unknown payloads
After (rule): both are object payloads, so only one of them may be a case

Diagnostic changed (an annotation over a tuple):
Example:
    @typeof("object") type t = private (int, int)
Before (diagnostic):
    This annotation, @typeof("object"), contradicts the abbreviated type,
    whose values are recognized by @typeof("object").
After (diagnostic):
    This annotation, @typeof("object"), contradicts the abbreviated type,
    whose values are recognized by Array.isArray.

Diagnostic added (a class over an array type):
Example:
    @instanceof("MyArr") type myArr = private array<int>
Before (diagnostic):
    This annotation, @instanceof("MyArr"), contradicts the abbreviated type,
    whose values are recognized by @typeof("object").
After (diagnostic):
    @instanceof("MyArr") cannot describe the values of an array type: they
    are recognized by Array.isArray, and subclasses of Array are not
    supported.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
Why:
    The test added to the bottom of the stack is compiled by this pull
    request's dispatch, which checks the function arm against the classes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <[email protected]>
@cknitt
cknitt force-pushed the untagged-declarations branch from c7b1bb5 to 828bbc4 Compare October 25, 2026 17:05

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant