An outer loop's statically ranged accesses (the sieve's flags[i]) are
proved once per entry even when its body holds an inner loop. Versions
nest at most three deep and never inside their own copies, and fixed
checks only cover lengths a buffer can have, so an outer loop never pays
for a guard on an inner loop's unbounded counter.
Typedarray element stores could alias every header and module-global load,
so each access reloaded the length, data pointer and global receiver slot and
kept its own bounds branch. Element accesses now share one TBAA category
(views of any element type may share a buffer), header loads another, and
whole-slot loads of bytes/number globals their own, so LICM keeps them in
registers. The data pointer loads beside the length, ahead of the branch.
ScrBytes lengths and data pointers are fixed at construction, so innermost
synchronous for loops whose accesses use a statically ranged index or an
affine counter index (k * counter + invariant + c) are versioned: one test
before the loop proves every access in bounds (bounding the counter so every
affine operand stays an exact safe integer), and the copy behind it runs
without per-element checks. The checked loop keeps exact out-of-bounds,
NaN, fractional and DataView-throw behavior.
A narrowing guard fails on a compiler bug, and also on a program whose
hand-written type predicate or assertion function (`x is T`,
`asserts x is T`) lied about a value's type: TypeScript trusts those
claims without checking them. The trap messages no longer call the
failure an internal error:
scriptc: a narrowed value does not have the type its narrowing proved
(a type predicate or assertion may be wrong)
with matching wording for downcasts and catch bindings. The guard header
and the IR comments say the same. A native test pins that a lying
predicate and a lying assertion function print their earlier output and
then abort with the message, where they used to crash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
unionNarrow, downcast and caughtNarrow extracted a payload as the arm,
class or caught kind TypeScript's narrowing proved, with no runtime
check. A wrong proof read the payload as another type. Each extraction
now verifies its proof once: the union tag (for a two-arm union, that
the value does not hold the other arm, which LLVM folds under either
dominating test), the target's vtable preorder intervals (shared with
instanceof), or the caught kind or class (shared with caughtTest). A
mismatch calls scr_trap with an internal-error message from a cold
block. A correct program never reaches it: narrowings the frontend
cannot prove already lower to checked extraction, which throws the
catchable TypeError, and analyses treat these expressions as
non-throwing, so a failing guard aborts instead of unwinding.
The guards found a stale narrowing the stability analysis missed: a
binding initialized or assigned from a narrowed read whose narrowing a
callback invalidated inherits that narrowing (`const held: Expr =
current`). main segfaults on that program where Node throws a
TypeError. NarrowingStability now treats such bindings like captured
mutable ones, so their narrowed reads use checked extraction.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An object literal with a computed key that does not fold to a constant,
such as `{ [key]: 1 }`, entered the method replacement census as a write
with an unknown key and no receiver. That marked every class method name
in the program as replaceable, so method calls went through the checked
property lookup. A computed key in an object literal defines a property of
the new object, which is not a class instance, so the census now skips it.
Literals passed to Object.assign or Object.defineProperties are still
judged with their receiver.
Closes#962
Co-authored-by: Quim T. <26749475+espetro@users.noreply.github.com>
A record held in another record's field (`state.parsed`) and passed to a
function that stores a possibly missing array element in it kept its
declared layout, so the store threw where Node stores undefined. The field
read opened the record flow class of the function's parameter.
Such a field read now gets a field slot in the record flow classes. Field
slots join the same field of every member of the owner's class, take the
values literals and assignments store in the field, and widen only when
the owner's class can take the new layout too. Every member of the owner's
class then holds the widened record, so writes and identity are kept. An
owner that is spread, destructured or passed to Object.values or
Object.entries keeps the declared layout.
A constructor that let other code read a field before assigning it (a base
constructor calling an override, or the constructor calling its own method)
read an empty slot, which crashed the executable for string fields. Node
reads undefined.
Fields assigned after construction can expose the instance now admit
undefined. Allocation writes undefined into them. String, number, boolean,
bigint and union fields read as runtime-optional values, and class, array,
record and collection fields use the deferred-init reads, whose member
access throws a TypeError. Classes whose construction cannot expose the
instance before their assignments keep their layouts.
promisify(crypto.randomBytes), pbkdf2, scrypt, hkdf, randomFill, randomInt
and generateKey, and promisify(dns.lookup) and dns.lookupService, over a
named or namespace import, now lower. A call through the binding runs the
callback form with a runtime callback that settles the returned promise
where the callback would run: fulfilled with the callback's value, or
rejected with its error. An argument error the callback form throws rejects
the promise instead, as the throw inside Node's promisify wrapper does.
The crypto promise's value follows the declared result type: a Buffer,
the filled view (the same object), a number, or a checked-dynamic value
where @types/node declares unknown (scrypt, randomFill, randomInt) and for
ArrayBuffer and KeyObject results. The runtime adds a callback closure that
owns the promise and settle adapters for each representation, next to the
existing callback forms in scr_crypto_async.c.
dns.lookup's promise fulfills with { address, family }, or the address
array with options.all, and lookupService's with { hostname, service }, as
Node's custom promisified forms do. The runtime builds the checked-dynamic
result; a declared record type is reached through an async helper that
awaits it and checks the value. The type mapper maps the promisified
lookupService result literal like the promisified exec result.
The fallback declarations gain the matching util.promisify overloads.
TypeScript class fields declared `unknown` or `object` compile since #1147,
but every other field type without a typed representation was still
reported as "'unknown'-typed class fields": `any`, `Array<any>`, and
`WeakMap<object, string>` alike. The refusal now names the declared type,
for example "'any'-typed class fields", so the blocker reads as what it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`const w = words[i]; w[w.length - 1]` and the narrowed form
`typeof t === "string" ? t[t.length - 1] : ...` were refused with SC1090
("string indexing with this index/result shape"), in `if` statements as well
as in conditional expressions. A binding read from an array keeps an
undefined arm in its storage, and a typeof guard narrows only the checker's
type, so the receiver reached the string-index lowering as the storage
union rather than a string.
The refused path now captures the union and the index and reads the string
arm. A missing value performs the checked-dynamic keyed read on the unit,
which throws Node's "Cannot read properties of undefined (reading '<key>')"
with the evaluated key; a value arm the checker excluded fails the checked
extraction. Receivers that compiled before take the paths they took before.
This is path-to-regexp's tokensToRegexp shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`const { 0: path, index } = re.exec(s)` was refused with SC1031 because a
match result lowers to an honest string[] slice, and object patterns only
accepted records. Object patterns whose source the checker types as
RegExpExecArray or RegExpMatchArray now lower each element to the read its
member expression already has: numeric keys are the positional reads of the
equivalent array pattern (defaults included), `index` and `input` are the
result's own properties, and `groups` is the named-group projection when the
regex is statically known. Rest elements, defaults on `index` and `input`,
and other keys keep named refusals. This is path-to-regexp's
regexpToFunction shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A TypeScript subclass of Error that declared `cause` was refused with SC1090,
although the same field in a JavaScript class already compiled. The
declaration now lowers like the JavaScript field: it defines the error's own
enumerable `cause` after super() returns, undefined without an initializer,
replacing a cause passed through the options exactly as Node does. `declare`
and `abstract` declarations are erased and define nothing.
Reads of `.cause` on Error-rooted classes are always the checked-dynamic
property, so only declarations typed `unknown` or `object` are accepted.
Other declared types, parameter properties and accessors named `cause` keep
named refusals. This unblocks the source of @openfeature/core, whose
abstract error base declares `cause?: unknown`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
exec(command, callback) and exec(command, { encoding, maxBuffer },
callback) lower like execFile's callback slice. The command line runs
with /bin/sh -c, as execSync runs it, the call returns the live
ChildProcess, and the error-first callback receives the captured utf8
stdout and stderr. A non-zero exit, including the shell's 127 for a
missing command, reports Node's "Command failed: <command>\n<stderr>":
scr_exec_file takes a shell flag so the message shows the command line.
Under @types/node, child_process.ExecException maps to Error, as
ExecFileException does. Its code member is the child's numeric exit
status, which the runtime error's string code slot does not hold, so
reading it, testing it with `in`, or testing its typeof is a named
refusal.
The test asserts that the TDZ analysis batches symbol queries over the
roots it scans, observed through an SC1013 refusal. Its const binding now
compiles with initialization checks instead of a refusal, so the test
uses a var binding, which keeps the fence and runs the same scan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5973 is also taken by 5973-large-closure-environment.ts on
opt/compile-closures. Move the program and its order baseline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the NullableUnions constructor next to main's mock-timer flag,
and offset the JSON schema's index-signature overflow slot of a record by
its shape's field base, like its declared fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shard harness requires measured costs for more than 90% of the corpus.
Take the costs of programs 5993-5995 from this branch's green Linux shard
logs the way scripts/ci-corpus-costs.mjs computes them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A require cycle was always refused (SC1016). Node answers a require of a
module that is still evaluating with its exports as they stand, while the
compiled bindings alias the final declarations; the two agree when every
module of the cycle assigns all of its exports before its first statement
that can run another module's code, every other use of its export object
is a plain member read, and no module's top-level code can reach its
binding of another member. Each module then initializes exactly as it
would outside the cycle. This admits semver's comparator/range cycle
("hoisted class for cyclic dependency"); other cycles keep SC1016 with a
reason naming the offending statement.
The require reachability scan no longer treats a declaration's own name
as a read (esbuild's `var X = class {...}` before its requires), treats a
plain write of an export member as running only a setter published for it
(on-finished's `module.exports.isFinished = isFinished`), and keeps the
conservative scan for writes of __proto__, __esModule and members of a
replaced export object.
Evidence: corpus 6145 (the cycle), 6142 gains the esbuild layout; the
npm-static harness compiles the published fs.realpath@1.0.0,
on-finished@2.4.1 (with ee-first@1.1.1) and statuses@2.0.1 statically;
program.test.ts pins the TDZ analysis through var bindings plus the cycle
admission and refusals. Preflight/order baselines recorded for the new
fixtures; a full re-record changed no other fixture. Dependencies and
limitations docs describe the supported require forms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scan walked every referenced component from its candidates, whitening
each object it reached and restoring the subgraphs of the externally
referenced ones, so garbage inside a live component was walked a second
time and objects whitened before a later root restored them were walked a
third. markGray already records every gray object in a pass of up to 64k
objects, and the survivors are exactly what the externally referenced
ones reach. So scan now sweeps the recorded entries of the referenced
components once, restores from each one that is still gray and
referenced, and drops survivors from the list as it sees them; what stays
gray is the dead set, with no whitening walk. A pass too large to record
still walks scan, but restores from its referenced candidates first. On
test_cycle's growing chain this cuts the objects traced from 1.53M to
1.06M, and the work-bound check now passes at thresholds 2 and 7 with a
growth cap of 1, where it failed before.
Every walk also visits a dense cycle-capable array's elements itself, in
a per-phase loop kept out of line so the per-edge visitors stay small,
instead of calling scr_arr_trace_v's indirect visit per element. Arrays
with sparse entries, properties or metadata keep the trace callback. The
array descriptor's kind is what identifies them, so it is reserved.
The new corpus program mixes dead and live structures in one pass,
inner nodes held from outside, arrays with holes and sparse slots, and a
live tree too large for a pass to record.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- node:punycode: encode, decode, toASCII, toUnicode, ucs2.decode,
ucs2.encode and version, ported from the punycode.js 2.1.0 Node bundles
(scr_url.c), with its RangeErrors. DEP0040 follows Node: a static ES
import reports before any module evaluates and before the program can
register a 'warning' listener; require() reports on the next tick, once
for both spellings, not when a dependency under node_modules loads the
module first, and not under process.noDeprecation.
- node:constants: the frozen object Node builds from os.constants'
dlopen, errno, priority and signals groups, fs.constants (node_constants.cc
order and guards) and crypto.constants (scr_os.c); its bindings read it as
a checked-dynamic value.
These unblock tr46 (whatwg-url, node-fetch@2) and graceful-fs (fs-extra).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The order-parity baseline for the new corpus program records its module
order and empty preflight diagnostics. Uncached sanitized builds rebuild
the runtime for every compile, which takes longer than either measured
size, so the compile-time growth test runs in the plain lane only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The native range analysis now admits one fewer pass for a strict bound,
so the failing condition of `for (x = 0; x < 100; x += 24)` reads at
most 123 (the last pass enters at 99 at most).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the emitter.ts import conflict by keeping both RESULT_DST_ID
(frame-stored result slots) and specializeCallbackCalls (#1206).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the scr_resume_fiber conflict with stackless async coroutines
(#1204): the region the fiber carries is made current before either
resume path and restored after both. scr_async_spawn_stackless now
captures the spawning region like the fiber spawns, so stackless bodies
(every async body in 5013 and 5023 after the merge) keep allocating in
their region after an await and keep it alive while they exist.
5024-lifetime-region-await-forms pins regions carried by stackless and
fiber (for await) bodies that spawn and await each other.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With untyped Buffer.concat lists and typeof on builtin module exports supported, the fixture runs to completion and matches Node, so its expected deferred SC2020 refusal is removed.
The helper keeps an internal function that exactly one other function
references in its referrer's partition, so the partition can inline it.
A program's module initializers are each called only from the entry
point, so all of them, with every single-use helper they call, shared
the entry point's partition. In the three.js nodes fixture that
partition held 1.1 million lines of LLVM IR, almost five times the
average, and set the native build's longest compile.
A function whose body is larger than 256 KiB of LLVM text is past
LLVM's inline cost limit even for the only call to a local function, so
it is now placed by the hash of its own name. Its own single-use
callees still stay with it.
pop and a discarded pop (the length - 1 store) branch on the array's
length and hole-free prefix bound. When the bound equals the length, the
last element is a present dense value: the emitted code clears its slot
and state, lowers the bound and the length, and moves the slot out (or
releases it for a discarded pop). An empty array answers undefined
without a call. Holes, present undefined and sparse storage keep the
runtime call.
superCallStmt asserted the class base before checking for a callable
base. A class that extends an ordinary function constructor has no base
class info, so the assertion held null. Node ignores the unused value;
the native compiler throws the TypeError of a non-null assertion that
held null, and failed to compile programs with such classes (for example
the Effect stream fixtures). The base is now read after that case
returns.
Reads of defaultMaxListeners through an ES module namespace import or a
named import answer the namespace's copy of the export, which keeps its
value from program start until module.syncBuiltinESMExports() refreshes it,
as in Node. The default import, the EventEmitter class and CommonJS module
bindings keep reading the current default. A destructured CommonJS binding
of defaultMaxListeners, which held a placeholder string in JavaScript
files, is refused.