`const E = EventEmitter`, `const M = Map`, `const T = TypeError` and
`const R = Readable` stopped at SC2020, and so did `typeof EventEmitter`,
`Readable.name`, `Map.name`, `Error.name` and `===` between builtin
classes. The zlib class alias resolver now covers the error classes,
EventEmitter, the node:stream classes and Map/Set: a const binding of one
(an alias of an alias, or an export used through a named or namespace
import) constructs, extends and tests instanceof like the direct name,
and typeof, .name and === fold for aliases and direct names.
In TypeScript files the alias owns no runtime slot and other value uses
report SC1090 at the use. In JavaScript files a copy of a name or of a
globalThis property keeps its slot, which holds the same value as the
name it copies; builtin module members (`zlib.Gzip`) own none, as before.
Keep both the entry module's DEP0040 link warning and #1303's CommonJS
top-level this object in the module init header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
resolveInferredReturn turns a return statement into a runtime fence
through a Record<string, unknown> view with element writes
(`mutable["kind"] = ...`). The class method census treats a literal
element write as a possible write to every method of that name, so in the
native compiler every method named `kind` or `code` became a dynamic
callback: AstWireFile.kind, called for each AST node access, went through
a property bag and a method value.
Property writes go through the receiver check instead, and no class
instance is assignable to Record<string, unknown>, so those methods are
called directly again. Output of the compiler is unchanged.
The previous change dropped the diagnostics of a function-local class refused during a speculative whole-program type query, expecting the class's own declaration to report them. In JavaScript that declaration only defers them to a runtime fence, so two npm programs that the compile refused (stream-shims and node24-file-url-conversion, both SC1090) began to compile and diverged from Node at run time. The speculative query now leaves the refusal reported, as it was wherever such an analysis already caught the poison, and only stops the poison from escaping the compiler.
The JavaScript crash repro becomes a diagnostics fixture (local-class-type-query-js.js); the corpus program 6102 never crashed the base compiler and is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refused statement in a JavaScript function body was dropped without a diagnostic or runtime fence when the body lowered inside a capture buffer whose owner kept the result. tryLowerExpression lowers the receiver of `(function () { ... }).call(this)` or `.apply(...)` and a computed class base that way: the refused statement (for example `console.log(root === module.exports)`) was missing from the kept function, its diagnostic was handed back to the enclosing statement, and that statement, having lowered cleanly, filed it as an informational runtime-fence entry. The program printed nothing for that statement.
lowerStmts now emits the statement's runtime fence inside a buffer as well (Lowerer.capturedRuntimeFence, the same message deferToRuntimeFence would give), leaving the diagnostics with the buffer's owner. The three JavaScript class initializer sites that skipped their fence under a buffer (static field, instance field, static initializer block) do the same. An owner that discards the candidate discards the fence with it, so every program that compiled before keeps byte-identical IR unless it contained such a dropped statement: all 3006 corpus programs produce the same IR as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The self-hosted compiler refuses a conditional spread of `tiers` over a
LowerResult that already declares the field, so assign it instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shape union's arms all carry a collector header when one of them does.
The emitter applied that rule in one pass over the unions in module order,
so a header that a later union gave a shared arm did not reach the other
arms of an earlier union. With First = A | B and Second = B | C, where only
C is cyclic, B got a header through Second, First's trace helper then
called A's trace function, and A had none: the module failed LLVM
verification with "use of undefined value '@sc_rtrace_r0'".
The rule now repeats until no union adds a header. Corpus
5919-record-unions-shared-arms chains Third = D | A, First = A | B and
Second = B | C, so the requirement passes from Second through First to
Third.
Class dynamic dispatch decides whether a class family keeps a fixed
field layout from the property names a delete can remove. It cached that
set with ??=, but the scan returns null when the program reflects on
properties, so a program that does recomputed it, walking every source
file, for each class in every dispatch wave. Only an undefined value now
means the set has not been computed.
npm-static re-export pruning read and parsed a package's package.json
for every file whose purity it checked, and for every package boundary
an import crossed. Each plan now parses each package.json once.
Output is unchanged. three.js nodes lowering takes 6 s less and an
Effect fixture's frontend load 4 s less.
Stage S2 of the two-tier design. IrFunction gains a tier (absent means
static); lowering assigns it from function bodies, placing functions that
use the dyn vocabulary in the dynamic tier, and the validator enforces
invariant I1: a static function performs no dynamic operation, only
boundary operations (dynFrom, dynCheck, kind tests) and moves. Serialized
IR keeps the field out in this stage and loaders derive it again, so every
IR, LLVM and executable artifact is unchanged.
Every committed refusal and runtime fence becomes a recorded demotion
request attributed to the binding, field, function instance, class or
module the dynamic tier would take over, while the compiler still refuses
and fences as before. `scriptc coverage` reports dynamic-tier statements
(statementsDynamic), `--explain-dynamic[=<name>]` prints the demotion
chains, and `--tier=auto|static-only` is wired through build, run and
coverage for stage S6; both modes compile identically today.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
node:test changes, matching Node 24.22:
- before and after hooks outside any suite run, with the root's context,
whose name is "<root>". They did not run before.
- A module-level before hook runs at its before() call, as Node runs the
hooks of its already-started root at once: its synchronous part runs
before the code after the call, and the first top-level test waits for
an async hook's promise. Every module-level before hook runs, and an
async hook's rejection is not reported as an unhandled rejection.
- Module-level after hooks run after the last top-level test or suite.
- A throwing root before hook fails each top-level test with the first
failure; the test's body and beforeEach hooks do not run, and its
afterEach hooks do. Each top-level suite fails with the error, and the
tests and suites in it are cancelled before they start: they report
without a duration and fail with "test did not finish before its parent
and was cancelled", listed in the failing section in completion order.
- A throwing root after hook reports the entry file as a failed result,
after the top-level results, and lists it in the failing section at the
hook's location. It is not counted, and the exit code is unchanged.
- Hooks record the "file:line:col" of their call for that location.
The root-hooks, root-before-throws and root-after-throws fixtures compare
the reporter output, the order of the hooks against the module code, and
the exit codes with Node, plain and with SCRIPTC_SAN=1.
chalk's callable builder (a function whose prototype chain is set with
Object.setPrototypeOf, with style getters from Object.defineProperties)
already ran on the checked-dynamic model. What failed was the code
around it. Each fix is general and sits on a path that used to end in a
refusal or a runtime fence, or is confined to JavaScript files:
- package.json "imports" inside a --npm-static package resolve to the
shipped JS with the "node" condition hoisted, like the tsgo fs shadow;
- JavaScript object literals that the file reshapes at run time (keyed
writes with runtime keys, delete, Object.defineProperty/ies) map to
the checked-dynamic type, with the literals nested in them;
- a JavaScript class whose only member is a constructor that returns a
value and never reads `this` answers that value from `new` (the return
used to be discarded silently);
- `const { env } = process` binds the data member in JavaScript;
- `o && x` / `o || x` with an always-truthy object, array or function
left operand;
- Number.parseInt/parseFloat over checked-dynamic values apply ToString;
- Function.prototype as a value is one compiler-emitted function;
- calls of String.prototype-only names (bold, italics, ...) on untyped
receivers call the receiver's own or inherited member; a string
receiver throws the refusal at run time.
Programs without these patterns compile to identical bytes: the runtime
benchmark workloads, the self-hosted compiler (LLVM and executable) and
the TypeScript corpus IR were compared against the baseline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Top-level `this` in a CommonJS JavaScript file, also inside top-level arrow functions, read as undefined; Node binds it to the module's original exports object. It is now a per-module object created when the module starts: typeof, truthiness, identity and passing it along (the UMD wrapper `(function (root) { ... })(this)`, CoffeeScript's `.call(this)`) behave as in Node. Exports stay the static export table, so a property access through top-level `this` is a named refusal rather than a write no requirer would see. ES modules keep undefined.
The UMD host test `typeof module !== "undefined" && module.exports` read `module.exports` through the module handle as undefined inside functions, so such modules took the non-CommonJS branch (this is what broke ipaddr.js). `module.exports` used only for its truthiness now answers true when every `module.exports =` in the file assigns an object or function.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`const { inspect } = require("util"); typeof inspect` printed "string", as did `import { inspect } from "node:util"` in an ES module and `typeof promises` from `fs`. JavaScript sources represent builtin values without their own lowering as identity tokens (strings), and typeof answered from that representation. typeof on a builtin import binding in a JavaScript source now answers "function" for exported functions and classes and "object" for exported objects; the binding read has no effects. TypeScript sources compile as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whole-program analyses that query types before reachability is known (the computed-key write census for JavaScript method dispatch, and the runtime-optional array analysis) can name a function-local class whose collection is refused. No statement owns that refusal there, so it escaped as an uncaught PoisonError from collectClassShapeInner and crashed the compiler (the ts-api-utils probe, @typescript-eslint/typescript-estree, and small TypeScript programs). These analyses now run under Lowerer.speculativeTypeQueries: a refused local class is unmappable for the query and its diagnostics are dropped, and the class's own declaration collects it again and reports them if the program reaches it. Collection and body lowering keep their poison handling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`let e = map[k]; if (e === undefined) { e = {}; map[k] = e } e[c] = v` on a `Record<string, Record<string, string>>` compiled, then aborted with "record has no key (typed slot…)" where Node prints the result. Without noUncheckedIndexedAccess a keyed record read keeps its typed trap unless the next statement tests the binding for absence, and only `if (!e)` counted. Comparisons with the missing value now count too (`=== undefined`, `== null`, `!== void 0`, `typeof e === "undefined"` and their mirrored forms) for index-signature record reads whose checker type lacks undefined. Module-scope declarations get the same treatment: their pre-registered global takes the undefined arm before any body reads it, and the initializer lowers as the absence probe. Array reads, annotated bindings and noUncheckedIndexedAccess programs compile as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under noUncheckedIndexedAccess, `let e = map[k]; if (e === undefined) { e = {}; map[k] = e } e[c] = v` failed with SC9001 "recordKeySet receiver: expected record, got union". A runtime-optional binding read with a computed key reaches the record read and write lowerings as its `record | undefined` storage. Those lowerings now hold the receiver, the key and, for writes, the value in temporaries and throw Node's TypeError for an undefined receiver ("Cannot read properties of undefined (reading 'k')", "Cannot set properties of undefined (setting 'k')") after evaluating them in Node's order. Every other receiver builds the same IR as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
node:test changes, matching Node 24.22:
- A describe or suite body may take its SuiteContext, with name,
fullName, filePath, attempt, passed and diagnostic. Its passed is false
until the suite has finished, after its after hooks.
- before and after hooks may take the suite's context, and beforeEach
and afterEach hooks the test's. A test's t.before, t.after,
t.beforeEach and t.afterEach hooks may take the test's context.
- SuiteContext maps to the runner node that TestContext maps to, so the
TestContext members that SuiteContext shares lower the same way.
The suite-context fixture compares the reporter output and the order of
the bodies and hooks with Node, plain and with SCRIPTC_SAN=1.
An object literal asserted to a discriminated union keeps its own record
layout, and that layout can equal another member's layout: in
`{ kind: "return", value, loc } as Stmt`, a non-null `value` gives the
fields of a `throw` member. The value was wrapped as that member, so a
switch on `kind` read it with the `return` member's layout. The compiled
program aborted with an invalid union tag or read past the record; the
native compiler crashed this way while lowering three.js.
The literal's discriminant now selects the member through type
assertions too, as it already did through parentheses and `satisfies`.
`m?.groups?.year` compiles on exec and match results, including a null
match, where the read was refused before. The matchAll row callbacks also
cover findIndex and findLastIndex.
Reshaping an index-signature record into another index-signature type
keeps the source's null prototype, since the two are one object in
JavaScript. A match's groups read through `m?.groups`, or assigned to a
declared type such as `{ [k: string]: string } | undefined`, now prints
"[Object: null prototype]" as in Node, where it printed a plain object.
node:test changes, matching Node 24.22:
- t.filePath is the entry file's path, as Node's entryFile, fixed when the
program is compiled.
- t.attempt is 0, since a test runs once.
- t.passed is false while the body runs and the test's outcome once the
body ends, so afterEach and t.after hooks see it, as Node's pass() and
fail() set it before them.
- t.workerId is Number(process.env.NODE_TEST_WORKER_ID) || undefined.
- t.tags is [], since the tags option has no lowering.
- A test's t.before, t.after, t.beforeEach and t.afterEach hooks are
released when the test ends. Hooks whose closures capture the test's
context no longer keep it alive in a cycle, which the SCRIPTC_SAN=1
audit reported as live closures at exit.
The ctx-members fixture compares the reporter output with Node, plain and
with SCRIPTC_SAN=1.