A record that converts into several arms of a union by a width conversion
preferred the unique arm whose fields the record has or that are
undefined-only, even when that arm holds the record only by dropping fields
and another arm keeps every field but declares an optional field the record
lacks (`adapts?: true`). The record was wrapped as the dropping arm:
- In a discriminated union the value kept its discriminant but had the other
arm's tag, so narrowing on the discriminant stopped the program with
"a narrowed value does not have the type its narrowing proved".
- Without a discriminant the dropped fields were lost: `"fnName" in value`
was false and JSON.stringify left them out.
The native compiler hit the first case on
tests/corpus/2910-overloaded-callable-alias.ts: the closure that a value read
of an overloaded function alias builds is a record that is not typed by
IrExpr, and it converted into the `selfRef` arm.
The undefined-only preference no longer selects an arm that drops fields
when another candidate keeps every field, so a discriminated union selects
the arm by the record's discriminant. The lossless-arm preference still
prefers the unique arm with exactly the record's fields; when there is none,
it accepts the unique arm that keeps every field and declares optional
fields the record lacks, which complete as absent.
A for…of or for await loop over an untyped iterable now tests, once it has
its iterator and next method, whether the iterator is one of the
program's own generators with its own next method. Each step of such a
loop resumes the generator directly and boxes only the value, where the
dynamic protocol boxed the result record and its promise and read them
back. Every other iterator keeps the dynamic protocol, and early exits
still close the iterator through it. The awaits are the protocol's: an
async generator's next() promise is awaited, and a sync generator under
for await awaits the result and then the value.
Stage L5 of the localized-failure design.
- SC6101 names a dynamic slot as the cause of a likely-hot untyped function
('tool' is a dynamic slot, declared 'ToolDefinition', which does not
compile).
- Coverage counts the dynamic operations each slot's values take part in,
lists the costly slots first, and puts the count in the JSON envelope.
- A build whose only performance warnings are dynamic slots closes with a
line about declared types instead of function types.
- Docs: a Dynamic slots section in the coverage guide, the limitations
page bullet, and --fail-on=dynamic-slots in the CLI reference and usage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage L4 of the localized-failure design.
A cast to a declared type that does not compile, a literal whose contextual
type does not compile, a spread clone of such a value or a temporary of
that type used to reject the statement. The statement commit point now
degrades those expression types too (with push-down), so the cast is a
checked conversion to the type's compilable shape and the literal builds
in it. The SC6104 of an expression names the expression.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A class extending a program class whose own declaration was rejected
(a degradable field type) failed collection with an inheritance error
that carried no cause. The localized-failure report then counted the
derived class, and the uses of it, as remaining blockers instead of the
base field's cascade. The error now names the base's failure group as its
cause.
Stage L3 of the localized-failure design.
A degraded type used to become the checked dynamic as a whole slot. Its
lenient mapping now keeps the compilable shape: the type maps again with
every nested component whose mapping fails as the checked dynamic, so a
record keeps its static members with one dynamic member, an array of
records keeps its records, and a Map, Promise or function keeps its static
parts. When even that shape does not map, or a unit still fails with it,
the type falls back to the whole-slot checked dynamic (one more round).
Lenient forms are IR types of one lowerer's shape tables, so the coverage
remainder re-derives them. A pushed-down type is one SC6104, at the slot
where it degraded, marked as keeping its shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Index walks (slice, substring, substr, startsWith/endsWith with a
position, lastIndexOf results) on a non-ASCII receiver of 64 KiB or
more: once they would have paid for one pass over it, the receiver's
index entry keeps every unit's byte offset (a 32-bit base per 64 units
and one byte per unit), and indexes map with two loads. Before that, a
walk within 64 units of the hot cursor starts from it, without the index
search. The index entry stays 128 bytes on 64-bit targets.
- lastIndexOf on non-ASCII receivers tests the needle's first byte before
comparing, as the ASCII path does.
- normalize: a quick check (UAX #15) returns the receiver when every code
point is inert for the form: unchanged when normalized alone, combining
class 0, and, in the ranges checked, never the second element of a
composition. Other text normalizes segment by segment, each starting at
a starter, so runs of inert code points copy as they are and a lone
precomposed letter's result is kept per form. The per-code-point facts
come from unicode_normalize, 256 code points at a time.
A function expression's or arrow's parameter and return types commit at the
statement that holds it, and a generic instance's at the instance drain.
Both are now localized-failure commit points for declaration slots: the
failing type degrades and the statement or instance lowers again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A flush inside a capture buffer cannot retry (a probe that later declines
must see today's answer), so its copies of the declaration's diagnostics
reach the build when the statement commits. That statement commit point
now reopens such a declaration: when its diagnostics are retriable, they
leave the build, the declaration retries, and the statement lowers again.
Probe copies of a declaration that already registered are dropped.
The 'any'-typed class field refusal stays a fence at collection and is
lifted by the flush retry, so only reached classes change; the coverage
remainder registers the declarations reachable emit registered, and maps
the degraded types alike.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage L2 of the localized-failure design.
A class whose field (or parameter property) type has no static lowering,
or a function or method whose parameter or return type has none, used to
be rejected, and every use of it failed on its own line. Its collection
deferred the diagnostics until a reachable reference flushed them into the
build. That flush is now a commit point: when every deferred diagnostic is
a degradable type failure at one of the declaration's slots, the failing
types degrade and the collection runs again, up to 16 rounds. The
declaration registers with checked dynamic slots (one SC6104 each), the
reference proceeds, and emitReachable (or run) lowers the bodies it had
skipped. A derived class whose base registers this way retries with it.
The 'any'-typed class field refusal is lifted: a TypeScript class field
whose type maps to the checked dynamic stores it, as JavaScript classes
already do.
A program that compiles today flushes no deferred diagnostics into its
build, so nothing degrades and its output is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its programs pin today's refusals; localized failure turns several of them
into checked dynamic slots in the default mode. Under static-only every
snapshot stays byte-identical, which is the stage's proof that the switch
restores the refusals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a native build failed and the packages it compiled with runtime fences
then failed as islands too, the native build's diagnostics were reported.
That build types those packages from their JavaScript, where declared-only
types (interfaces, `Static<T>`-style aliases) are `any`, so typed user code
reported cascades the island build does not have (a schema library's `TSchema`).
The island build's errors are reported instead.
In a generic function instance over island types (`<T extends TSchema>`),
a `T` value bound to dyn now crosses into the island the way `any` does,
instead of being refused as a native object.
Corpus 6829 types user code with an island package's interfaces, generic
aliases and a generic function; node-island.test.ts pins the reported
diagnostics of a failed build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage L1 of the localized-failure design.
A local, loop variable or module global whose declared TypeScript type has
no static lowering used to reject its declaration statement, and every
later use of the binding failed with it. Where that failure is about to
enter the build (the statement's poison in a TypeScript file, or the
module global's registration), the failing type now joins the lowerer's
degraded types and the unit lowers again: the type maps to the checked
dynamic wherever its strict mapping fails, so the slot holds values by
reference (typed capsules, live records and arrays, function thunks) and
typed reads out of it are checked. Each such slot is an SC6104
performance warning, listed under coverage's statement counts and in the
JSON envelope; coverage --fail-on=dynamic-slots fails on them, and
--tier=static-only keeps today's refusals.
A program that compiles today reaches neither commit point, so nothing
degrades and its output is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A native record, array or class instance boxed as unknown or any is the same
object: every box of it is one value, untyped writes, optional fields,
deletes and expandos show through typed reads and other boxes, nested
records and arrays are the typed program's own, and the value keys Map, Set
and WeakMap entries as itself. Main already behaves this way since records
and arrays box as live references; this corpus program pins it for the
single object model's later stages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sum of products of integer typed-array reads that only reaches a
bitwise operator, such as `(p[j] * 77 + p[j + 1] * 150) >> 8`, now adds
in 32-bit integers like a sum of the reads themselves: a product whose
every result is a safe integer, and never -0, is an exact integer or NaN.
A NaN-coded read passes its validity bit up through those sums, so the
integer result selects 0 for a missed read without a floating compare,
and a loop over such sums vectorizes without converting to doubles.
With --cpu-dispatch, an x86-64 executable carries two copies of each
program function: the baseline code and a copy compiled for x86-64-v3.
main reads CPUID and XGETBV once. Calls between the program's functions
go to the caller's copy. A function that is also named outside the
copies (a function value, a call from main, a runtime callback or a
coroutine body) keeps a dispatcher under its own name, which tests the
level and tail-calls one copy. Functions without such a use get no
dispatcher, so their copies have the callers the original had and LLVM
inlines them the same way.
The executable still runs on any x86-64 CPU. SCRIPTC_CPU_LEVEL caps the
level at run time. The option is off by default, applies to executables
without a --target-cpu level, and is part of the executable cache key.
bench:runtime takes --baseline-cpu-dispatch and --candidate-cpu-dispatch,
and the LLVM differential suite runs each program at levels 0 and 3 with
SCRIPTC_CPU_DISPATCH=1.
A host callback marks where it runs on Node's stack, and a call into
Node.js from a fiber starts below that mark. Runtime code the callback
runs may go deeper on Node's stack before it starts or resumes a fiber,
and those frames stay live while the fiber runs: a call into Node.js
from the fiber overwrote them. scr_switch now reports where it leaves
Node's stack, and the mark moves down to it for the rest of the
callback.
6823 runs a timer callback 200 frames deep that starts an async
function calling into Node.js before its first await.
A callback that declares no element parameter, or binds it to a name
that appears nowhere else in the function (`filter(() => true)`,
`map((_, i) => i)`, `reduce((s, v) => s + 1, 0)`), no longer gets the
element read and converted for each visit. The helpers and the fused
loops pass a constant of the parameter's type in its place (NaN, "",
false, or the undefined arm of a union that has one); other parameter
types keep the read.
An array holding an element its type cannot hold, written through an
unknown[] view, threw a TypeError at that element for every callback
method. With such callbacks it now behaves as in Node:
- map, flatMap, forEach, some, every, findIndex, findLastIndex, reduce
and reduceRight visit it without reading it;
- filter appends each kept element as it is stored, so the written value
stays in the result as the view's push would store it;
- find and findLast read the element only once the predicate holds.
Callbacks that read the element keep the TypeError for a value their
parameter type cannot hold.