s.replace(re, fn) and replaceAll boxed a typed callback into a dyn
function and called it through the dyn ABI: per match, a dyn argument
array, a dyn node per capture and for the offset, the box's argument
checks back to typed values, and a dyn result converted to a string.
When every parameter the callback declares receives a value of its
declared kind in each match, the box is unobservable (the runtime only
calls it, with `this` undefined), so the backend passes the closure to
scr_regex_replace_fn, which collects the matches as before and calls the
closure through its own ABI. That holds for callbacks returning a string
that take the match string alone, or, with a regex literal whose captures
the compiled-matcher parser reads, captures every match sets (strings),
then the offset (a number) and the subject (a string). Callbacks with
optional, union or rest parameters, `arguments`, or captures some match
leaves unset keep the dyn path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scr_regex_replace_callback freed its output buffer with free() when the
replacer threw after an earlier match had been written, but the buffer
is the body of a runtime string, so the process aborted with "free():
invalid pointer" instead of propagating the exception. It now abandons
the buffer through scr_jb_abandon, which releases the string.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
A generic function, arrow or method whose body returns an array element
or an index-signature entry now returns undefined for a missing one, as
in Node, instead of throwing. Only declarations whose bodies return such
a read change their instance result type.
Fixes#964
Co-authored-by: Quim T. <26749475+espetro@users.noreply.github.com>
298 of the 3008 corpus programs had no measured cost, which left main
three unmeasured programs above the shard harness's 90% coverage floor:
any PR adding a few corpus programs failed shard.test.ts. Rerun
scripts/ci-corpus-costs.mjs over the 120 green Linux corpus shard logs
of main runs 39131161610, 39130670620 and 39126633135 (31b52e215,
aba3c3a97, 572b26ef8), and drop the entry of the renumbered
5942-local-class-observed-methods.ts. 2989 programs are now measured.
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>
--cachegrind now also runs differing corpus executables (without
arguments, accepting intentional non-zero exits), so emitter changes that
only touch corpus programs get per-function instruction deltas. AGENTS.md
mentions the dynamic-tier symbol rule.
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.
scripts/zero-cost-gate.mjs compiles a fixed good-code set (the runtime
benchmark workloads, the self-hosted compiler, and the TypeScript corpus)
with a baseline and a candidate checkout. It requires byte-identical IR,
LLVM and release executables, and names the first differing IR function,
LLVM function or executable symbol when something changes. Programs that
match the change's pattern signature (--allow) may differ. It also checks
compile time (paired builds, bootstrap interval), binary size, optional
per-function cachegrind instruction counts, and the two-tier I3 symbol
rule: no executable gains dynamic-tier runtime symbols, and fully typed
benchmark workloads define none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
n.toString(radix) passed the radix as a dyn value, so every call boxed a
number and converted it back, and kept scr_json.c's dyn machinery linked
into programs that use nothing else of it.
A number-typed radix now calls scr_num_to_string_radix_f64, which
truncates it as ToIntegerOrInfinity does and holds the formatting that
moved from scr_json.c to scr_lib.c. Other radix types keep the generic
entry point, which converts and then calls the same function.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every closed record written by a typed JSON.stringify called
scr_jb_try_record_source first, an out-of-line call and table probe for
records that almost never keep a source (2% of json-records samples).
The record writer now tests the record-sources constant first, so a
program without a checked conversion that can keep a source emits no
lookup. Where one can, the writer tests the dispose hook inline before
calling: keeping a source installs the hook, so a NULL hook means the
table is empty, and only a program that has kept a source pays the call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every emitted free loaded the runtime's dispose hook and branched on it,
and every runtime array, map and byte-store free made an out-of-line call
to scr_weak_dispose to do the same. The hook has two clients, weak
collections and the kept sources of checked records, and a program that
has neither never installs it.
The emitted frees now test two module constants the emitter defines after
every helper is emitted: whether the program constructs a WeakMap or
WeakSet, and whether some helper can keep a record source (a checked
record builder that notes one, or a JSON schema of a closed record).
Class instances test the first, closed record layouts both; LLVM folds the
test away, so a program without either frees with no hook load at all.
The runtime's hot frees test the hook inline instead of calling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
`const C = zlib.Gzip` stopped at SC2020, and `instanceof zlib.Gzip`
(any zlib class) stopped at SC1090. A const binding of a zlib class is
now a compile-time alias with no runtime slot: `new C()`, `typeof C`,
`C.name` and `C === zlib.Gzip` lower at the use. `instanceof` against a
zlib class asks the runtime for the stream's mode when the operand is a
Transform, tests for a Transform first when it is typed as one of
Transform's bases, and answers false for other static types, as in Node.
Other uses of an alias as a value report SC1090 at the use.
Bodies with thousands of capture boxes compile optnone so code generation
stays linear. There a versioned copy only adds code: nothing hoists the
header loads or removes the checks it was made for.
Float16Array elements are two bytes and convert through the runtime.
Versioned loops bound them in elements against the element count, as for
every other element type, and their loads and stores share the element
alias category, since views of any type may share a buffer. DataView
half-precision accesses are two bytes wide in the guard.
The corpus covers unchecked and checked f16 loops, rounding in the
unchecked copy, a masked index against a short array, a DataView loop that
throws at an odd final offset, and f16 stores observed through integer
views of the same buffer.