A program with no async functions, generators or timers never ran the
event loop, so a promise rejection nothing handled went unreported:
`Promise.reject(new Error('x'))` alone exited 0, where Node prints the
error and exits 1. The loop now also runs when the program makes a
promise (a Promise static, `new Promise` or Promise.withResolvers), and
its end reports the rejection, runs the 'exit' listeners with code 1 and
exits 1, as for async programs. Programs that make no promise keep their
code unchanged.
mock-timers' "named and namespace imports keep the loop's timers" and
6811-node-island-uncaught-listener check the order in which real timers
fire, with delays 1 to 5 ms apart that are scheduled one after another.
On a busy runner more time than that can pass between scheduling two of
them, and the order flips in either lane: the sanitized node:test run saw
Node's reference print 'named promise' first, and the sanitized
llvm-differential run saw 'island timer 1' before 'native timer after'.
The delays are now 100 ms apart in mock-timers (100, 200, 300) and the
island's timers (150, 250) start 100 ms after the native ones (10, 50).
Node prints the same output as before.
static-spatial bundles three.js and builds it with the sanitizer, which
passed the default 300 s test timeout on a busy runner. Its cases take
three.test.ts's 1200 s timeout.
A shorthand property's name carries the property's symbol, so the
identifier lowering's typed-array and NaN/Infinity cases, which checked
that symbol's provenance, did not match `{ Uint8Array }` or `{ NaN }`. A
JavaScript source stored the opaque identity token there, and TypeScript
stored a runtime fence; the long form `{ Uint8Array: Uint8Array }` read the
constructor. Both cases now resolve the name as the other builtin cases do
(isStdlibGlobal), which reads the shorthand's value symbol.
In JavaScript sources, `globalThis.NaN` and `globalThis.Infinity`, and
destructuring them from the global object, read the identity token too.
They now read the numbers.
The corpus fixture gained `mod-default.ts`, so the recorded module
evaluation order for `main.ts` must list it after `mod.ts` and before
`main.ts`. Re-recorded from the native frontend; the order-parity sweep
(1959 entries) passes against it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`export default <expr>` and the anonymous default function/class have no
named binding for the dynamic namespace getter to read, so a namespace
used as a dynamic value fenced its `default` property with SC1090. They
do have storage: the module registers a `default` global keyed by its
default export symbol, which is what a default import reads. The getter
keeps that symbol per export and answers the snapshot coerced to `dyn`,
so the boxed namespace's `default` matches Node; a missing registration
still fences SC1090 and a value with no checked-dynamic form still
fences SC1101.
Corpus: 8412-module-namespace-untyped gains an `export default 42`
module and reads its boxed and typed `default`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A direct call to an untyped method that returns `this` on every path,
used as a statement (`builder.add(part);`), boxed its receiver into a
capsule that nothing read. Such a call now runs the method's copy that
returns nothing, as the class dispatch helpers already do.
Response.clone() and Request.clone() on a value typed Response or Request
were refused: in TypeScript the fallback declarations had no clone, and
the fetch profile listed both as unsupported, so the call compiled to an
SC2020 refusal, also in JavaScript where the checker types the value. The
checked-dynamic handles already implement them (the body tees, and the
clone gets a copy of the headers with the same guard), so typed calls now
dispatch to those implementations, as text() and json() do.
The declarations gain clone(), and the fetch profile lists both as static
methods with the new static-clone fetch fixture as their differential
evidence. The surface manifest is regenerated, and the surface-manifest
probes that kept clone fenced now fence blob() and formData().
Adds the static-clone fixture and corpus programs fetch-clone.ts and
fetch-clone-untyped.mjs, with their corpus costs and preflight/order
baselines.
`Array.isArray([1])` folds after evaluating its argument, so the stdlib
fence battery no longer reports SC1090 for it. The snapshot drops that
diagnostic and the statement notes that it lowers.
- The jsval pass also promotes dyn module globals to jsval globals when
only dynamic-tier functions read and write them and the boxing their
writes save outweighs the boxing their reads add, the measure locals
use. `checksum += f(x)` in a top-level loop no longer allocates a
number cell per update.
- A class dispatch helper that calls a method returning `this` on every
path calls a copy of the method that returns nothing and answers its
own receiver, the capsule of the same instance at its exact class, so
the call boxes nothing.
array-from-typed-array-mapper.ts, promise-catch-optional-chain.ts,
promise-resolve-promise-or-unit.ts and promise-void-catch-undefined.ts each
load only themselves and report no preflight diagnostics.
An arena-aware record constructor zeroed the whole object after the bump,
then stored the count and shape number, and the record literal that called
it then stored every field. A record literal covers every field of its
shape, so the zeroing only mattered for the dispose entries of an arena
that is not closed, which read the fields when the arena drops.
The arena-aware constructor of a record shape without dispose entries now
clears only the collector header, when the shape has one. Its object is
never read before the literal's stores: a release of an arena object only
lowers its count, and scr_arena_obj and the heap fallback still return
zeroed objects. Shapes with dispose entries and classes, whose constructors
need not store every field, keep the whole clear.
The unit-journal test merged in from main requires every lowerer field
to be journaled or named as unjournaled, and the NI-6 fields were
neither: `sharedModulePlans` runs inside the first unit that lowers a
module, so journal the warning count like `diags`, and name the plan
memo and the per-statement TDZ flags it fills as unjournaled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Array.isArray(v)` is a compile-time constant when `v` has a static type:
an `T[]` or a tuple is an array, every other kind is not. The fold only
accepted a bare variable, a record field or a field read, because only
those could be dropped without losing effects, so `Array.isArray(make())`
and `Array.isArray([1, 2])` reached an SC1090 refusal.
A computed argument now folds the same way after being evaluated for its
effects: the value goes into a sequence whose discarded statement is the
argument. Unions keep answering by their runtime tag, and `unknown`
values keep their real runtime test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The species lowering for a callback method rebuilt its `%arr.<method>`
call by shape — the result had to be a call taking the receiver as its
first argument — so two forms the method lowering does produce fell to
SC1090 whenever the program declared a class that extends Array:
- a filter whose callback narrows: the filtered array comes back through
the checked-narrowing wrapper, `call(%arr.narrow, [call(%arr.filter,
…)])`, and the species path saw a call whose first argument is not the
receiver;
- a flatMap whose callback returns a union with an array arm, which
lowers to `%arr.flatMap.<n>` — a helper boundedArrayHofHelper rejected
because arrHofInfo, which holds the pipeline-fusable helpers, has no
entry for it.
The call is now found by walking the expression for the `%arr.` helper
call over the receiver, whatever wraps it, and boundedArrayHofHelper
accepts any `%arr.` helper (forEach excepted, it makes no array), so the
union flatMap earns a bounded variant. Hidden locals are still declared
in the old order, so a program that compiled before gets byte-identical
IR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
refuseX509DynValues walks a converted value's type looking for an
X509Certificate record, and it memoized only records (by shape id) and
unions (by union id). A type that reaches itself through an array, set,
map or promise has no id to memoize by, so `type Rec = string | Rec[]`
in a program that also holds a certificate recursed until the compiler
ran out of stack — a crash, not a diagnostic.
The walk now keeps an identity-based visited set over those kinds: a type
met again while it is still being visited answers false, the least fixed
point of reachability, since nothing outside the cycle makes a
certificate reachable. Unions keep their memo, which now also covers a
union cycle that names one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node's writeFileSync accepts an open file descriptor where it accepts a
path: it writes the data at the descriptor's current offset and leaves the
descriptor open, since closeSync owns its lifetime. The static fs surface
declared only the path forms, so `fs.writeFileSync(fd, text)` failed type
mapping — the compiler was missing a documented overload, not a
conversion.
Declare the (fd, data) and (fd, data, "utf8" | "utf-8") forms in the
node-fallback typings, route the call by the first argument's static f64
type to a new fs.writeFileSyncFd libfn, and implement it as
scr_fs_write_file_fd: a write loop over the whole string through the
descriptor-validating scr_fs_write_bytes, resuming short writes, with the
descriptor operation's errno shape on failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Promise<A | B> | undefined` awaits as Awaited of the operand, which the
maybe-promise lowering already computes: it awaits the narrowed promise
arm and coerces its settled union, and the non-promise arms take JS's one
microtask hop. The await lowering refused any union inner instead, so the
promise-or-unit shape now goes through that same path; a union whose
promise arm has no combined result keeps the SC1090 fence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An object pattern over a union whose arms type a field differently, or do
not declare it at all, was refused with SC1031, even though the pattern
element's own type is the checker's answer: the join of the arms' declared
types, with undefined for an arm that omits the key, or the narrowed arm
when a discriminant test or an `in` guard refined the source at the
pattern's site.
Keep the unionDisc fast path wherever every arm declares the field with
one type, so programs that compile today emit identical IR. Otherwise read
through the existing keyed join (unionKeyGet): each arm answers its
declared type and a record arm that does not declare the key answers
undefined, which is exactly the desugar's own property read. Only
destructuring passes the new missingFieldAsUndefined flag, so dotted reads
keep their current refusals.
Narrow first when the un-narrowed pattern does not fit: bind the
checker-narrowed view of the source to a hidden local — a trusting
arm extraction for one arm, a sub-union re-tag whose excluded arms trap,
and the stored layout view for an intersection refinement — and lower the
pattern over that view.
A rest element over a union source packs each arm's unconsumed fields into
a fresh record of the checker's arm-wise rest type, selected by the
runtime tag; CopyDataProperties makes a fresh object too, so nothing
aliases the source. Shapes that do not spell the rest type still fence.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Array.from(bytes, f)` had no lowering — only the mapper-less
`toArray` — so every mapper over a typed array fenced. It lifts a loop
that reads each element through the typed array itself, because Node
iterates live: a mapper that writes the source is visible to the
indexes it has not visited yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Promise.resolve(p)` over `Promise<void> | void` — an async callback's
declared result — was refused: the maybe-promise detection required
every non-promise arm to carry a value, while a nullish arm is exactly
what `Promise.resolve` fulfills as undefined. `promiseOrUnitUnionOf`
names that shape, and the resolution reuses the existing adopt-or-
fulfill path, so the promise arm passes through unchanged
(`Promise.resolve(p) === p`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`p?.catch(h)`, `p?.then(h)` and a guarded chain tail (`f()?.then(h)`)
were refused: the promise-method lowering rejected every
`questionDotToken` instead of asking whether the optional chain was
currently being lowered, so the guarded body never reached it. It uses
`chainBlocked` now, like every other lowering that rides the chain.
The call that ENDS a chain checker-types as the chain's undefined-armed
union (`Promise<T> | undefined`) while this lowering builds the promise
arm, which `finishOptionalChain` wraps back into the union. The
handler's result type takes that arm, so the handler is typed against
the promise it actually handles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Promise<void>.catch(() => undefined)` tripped the IR validator's
bare-unitLit rule: the handler body lowered as a bare expression, and
`undefined` has no standalone IR value. The handler's value is
discarded, so lower the body as a statement — the same rule a
discarded `undefined;` statement already follows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `file:` module namespace handed to typed code now boxes as Node's
namespace object instead of the "converting typed values to 'unknown'"
fence: `const u: unknown = ns`, `take(ns)`, a `Promise<typeof
import("./m")>` widened to `Promise<unknown>` (or `Promise<unknown> |
undefined`), and `await import("./m")` in an `unknown` slot all see one
non-extensible null-prototype object per module whose own properties are
the module's exports in Node's sorted order. They are enumerable
read-only accessors whose reads call the compiled namespace getter, so
they stay live, and the object carries Symbol.toStringTag "Module",
Node's strict-mode write TypeErrors, the not-extensible add error and
the ToPrimitive throw. `node:` builtins have no compiled export table
and keep their SC1101 fence.
The object is built by a new gated runtime unit,
packages/runtime/src/scr_module_namespace_dyn.c, selected by the
`moduleNamespaceDyn` runtime feature (`module.namespaceDyn` in the IR).
It adds no field to ScrDynHandleOps and no branch to a shared unit: the
core reaches it through the ordinary SCR_DYN_OBJ paths, so key listing,
for-in, `in`, JSON, identity, property reads and assignment all reuse
the existing dispatch. The compiler reuses the getter lifted for
module.namespaceValue (existing programs' IR is unchanged) and adds
moduleNamespacePromiseDynValue, one lifted async mapper per module for
the promise slots.
Zero cost: a program that never boxes a namespace links exactly what it
did. The zero-cost gate reports every program identical, with
byte-identical IR and LLVM; the runtime pack's only difference is the
new unit (0 changed runtime functions), and among the namespace corpus
programs only 8412 links it.
Evidence: tests/corpus/8412-module-namespace-untyped is a differential
program against Node in both the plain and sanitized lanes, and the
diagnostics snapshots and TypeScript 7 baselines stay unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A union that is a Set element or a Map key is stored as a union box, and
a union type has one representation in a program. The overlay's
`Set<ts.Node | undefined>` made the native compiler box every
`ts.Node | undefined` value, including each optional child a syntax node
returns. The set now holds the defined nodes only.
Each partition's source file name was the program entry's absolute path,
and profiles name a module's local functions after it. Since programs keep
partition-local functions internal (#2449), a profile collected in one
directory missed those functions in builds from any other: a native
compiler profile collected on main named 27% of its records, holding 2.2%
of its counts, after the checkout's path. Modules now carry the entry's
file name. The partition cache key still includes the full path.
After a fetch body's handle gained its web view, a read, cancel or pipe
started on the view left Response.bodyUsed false until the view's pull
reached the body, where Node's bodyUsed reads the stream's disturbed state
at once. bodyUsed now also answers the view's disturbed state, which the
web streams' bridge reports (ScrWebStreamBridge.disturbed).
A program that names none of the web stream classes linked them only for
member accesses the checker resolves to ReadableStream, so body.tee() or
for await over a body inside a function whose parameters have no type
threw a TypeError. The same member names on an untyped receiver, and
for await over an untyped value, now link the classes in a program that
links the fetch unit.
Adds corpus program fetch-body-web-stream-helpers.mjs, with its corpus
cost and preflight/order baseline, and bodyUsed cases to
fetch-body-web-stream-linked.mjs.