Commit Graph
6750 Commits
Author SHA1 Message Date
Chris Tate ae83a53dad Give interfaces extending class-backed protocols checked identity
An interface that extends a class-backed protocol, such as
`interface TupleTypeReference extends TypeReference { getTarget():
TupleType }` over a `Type` that class instances flow into, mapped as a
record when no class declared itself assignable to it. Asserting a base
value to it copied the instance into that record, which has no methods:
the next method call threw "expected function at $.getTarget, got
undefined", or the assertion reported a conversion diagnostic.

Such an interface holds values of its base protocol, so it now keeps the
same checked identity: the assertion keeps the instance, and its methods
and fields read as on the base type.
2026-10-19 23:06:52 -05:00
Malte UblandClaude Opus 5.5 72eab2aa0e Document process.exitCode reads on the limitations page
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:06:01 -07:00
Malte UblandClaude Opus 5.5 c208973d75 Read T[keyof T] as unknown for T bound to an unknown-valued record
Inside a generic body Object.entries(t) over T extends Record<string,
unknown> types its values 'keyof T extends never ? unknown : T[keyof T]'.
An instance whose T maps to the checked-dynamic value now reads both
branches as unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 21:01:37 -07:00
Chris Tate 73da78342c Box URL into checked-dynamic unions beside fetch's Request and Response (#1459) 2026-10-19 22:59:51 -05:00
Chris Tate c17fbcd327 Split emitter.ts's statement, function and module emission into their own files (#1457) 2026-10-19 22:59:27 -05:00
Malte UblandClaude Opus 5.5 5ffc20450e Compile SDK-declared data types, any-valued members and Pick views of classes
Message types from SDK declarations often use shapes scriptc refused:

- `{ [key: string]: any }` and `Dictionary<any>` (also a bare
  `Dictionary<T = any>`): the overflow values are checked-dynamic, like
  `Record<string, unknown>`'s.
- `any[]` members an interface, type literal or class declares, also
  optional or nullable: the checked-dynamic array, as `unknown[]` maps.
  An object literal's inferred `any[]` member keeps its existing story.
- A class a types-only package declares with public fields only (aws-sdk's
  `declare class PresignedPost { url: string; fields: Fields }`, with a
  merged namespace of types): it describes JSON data and is never
  constructed, so it maps structurally like an interface.
- `Pick<C, K>` and `Omit<C, K>` over a program class, when the view exposes
  a method: like an interface the class matches, it keeps a live receiver,
  so calls through the view run on the instance.

A record blocked by a member deep inside it now names that member's path
(`member 'poll().jobs[].handle' has type ...`) instead of the outermost
member only; a member holding `any` is named itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:55:04 -07:00
Chris Tate 6c742a7264 Compile class hierarchies across the CommonJS modules of npm packages (#1412) 2026-10-19 22:53:32 -05:00
Malte UblandClaude Opus 5.5 6f2a7ce814 Share the initializer and top-level binding shapes with the class hoisting
The self-hosted compiler needs the same record type on both sides of the
call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:52:10 -07:00
Chris Tate ce1010a531 Split emitter.ts's statement, function and module emission into their own files
emitter.ts had grown to 11.6k lines, and its import block and its large
emission methods were edited by most changes to the LLVM backend, so
concurrent changes conflicted there: 17 merges into main resolved it in
two weeks, more than any other compiler file. 26 LlEmitter methods move,
as is, into functions of the emitter in four files next to it:
emit-module.ts (module parts, helper and library definitions),
emit-statements.ts (the statement switch, try/catch, switch, loop copies),
emit-function.ts (function bodies and generator and async scaffolding) and
receivers.ts (receiver reads and the borrow analyses). Each method keeps
a one-line stub in the class, so callers are unchanged, and imports that
only the moved bodies use move with them.

The move changes no behavior: the zero-cost gate compiles the benchmarks,
the self-hosted compiler and the corpus to identical IR, LLVM and
executables.
2026-10-19 22:48:39 -05:00
Chris Tate 13cdc77ad0 Match class capsules in union fields of checked unions (#1456) 2026-10-19 22:48:20 -05:00
Chris Tate a1b077e16f Check private fields of function-local classes per class evaluation
Each evaluation of a function-local class creates its own private names,
while one compiled class serves every evaluation. A JavaScript class's
prototype methods are closures per evaluation whose `this` is the
call-site value, so a method taken from one evaluation could be called
with another evaluation's instance (`a.next.call(b)`, a prototype read,
Reflect.get with a receiver). Those calls read and wrote the other
instance's fields, and `#x in this` answered true. Reads and writes of
`this.#x` inside the closures also went through the property bag under
the key "#x" instead of the native field.

Private fields on a checked `this` now go through a brand check: the
receiver must be an instance of the class or a subclass whose class
environment is the closure's own evaluation, which the class's own
binding names. A matching receiver reads and writes the native field;
any other receiver throws Node's TypeError for the read or the write
(after the assigned value evaluates), and `#x in this` answers false.
Method values and accessors taken from typed instances of these classes
resolve to the instance's own evaluation's prototype functions instead of
a native thunk shared by every evaluation.

The classes this applies to are narrowed to the shapes the check covers:
JavaScript class declarations whose private members are instance fields
used only on `this`. Private methods and accessors, class expressions,
and TypeScript classes keep the "function-local classes with private
brands" refusal.
2026-10-19 22:45:03 -05:00
Chris Tate 064b072f94 Match class capsules in union fields of checked unions
A checked conversion to a union of object types tests each member's
fields with the members' matchers. For a field of type `C | undefined`
holding a class instance, the union's matcher found the instance's
capsule, which is not a capsule of the union itself, and answered no
(or matched the instance's plain-object view, which no class arm
accepts), so no member matched and the conversion threw a TypeError.
workerData read as such a union failed this way in every worker. A union
now matches capsules of other types through its arms, where the class
arm accepts the instance.
2026-10-19 22:44:28 -05:00
Chris Tate b9be4d51c7 Map classes reached only through import type as their structural types (#1454) 2026-10-19 22:43:36 -05:00
Malte UblandClaude Opus 5.5 52f9bf284f Destructure a match-or-empty-string union by code point
`const [hit] = re.exec(s) || ""` destructures `RegExpExecArray | string`:
the string arm answers its code point at each position, or undefined past
its end, as the string iterator does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:40:56 -07:00
Chris Tate 27017ef0af Map classes reached only through import type as their structural types
A class whose module the program reaches only through `import type`
never registers, and its instance type stayed unmapped: a field such as
`items: Emitted | undefined` reported SC2009, and the class holding it
SC1090. The program holds no instance of such a class, so its instance
type now maps as the structural type it describes, like an interface:
fields of the type hold undefined, object literals and arrays of them.
A JavaScript class outside the program keeps the unmapped type, so its
JSDoc-annotated slots still take the dynamic value.
2026-10-19 22:37:25 -05:00
Chris Tate 0afea2639d Print holes in arrays reached through unknown as Node does (#1451) 2026-10-19 22:34:57 -05:00
Malte UblandClaude Opus 5.5 3d8c7259a0 Hoist lazily initialized ES module classes and follow on-demand workspace imports
A class a bundled ES module's __esm initializer stores into a module
binding (`Multi = class extends import_stream.Readable { … }`) becomes a
class declaration at the same text position: the initializer's body splits
around it into run-once pieces, so a class over a runtime-provided base no
longer has to exist as a stored class value. Only definitions whose
evaluation runs no code (no static fields, static blocks or computed names;
heritage absent, a builtin module member, or a hoisted class), declared
once, assigned only there and read only inside functions after the
assignment move.

npm selection now also scans the program modules a literal import()
reaches, with their static graphs: a workspace package loaded on demand
had its npm dependencies refused as "not part of the compiled module
graph".

`new import_x.C()` respelled `new require("x").C()`, which constructs the
require call; the interop binding now becomes `(require("x"))` there.

util.inherits onto a runtime-provided class (stream.Readable, the from2
and multistream shape) fences where it is written instead of failing in
Node's argument validation at run time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:33:19 -07:00
Chris Tate 60f5c852e0 Print holes in arrays reached through unknown as Node does
A holey array converted to a checked-dynamic value keeps its holes (the
conversion records them in the array's presence map), but the runtime
inspector printed every element: `const u: unknown = a` for
`a[2] = 1` printed `[ undefined, undefined, 1 ]`, where Node prints
`[ <2 empty items>, 1 ]`, and `new Array(3)` through `unknown` printed
`[ undefined, undefined, undefined ]`.

The inspector now prints each run of holes as one `<n empty items>`
entry that counts toward the 100-entry limit, as Node's
formatSpecialArray does and as the typed array inspector already does.
2026-10-19 22:31:09 -05:00
Malte UblandClaude Opus 5.5 762718a254 Compile TLSSocket.isSessionReused, keyed short-circuit assignment and Object.assign into class instances
- TLSSocket.isSessionReused() is declared in the fallback types and lowers
  to false: the TLS client never offers a cached session (no session
  resumption is a documented divergence), so no handshake resumes one.
- `o[k] ??= v`, `||=` and `&&=` through a pure index-signature record with
  a string key use the keyed read and write `o[k]` and `o[k] = v` lower to.
- Object.assign(instance, record) writes the record's fields into the
  class instance's declared fields (same or width-compatible types;
  optional source fields only when present) and answers the instance.
- Object.assign({}, instance) whose result the checker types as the class
  is refused at compile time: the copy is a plain object, so the checked
  cast into the class threw at run time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 20:26:47 -07:00
Chris Tate 80355af8ea Give errors held as any their own stack accessor
In Node every Error has an own, non-enumerable `stack` accessor, shared by
all errors, that formats the stack on first access. Errors that reached
checked-dynamic code read `stack` as undefined: the error encoding carried
a stack only when a typed read had already formatted one. Reads on
instances of Error subclasses found no class arm for it, and assigning
`stack` on a subclass instance, typed or through `any`, did not change
what a typed read answered.

The encoding now lists `stack` first among its own properties as a
non-enumerable accessor. Its getter formats the native error's stack on
first access, so the heading is the name and message at that point, and
its setter stores a string in the native error, which typed reads share.
Any other value replaces the accessor with a data property. Objects that
inherit from an error read the error's stack, and a getter called on any
other receiver answers undefined.

A `stack` read on a checked-dynamic value counts as a stack observation,
so such programs capture frames as they do for typed reads. Dynamic reads
and writes of `stack` on Error subclass instances use the native stack;
non-string values are kept with the instance's own properties.
2026-10-19 22:26:32 -05:00
Chris Tate 62fb17bf8c Keep thrown Maps, Sets, typed arrays and functions as the thrown objects (#1448) 2026-10-19 22:17:41 -05:00
Chris Tate 6ef50ddcc7 List the executable's optional runtime units and defines in one table each (#1446) 2026-10-19 22:16:45 -05:00
Chris Tate dfb4e2a5f1 Derive the runtime features from one table of rules (#1445) 2026-10-19 22:16:17 -05:00
Chris Tate 41ce776e4f Keep one list of runtime pack sources for every matrix (#1447) 2026-10-19 22:15:51 -05:00
Chris Tate d23942ecfa Keep thrown Maps, Sets, typed arrays and functions as the thrown objects
A thrown Map, Set, typed array, Buffer, function or RegExp reached its
catch binding as a type-erased reference, so inspection printed `{}`,
String() gave "[object Object]", instanceof was false and typeof of a
thrown function was "object". These values now take the checked-dynamic
conversion that thrown records and arrays use, so the catch binding is
the thrown object, as in Node.
2026-10-19 22:11:54 -05:00
Chris Tate 1b73e00eb6 Keep one list of runtime pack sources for every matrix
The darwin-arm64 runtime pack matrix repeated the common matrix's base
and optional runtime source lists and its variantsFor table, so a new
runtime source needed the same line in both files. The common matrix
now exports BASE_RUNTIME_SOURCES, OPTIONAL_RUNTIME_SOURCES and
variantsFor, and the darwin-arm64 matrix imports them. A comment on the
lists notes that their order is the link order of an executable's
runtime units. Both matrices evaluate to the same objects as before.
2026-10-19 22:10:46 -05:00
Chris Tate fe32ee3e69 Read ThreadSlots values with the layout they are stored with (#1443) 2026-10-19 22:09:58 -05:00
Chris Tate 87895c340e Test the undici shims against a peer that sends malformed responses
The undici5 and undici7 npm cases now also run peers.ts against Node with
the vendored undici: header values with CR or LF, chunk-size lines that
are valid, malformed, overflowing or 1 MiB long, and fetch bodies that
are endless (read once, then cancelled), gzipped, truncated, empty,
corrupt, cut off, aborted after the headers, redirected while endless,
or carry six content codings. The undici5 case leaves out the Host
values, which undici 5.12 writes into the request head, and the two
fetch cases where undici 5.12 differs (six codings, and an endless
redirect, which fails with a TypeError in 5.12).

package-shims.test.ts compiles two programs in copies of the cases: one
reads 64 MiB chunk-size lines and 64 MiB plain and gzipped fetch bodies
from a separate Node peer and checks that RSS stays under 48 MiB (plain
lane), the other checks that the 5.x profile rejects a Host value with
CR or LF with undici 5.29.0's error.
2026-10-19 22:08:08 -05:00
Chris Tate 1d007024ef List the executable's optional runtime units and defines in one table each
compileCInternal built the program's command line from a chain of
conditional spreads, one per optional runtime unit, and wrote the
optional -D flags out twice (the program build and the runtime-object
flags). RUNTIME_UNITS now lists the optional runtime inputs in link
order, one entry per unit: the sources and the condition that selects
them, or a small function for the entries that add vendor objects,
include directories or platform libraries. RUNTIME_DEFINES lists the
optional definitions once, and both flag lists use it. The command
lines keep their order, so the executables do not change.

The comment fragment before the process-events unit, left from a
removed zlib entry, is dropped.
2026-10-19 22:06:09 -05:00
Chris Tate 1af7a862ce Keep stored cycle functions and self-referencing helper copies refused
Two fixes for the CommonJS cycle check and the bundled-chunk splitter:
- During a cycle's initialization, a read of a partner's exported
  function is safe only where the value neither runs nor stays: an
  expression statement, typeof, void, !, an equality operand, or the test
  of if, while, do and ?:. A const alias (`const f = binding.fn`), an
  object or array literal, a return, or an argument can be called before
  the cycle settles, so these keep SC1016 (Node throws when the function
  reads a binding in its temporal dead zone). A top-level const that only
  publishes the function in an export statement, as the static rewrite
  spells an export-table re-export, stays a read.
- A copied helper whose initializer is a named function expression
  reaches itself through its own name. Each chunk gets its own copy, so
  that name must also be used only as a callee; otherwise the chunk keeps
  the package refused with the existing reason.
2026-10-19 22:05:25 -05:00
Chris Tate 2b33d25a7e Derive the runtime features from one table of rules
ir/runtime-features.ts kept three parallel lists that every new runtime
feature extended at the same place: the RuntimeFeatures interface fields,
the all-false initializer, and the chain of libCall checks in the scan.
They are now one table, RUNTIME_FEATURE_RULES, with one entry per
feature, sorted by name. An entry lists what sets the feature: a
module-level initial check, libCall names and name prefixes, libCall and
expression predicates, and IR type kinds. A libCall or type that needs
several units appears in each unit's entry. RuntimeFeatures, the scan and
its early exit derive from the table; noRuntimeFeatures lists every
feature once, and TypeScript rejects it when it misses one.

Every corpus program gives the same features as before (3217 programs
compared field by field on their IR), and the module still lowers
statically when the compiler compiles itself.
2026-10-19 22:03:11 -05:00
Chris Tate 2c6744e97d Merge main 2026-10-19 22:02:31 -05:00
Chris Tate d07188d250 Merge base 2026-10-19 22:00:59 -05:00
Chris Tate 6dd82db8ff Compile module exports that pass this on with apply or call 2026-10-19 21:59:58 -05:00
Malte UblandClaude Opus 5.5 33588981ae Bake mixed JSON tables and destructure unions of arrays and tuples
JSON documents whose rows mix strings and numbers (`(string | number)[][]`,
`(string | number[])[][]`, the character-set and code-point tables of
iconv-lite and tr46) now bake: comptime literals take union-typed elements
and pick each value's arm from its JSON kind and contents.

Array destructuring of a union of array and tuple types
(`const [m, a, b] = re.exec(s) || ["", "", ""]`) reads each position from
the arm the value holds: a tuple arm answers its field or undefined past
its end, an array arm its element or undefined past its end.

Unions of tuple types no longer pick the checker's `length` literal as a
discriminant: tuple shapes have no such slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:59:40 -07:00
Chris Tate d04b7a5cf2 Read ThreadSlots values with the layout they are stored with 2026-10-19 21:59:15 -05:00
Malte UblandClaude Opus 5.5 f176de2124 Construct CommonJS constructor functions imported through a member alias
A package that exports a constructor function whole and also as a named member (`exports = module.exports = Client; exports.StatsD = Client`, the statsd-client shape) could not be constructed from TypeScript: `new StatsD(...)` was refused as "constructing values other than classes declared in the program", and a bare reference had "no lowering".

- `exports = module.exports = value` is now the module's whole export, and it rebinds `exports` too, so members attached through `exports` afterwards are exported. The fresh-table reset `exports = module.exports = {}` keeps its own handling.
- An `import { name }` or a top-level `const { name } = require(...)` of a member written once as `exports.<name> = <binding or primitive literal>` on such a module reads that value, following `module.exports = require("./x")` re-exports. The checker gives these bindings a synthesized property symbol, so the module's statements answer.
- Construction then takes the existing checked path for JavaScript functions: prototype methods, `util.inherits` between two constructor functions, and `Parent.call(this, ...)` behave as in Node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:58:56 -07:00
Malte UblandClaude Opus 5.5 4e0e7fc1af Box URL into checked-dynamic unions beside fetch handles
fetch's input under @types/node is string | URL | Request. A union with a
checked-dynamic arm collapses to the checked-dynamic representation only
when every sibling survives the collapse; URL did not qualify, so the type
(and anything typed as typeof globalThis.fetch, such as a custom fetch cast
through unknown) did not compile. URL's handle boxes by reference, keeps its
accessors and setters, answers instanceof URL and checks back out to the
typed value, so URL arms now join the collapse. A fetch() input typed
string | URL also converts through the checked-dynamic input path instead
of being narrowed to a string, which threw on the URL arm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:55:18 -07:00
Malte UblandClaude Opus 5.5 c0e2e69a21 Record order baselines for the alias cases and corpus programs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:54:12 -07:00
Malte UblandClaude Opus 5.5 297ec38f07 Promisify functions whose callback is optional
`util.promisify(client.close.bind(client))` over `close(callback?: (error?: Error) => void)` was refused because the callback parameter is `callback | undefined`. The promisified function now takes the union's function arm and passes the callback into the union.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:53:12 -07:00
Malte UblandClaude Opus 5.5 3a9c13a244 Format the alias tests and drop the merged duplicate moduleNs case
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:53:03 -07:00
Malte Ubl 6a61eced36 Merge remote-tracking branch 'origin/opt/npm-cjs-exports' into opt/npm-polyfill-shims 2026-10-19 19:52:09 -07:00
Malte UblandClaude Opus 5.5 2efa5d9411 Fold global detection typeof tests and follow var builtin aliases
A condition built from typeof tests whose answers are static (an
undeclared global is "undefined", the global object and process are
"object") folds, so the untaken arm of `typeof window < "u" ? window : …`
never lowers, and a detection chain used as a receiver resolves to the
global object it selects. JavaScript files no longer report a name the
file feature-tests with typeof as undeclared. `typeof global.setTimeout`
answers "function" instead of the identity token's "string".

`var pna = process` (process-nextick-args once it compiles as process)
and `var Transform = require("stream").Transform` alias like their const
spellings when nothing reassigns them; before, the first silently lost
process's members.

Also reads symbol declarations directly in the CommonJS accessor
snapshot path, after the checker helper's removal on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:52:05 -07:00
Malte UblandClaude Opus 5.5 f80e4c1a09 Compile polyfill packages that are Node builtins as the builtins
isarray, safe-buffer, process-nextick-args, inherits and util-deprecate
evaluate to a Node builtin on Node 24 at their checked major versions, so
a require of one compiles as that builtin expression at the require's
position: from an opted-in package's files (the installed version where
Node's resolution finds it), from the program's own CommonJS files, and
through an esbuild __commonJS chunk whose pnpm path key names the package
and its version. The package's files never join the program.

readable-stream 2.x and 4.x compile as node:stream, the package's own
READABLE_STREAM=disable behavior (a documented divergence). A use of the
entry's export picks the builtin module (member reads of the carried
classes) or stream.Readable (value uses); a mixture keeps the package.
3.x stays as is: its disable branch throws on Node 24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:52:05 -07:00
Malte UblandClaude Opus 5.5 c1a9341739 Name the module truthiness corpus program instead of numbering it
Its number collided with another open change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:51:39 -07:00
Malte Ubl 28dc1b25a6 Merge branch 'opt/npm-lazy-loaders' into opt/npm-cjs-exports 2026-10-19 19:51:39 -07:00
Malte UblandClaude Opus 5.5 fa012f5da3 Name the corpus programs instead of numbering them
The numbers collided with other open changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:51:31 -07:00
Chris Tate 0b00ecfc23 Read x.constructor on EventEmitter and stream instances
`x.constructor` on a value typed as EventEmitter or a stream class
stopped at SC2020. The runtime now records each instance's Node class
name: fs streams as ReadStream and WriteStream, child process stdio as
Socket, worker stdio as ReadableWorkerStdio and WritableWorkerStdio, and
Hash/Hmac stream views as Hash and Hmac (zlib streams, the base classes
and program subclasses already did). The name is the one Node's leak
warning and argument-type errors print, so those messages now name the
same class as Node.

`x.constructor.name` reads that name (scr_emitter_class_name).
`x.constructor === C` for EventEmitter or a stream class tests the
instance's own class: C's interval, none of its direct subclasses', and
C's name, as fs, zlib, crypto, child process and worker streams share the
base classes' vtables. `x.constructor` as a value is the class object of
EventEmitter or the stream class the instance belongs to; for instances
of the other runtime classes and of program subclasses it reports SC1090
at run time.
2026-10-19 21:44:33 -05:00
Malte UblandClaude Opus 5.5 fa64a7cf5a Merge main into opt/bc-node-surface
Main split lower-exprs.ts into expression families; the typeof and __dirname changes move to expressions/typeof.ts and expressions/identifiers.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:42:15 -07:00
Chris Tate 431a8d6874 Merge main 2026-10-19 21:39:39 -05:00