Skip to content

Make typed arrays distinct types - #9050

Merged
cknitt merged 3 commits into
masterfrom
typed-array-private
Oct 26, 2026
Merged

cknitt merged 3 commits into
masterfrom
typed-array-private

Conversation

@cknitt

@cknitt cknitt commented Oct 26, 2026

Copy link
Copy Markdown
Member

Fixes #8955.

Every typed array type was a public abbreviation of TypedArray.t<_>, so the type checker treated Uint8Array.t and Int8Array.t as the same type, while an untagged variant recognized them by their classes. As a result, @unboxed type ta = I8(Int8Array.t) | U8(Uint8Array.t) accepted I8(uint8Array) and then matched it as U8.

Typed arrays are distinct types. Each one is @instanceOf("Int8Array") type t = private TypedArray.t<int> (and so on). It upcasts to TypedArray.t<_> with :>, so the generic TypedArray functions still work through a coercion. To keep view->Int8Array.get(0) working, and to have Int8Array.slice, map, filter etc. return an Int8Array.t, each module includes the new TypedArray.Impl functor. It declares the TypedArray functions for the module's own type, with their documentation.

The functor holds only externals, so it costs nothing at run time:

  • including it emits no code;
  • calls compile to the same direct JavaScript as before;
  • Stdlib_TypedArray gains one empty Impl function.

Editor completion on a typed array now lists the module's own functions with precise types ((t, bigint) => int instead of (t<'a>, 'a) => int).

A general rule closes the same hole elsewhere. An @instanceOf that introduces a class on an abbreviation of an abstract type must be private, as a subclass already had to be. A public abbreviation would make every value of the abstract type an instance, and a second abbreviation could claim another class for the same values.

Breaking (changelog fragment included):

  • Code calling TypedArray.f(view) on a specific typed array must use Int8Array.f(view) or (view :> TypedArray.t<int>).
  • Code mixing typed-array types no longer type-checks.
  • A public @instanceOf abbreviation of an abstract type needs private.

The deprecated …ToEnd functions stay on TypedArray only.

New tests:

  • super_errors/fixtures/typed_array_cases_distinct.res: the Untagged variant puts one typed array in another typed array's case #8955 repro, now a type error.
  • instance_of_public_abbreviation.res: the new rule.
  • untagged_runtime_annotations_test.res: a variant over three typed arrays dispatches to the right case.
  • Stdlib_TypedArrayTests.res: the coercion to TypedArray.t.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t

cknitt and others added 2 commits October 26, 2026 06:36
Why:
    Every typed array type was a public abbreviation of `TypedArray.t<_>`, so
    the type checker took `Uint8Array.t` for `Int8Array.t`, while an untagged
    variant recognized them by their classes: `I8(uint8Array)` type-checked
    and then matched the `U8` case (#8955).

Each typed array type is `private TypedArray.t<_>`: a distinct type that
upcasts to `TypedArray.t<_>` with `:>`. The `TypedArray.Impl` functor gives
each typed array module the functions of `TypedArray` for its own type, with
their documentation, so `Int8Array.get` works on an `Int8Array.t` and
`Int8Array.slice` returns one. The functor holds only externals, so including
it emits no code; `Stdlib_TypedArray` gains an empty `Impl` function.

Typing rules changed (typed arrays):
Example:
    @unboxed type ta = I8(Int8Array.t) | U8(Uint8Array.t)
    let x = I8(Uint8Array.fromArray([1]))
Before (rule): `Int8Array.t` and `Uint8Array.t` are both `TypedArray.t<int>`,
    so `I8(uint8Array)` type-checks and is matched as `U8` at run time
After (rule): they are distinct types; `I8(uint8Array)` is a type error

Diagnostic added (one typed array for another):
Example:
    let x = I8(Uint8Array.fromArray([1]))
Before (diagnostic): none
After (diagnostic):
    This has type: Uint8Array.t
    But it's expected to have type: Int8Array.t

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <ck@cca.io>
Why:
    A class declared with @instanceof on a public abbreviation of an
    abstract type made every value of that type an instance, and two such
    abbreviations could declare the same values instances of two classes
    while the type checker treated them as one type, as the typed arrays did.

`Typedecl.check_manifest_annotation` rejects @instanceof on a public
abbreviation of a type the compiler does not see; a subclass already had to
be private.

Typing rules changed (a class on an abbreviation):
Example:
    type node
    @instanceof("Element") type element = node
Before (rule): a class on a public abbreviation of an abstract type is
    accepted
After (rule): the abbreviation must be private,
    `@instanceOf("Element") type element = private node`

Diagnostic added (a class on a public abbreviation):
Example:
    type node
    @instanceof("Element") type element = node
Before (diagnostic): none
After (diagnostic):
    A class must be declared on an abstract type or a private abbreviation:
    write `= private ...`, so that values of the abbreviated type are not
    instances of Element without a coercion, and another abbreviation cannot
    declare them instances of another class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <ck@cca.io>
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Changelog

💥 Breaking Change

  • Typed arrays are distinct types: Int8Array.t and the others are private abbreviations of TypedArray.t<_>, so an untagged variant can no longer take one typed array for another. Use each module's own functions, such as Int8Array.get, or upcast with (view :> TypedArray.t<int>) to call the TypedArray functions.
  • @instanceOf that introduces a class on an abbreviation of an abstract type requires a private abbreviation: write @instanceOf("Element") type element = private node.

💥 A changelog entry is a breaking change, but no commit says what it breaks (Breaking:).

Semantic Diff

3 commits · Full page

Make typed arrays private abbreviations of TypedArray.t

0839518 · cknitt + Claude · Oct 26, 2026, 06:36 UTC

Why

Every typed array type was a public abbreviation of TypedArray.t<_>, so the type checker took Uint8Array.t for Int8Array.t, while an untagged variant recognized them by their classes: I8(uint8Array) type-checked and then matched the U8 case (#8955).

Each typed array type is private TypedArray.t<_>: a distinct type that upcasts to TypedArray.t<_> with :>. The TypedArray.Impl functor gives each typed array module the functions of TypedArray for its own type, with their documentation, so Int8Array.get works on an Int8Array.t and Int8Array.slice returns one. The functor holds only externals, so including it emits no code; Stdlib_TypedArray gains an empty Impl function.

Typing rules changed (typed arrays):

@unboxed type ta = I8(Int8Array.t) | U8(Uint8Array.t)
let x = I8(Uint8Array.fromArray([1]))

rule · Before: Int8Array.t and Uint8Array.t are both TypedArray.t<int>,

BeforeAfter
$$\begin{array}{l}\color{red}{\mathrm{so}\ \texttt{I8(uint8Array)}\ \mathrm{type}-\mathrm{checks}\ \mathrm{and}\ \mathrm{is}\ \mathrm{matched}\ \mathrm{as}\ \texttt{U8}\ \mathrm{at}\ \mathrm{run}\ \mathrm{time}}\end{array}$$ $$\begin{array}{l}\color{green}{\mathrm{they}\ \mathrm{are}\ \mathrm{distinct}\ \mathrm{types};\texttt{I8(uint8Array)}\ \mathrm{is}\ a\ \mathrm{type}\ \mathrm{error}}\end{array}$$

Diagnostic added (one typed array for another)

let x = I8(Uint8Array.fromArray([1]))

diagnostic

-none
+This has type: Uint8Array.t
+But it's expected to have type: Int8Array.t
Require a private abbreviation for a class on an abstract type

eb66527 · cknitt + Claude · Oct 26, 2026, 06:36 UTC

Why

A class declared with @instanceOf on a public abbreviation of an abstract type made every value of that type an instance, and two such abbreviations could declare the same values instances of two classes while the type checker treated them as one type, as the typed arrays did.

Typedecl.check_manifest_annotation rejects @instanceOf on a public abbreviation of a type the compiler does not see; a subclass already had to be private.

Typing rules changed (a class on an abbreviation):

type node
@instanceOf("Element") type element = node

rule · Before: a class on a public abbreviation of an abstract type is · After: the abbreviation must be private,

BeforeAfter
$$\begin{array}{l}\color{red}{\mathrm{accepted}}\end{array}$$ $$\begin{array}{l}\color{green}{\texttt{@instanceof("Element") type element = private node}}\end{array}$$

Diagnostic added (a class on a public abbreviation)

type node
@instanceOf("Element") type element = node

diagnostic

-none
+A class must be declared on an abstract type or a private abbreviation:
+write `= private ...`, so that values of the abbreviated type are not
+instances of Element without a coercion, and another abbreviation cannot
+declare them instances of another class.
✨ Migrate the deprecated TypedArray functions on a TypedArray.t

53a463e · cknitt + Claude · Oct 26, 2026, 06:52 UTC · new

Why

The migration test applied the deprecated TypedArray.*ToEnd functions to a Uint8Array.t, which is now a type of its own; those functions exist only on TypedArray.

The test's typed array is upcast to TypedArray.t<int>.

Why:
    The migration test applied the deprecated `TypedArray.*ToEnd` functions to
    a `Uint8Array.t`, which is now a type of its own; those functions exist
    only on `TypedArray`.

The test's typed array is upcast to `TypedArray.t<int>`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t
Signed-off-by: Christoph Knittel <ck@cca.io>
@codecov

codecov Bot commented Oct 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.33%. Comparing base (eb08738) to head (53a463e).

Files with missing lines Patch % Lines
compiler/ml/typedecl.ml 75.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #9050   +/-   ##
=======================================
  Coverage   81.33%   81.33%           
=======================================
  Files         456      456           
  Lines       64273    64277    +4     
=======================================
+ Hits        52275    52280    +5     
+ Misses      11998    11997    -1     
Files with missing lines Coverage Δ
compiler/ml/ast_untagged_variants.ml 83.24% <100.00%> (+0.09%) ⬆️
compiler/ml/typedecl.ml 82.86% <75.00%> (-0.05%) ⬇️

... and 2 files with indirect coverage changes

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

@pkg-pr-new

pkg-pr-new Bot commented Oct 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

rescript

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

@rescript/belt

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

@rescript/darwin-arm64

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

@rescript/linux-arm64

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

@rescript/linux-x64

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

@rescript/runtime

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

@rescript/win32-x64

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

commit: 53a463e

@github-actions

Copy link
Copy Markdown

@cknitt
cknitt marked this pull request as ready for review October 26, 2026 07:07
@cknitt

cknitt commented Oct 26, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 26, 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-26T07:09:55.673526Z 53a463e 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

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 53a463ee67

ℹ️ 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".

@cristianoc cristianoc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good catch

@cknitt
cknitt merged commit aff4439 into master Oct 26, 2026
26 checks passed
@cknitt
cknitt deleted the typed-array-private branch October 26, 2026 09:24
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.

Untagged variant puts one typed array in another typed array's case

2 participants