Commit Graph
3704 Commits
Author SHA1 Message Date
Malte Ubl 980036756c Skip typed-array loop versioning in bodies compiled without optimization
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.
2026-10-18 07:48:14 -07:00
Malte Ubl b7c109dbe1 Cover Float16Array in typed-array loop versioning
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.
2026-10-18 07:48:14 -07:00
Chris Tate 2fa4b19b45 Keep object-literal computed keys out of the method replacement census (#1294) 2026-10-18 09:46:31 -05:00
Chris Tate a52c713868 Merge main 2026-10-18 09:45:58 -05:00
Chris Tate ff90245b27 Read fields as undefined before their assignment (#1292) 2026-10-18 09:38:26 -05:00
Chris Tate 8dc0071e92 Merge main 2026-10-18 09:37:20 -05:00
Malte Ubl b0d2005b43 Keep typed-array loop versioning within the self-hosted language subset
The native bootstrap compiles the emitter with scriptc itself, which refuses
comparisons of union-typed IR nodes and hand-written filter predicates.
2026-10-18 07:32:56 -07:00
Malte Ubl 0cf44e9265 Version typed-array loops that contain nested loops
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.
2026-10-18 07:32:56 -07:00
Malte Ubl de782764c2 Version typed-array loops on one bounds guard and tag typed-array memory for alias analysis
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.
2026-10-18 07:32:55 -07:00
Malte UblandClaude Opus 5.5 23e445f3a0 Say that a lying type predicate can fail a narrowing guard
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>
2026-10-18 07:31:43 -07:00
Malte UblandClaude Opus 5.5 e12db2974f Verify checker-proven narrowings at run time
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>
2026-10-18 07:31:42 -07:00
Chris Tate 0bdcccef80 Lower child_process.exec's callback forms (#1290) 2026-10-18 09:26:38 -05:00
Chris Tate 6c4e4c39b4 Keep object-literal computed keys out of the method replacement census
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>
2026-10-18 09:23:31 -05:00
Chris Tate 90c697f770 Merge main 2026-10-18 09:19:32 -05:00
Chris Tate 33c9d91796 Widen nested record fields with the records that hold them
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.
2026-10-18 09:17:29 -05:00
Chris Tate 0d4e6a0cc1 Compile classic CommonJS packages: late requires, JSON requires, nested builtin destructuring, settled require cycles (#1287) 2026-10-18 09:16:05 -05:00
Chris Tate 27f78a981d Read fields as undefined before their assignment
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.
2026-10-18 09:14:01 -05:00
Chris Tate 1a007e9a2b Lower util.promisify over the crypto callback functions and dns lookups
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.
2026-10-18 09:09:03 -05:00
Malte UblandClaude Opus 5.5 542f6573bc Name the declared type in refused class field diagnostics
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>
2026-10-18 07:07:45 -07:00
Malte UblandClaude Opus 5.5 07b44cd4b1 Index strings whose binding still stores a union
`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>
2026-10-18 07:07:44 -07:00
Malte UblandClaude Opus 5.5 56269bdad8 Destructure regex match results with object patterns
`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>
2026-10-18 07:07:44 -07:00
Malte UblandClaude Opus 5.5 22cbd7623b Compile TypeScript redeclarations of Error.cause
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>
2026-10-18 07:07:37 -07:00
Chris Tate 9d0012ddab Lower child_process.exec's callback forms
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.
2026-10-18 09:05:09 -05:00
Malte UblandClaude Opus 5.5 04297f3064 Keep the preflight batching test on a fenced require binding
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>
2026-10-18 06:56:24 -07:00
Chris Tate 8c1d94e72e Compile large closure environments in linear time (#1284) 2026-10-18 08:51:59 -05:00
Chris Tate 1179a5e1d3 Merge main 2026-10-18 08:47:42 -05:00
Chris Tate 4ed75111a7 Carry lifetime regions across await and reuse freed region chunks (#1194) 2026-10-18 08:47:04 -05:00
Malte Ubl 8255315883 Merge origin/main into opt/cjs-interop 2026-10-18 06:46:23 -07:00
Chris Tate 1663d6d35e Merge main 2026-10-18 08:45:06 -05:00
Chris Tate f2e03bdb37 Keep records, tuples and spread updates that own references in frame storage (#1205) 2026-10-18 08:44:15 -05:00
Malte UblandClaude Opus 5.5 1c4a7f0807 Renumber the cycle components corpus program to 5980
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>
2026-10-18 06:34:55 -07:00
Chris Tate b143778641 Read events.defaultMaxListeners from the ES module namespace copy (#1280) 2026-10-18 08:34:13 -05:00
Malte UblandClaude Opus 5.5 12b7f82b6a Merge origin/main into opt/01-unions
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>
2026-10-18 06:28:41 -07:00
Malte UblandClaude Opus 5.5 d49fcf3c66 Record CI costs for the array length loop corpus programs
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>
2026-10-18 06:26:10 -07:00
Malte Ubl e7cf1b090b Merge remote-tracking branch 'origin/main' into opt/array-length-ranges 2026-10-18 06:26:00 -07:00
Malte UblandClaude Opus 5.5 ae91df908e Admit require cycles whose modules settle their exports first
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>
2026-10-18 06:24:04 -07:00
Malte UblandClaude Opus 5.5 062c77c05a Restore survivors from the recorded gray set instead of whitening
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>
2026-10-18 06:24:00 -07:00
Malte UblandClaude Opus 5.5 1c19d6613d Compile the deprecated punycode and constants builtin modules
- 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>
2026-10-18 06:23:56 -07:00
Malte UblandClaude Opus 5.5 b4e25cdba0 Leave an @types twin with no runtime package to the declaration bar in transitive selection
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 06:23:56 -07:00
Malte UblandClaude Opus 5.5 e2c7eac01d Record order baselines for the type-only import fixtures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 06:23:56 -07:00
Malte UblandClaude Opus 5.5 9118cbc258 Record corpus 5973's preflight baseline and skip the scaling test under ASan
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>
2026-10-18 06:23:52 -07:00
Malte UblandClaude Opus 5.5 0dc6e05e82 Expect the tighter strided-loop bound in the self-hosted range cases
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>
2026-10-18 06:23:50 -07:00
Malte UblandClaude Opus 5.5 ed5aa1043f Merge origin/main into opt/02-value-records
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>
2026-10-18 06:23:47 -07:00
Malte UblandClaude Opus 5.5 10c5e269d7 Merge origin/main into opt/06-regions
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>
2026-10-18 06:23:44 -07:00
Chris Tate 31b52e2152 Remove the last element of a packed array inline on pop (#1282) 2026-10-18 08:21:05 -05:00
Chris Tate aba3c3a97b Read the class base after the callable-base case in super calls (#1281) 2026-10-18 08:14:23 -05:00
Chris Tate 57b6a4460c Compare the zlib-shims npm case with Node
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.
2026-10-18 08:03:20 -05:00
Chris Tate 0e67cd1e3b Place functions too large to inline by their own name
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.
2026-10-18 08:02:34 -05:00
Chris Tate fc68a582d0 Remove the last element of a packed array inline on pop
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.
2026-10-18 07:59:03 -05:00
Chris Tate 201857eb6c Read the class base after the callable-base case in super calls
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.
2026-10-18 07:54:21 -05:00