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.
When an untyped destination function returned an async iterable, the
promise form settled its result without a payload, and code that read the
awaited value as an untyped value dereferenced NULL. The promise form for
an untyped destination now resolves to undefined when the destination
returned no promise, or its promise carries no value, as Node does.
A class may implement a void interface method with a method that returns a
value, and JavaScript passes that value on (`console.log(c.publish(m))`).
The call now keeps the dispatch's untyped result, as a call through a
non-generic void member already does, instead of replacing it with
undefined: printing or interpolating it shows the value, and binding it to
a void-typed name checks it.
A pipeline stage that arrives as an untyped value now follows Node's
stage ladder for more shapes. As the first stage, a function is a source
function: the pipeline calls it with { signal } while it is set up and
pumps its result, which must be iterable (ERR_INVALID_RETURN_VALUE
otherwise), and a { readable, writable } pair pumps its readable side.
After the first stage, the pipeline call is lowered once per combination
of its first three untyped stages being functions or not, and a typeof
test picks the branch at run time. A function stage is called with the
previous stage and { signal }; before the last stage its result must be
async iterable, and as the last stage a promise resolves the pipeline's
value and an async iterable feeds the PassThrough pipeline() returns
(SCR_PIPE_DEST_DYN). Any other value that is not a stream becomes
Duplex.from of it, which the next stage reads from without the previous
stage being piped into it, and Duplex.from's ERR_INVALID_ARG_TYPE for
the values it rejects.
An untyped stage followed by an untyped function stage hands its value to
that stage through a cell created per pipeline call, so the function
receives the previous stage's own iterator, as Node's makeAsyncIterable
answers it, instead of an async generator over it.
Untyped web streams, pairs after the first stage, and function stages
that return a stream throw the not-supported error.
RUNTIME_ABI_VERSION goes from 45 to 46 for the new stream exports.
`x as T` erases (or, record to record, converts), and the value then meets
its slot with x's representation. Where no conversion reaches the slot
the program was refused:
- a union whose arms only partly convert into T (`api as Partial<Record<
string, S>>` on `S | Partial<Record<K, S>> | undefined`) failed at the
next typed slot with SC2003;
- a record or array whose member types the assertion narrows (`models as
Model<"one">[]` on a union-element array, `item as Narrow` with a
narrower field) failed with SC2002 or SC2003.
The lowerer now remembers the asserted type of an erased value, and a slot
that would refuse it converts it as asserted: the arms T excludes throw
the catchable TypeError of a lying assertion and the others convert, and
records and arrays take the checked deep conversion, which validates the
value member by member and names the deepest failing path, as a cast of an
unknown value does. The conversions run only where the slot would
otherwise refuse the value, so everything that already lowered keeps its
code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A generic method called through an interface value that classes
implement dispatches by name on the checked-dynamic receiver, and the
result converts to the call's instantiated return type. For a void
method, that conversion (`unknown` to `void`) was refused (SC1100). The
result is now discarded, as JavaScript does.
The dispatch helper selected overriding subclasses' erased instances with
a ternary, which has no void form (an internal compiler error once the
call got that far). A void method now selects them with if statements.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lambda with no type parameters of its own, contextually typed by a
generic signature (`send: (m) => …` for an interface's `send<T>(m:
Message<T>): string`), takes that signature's type parameters: its
parameter is `Message<T>`. A generic lambda written with its own type
parameters already lowers once, erased, with them bound to `unknown`, but
this one tried to map `Message<T>` with `T` unbound and was refused
(SC2009).
It now lowers the same way, erased, binding the contextual signature's
type parameters to `unknown`. Parameters bound in the enclosing scope
keep their bindings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A parameter a TypeScript program types `any` maps to the checked-dynamic
slot for function declarations and spelled function types, but not for
method or call signatures of interfaces and type literals. So
`interface Tool { execute(id: string, params: any): Promise<string> }`
was refused (SC2009), along with every value of the interface, whenever
no class implementing it gave it a class view.
Method and call signatures in program source now take the same rule.
pi's ToolDefinition declares `execute(toolCallId: string, params:
Static<TParams>, …)`. Under --node-islands, typebox's types resolve to
`any`, so the parameter is `any`. The refusal rejected the AgentSession
class through its `_customTools: ToolDefinition[]` field, and
CreateAgentSessionRuntimeResult with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A union with a checked-dynamic arm (`unknown`, fetch's AbortSignal)
maps to the checked-dynamic value when every other arm is one the
checked-dynamic tree holds faithfully. Function arms were excluded, so
`Progress | AbortSignal | undefined` was refused (SC2009).
The tree boxes a function whose parameters check in and whose result
converts out by reference (canBoxFuncIntoDyn), and `typeof x ===
"function"` narrows it back out to the typed function. Such arms now
join the collapse. The corpus program checks calls, identity (`===`
against the original), and the other arm's members against Node.
pi's SessionManager.listAll takes `onProgressOrSignal?: SessionListProgress
| AbortSignal`. The refusal rejected the whole SessionManager class.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spreading a union of records and replacing a shared field keeps each
value's variant (lowerUnionRecordClone), but only when every override's
type lifts into the field. An empty array literal is typed `never[]`,
which maps to nothing, so `{ ...entry, content: [] }` over a union fell
back to the ordinary literal path. That path cannot build a union of
spread records, and the literal was refused (SC2001).
An empty array literal now builds an empty array of each arm's own
element type. This covers a field that is an array and a field that is a
union with exactly one array arm (`string | Part[]`).
pi's session-manager repairs messages with missing content this way
(`return [{ ...message, content: [] }]`). The refusal rejected the whole
SessionManager class.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
libunicode composes an LV syllable with U+11A7 as if it were a trailing
consonant (U+11A8..U+11C2), which drops it: "가ᆧ".normalize()
gave "가" where Node gives "가ᆧ". U+11A7 is a class-0 vowel
that composes with nothing, so for the composing forms the code points
between occurrences of it normalize on their own and it is kept as it
is. The guard sits in front of the vendored library (scr_unicode_normalize),
for String.prototype.normalize and the NFC step of IDNA mapping.
8009-normalize-hangul-jungseong-o-yae prints the code points of all four
forms for U+11A7 after LV and LVT syllables, conjoining jamo, other
starters and combining marks.
Values typed `F | undefined` for a function type F are now plain closure
pointers (nullable-pointer unions), so a keyed read of a
`Record<string, F>` or a Map.get no longer allocates a union box. Keyed
record reads, string-keyed Map.get and checked extractions lend the stored
closure as a receiver or callee when ReferenceWrites proves the call cannot
write the locations it was read from, and optional-chain receivers lend
under the same proof. Owned record overflow reads use the inline entry
lookup.
Indirect closure calls skip the ambient receiver bracket for closures
whose bodies never observe it (new ScrClosure.function_kind bit
SCR_FN_RECEIVER_FREE; signature adapters that only forward the receiver
copy it from their original), and a callValue that forwards `dyn.this`
needs no bracket at all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The range analysis caps a counter that steps by 1 at 2^53, where
`j + 1 === j`, so a loop such as `for (let j = 2 ** 53 - 2;
j < 2 ** 53 + 10; j++)` has a range of three values at its condition and
was taken for a loop of three runs. It never ends, and without its poll
worker.terminate() could not stop it. A loop's runs now count as bounded
only when a step from every value in its counter's range gives a
different value.
A number-key Map or Set read whose key has an integer view
(sc_map_entry_f64_int) fell back to the same hashed probe as any double
key, which tests for NaN and -0 before hashing. The fallback is now
sc_map_entry_f64_hashed_int, which takes the integer and converts it to
the key word directly: an integer is never NaN, and zero converts to +0.
Both hashed probes test the absent-key filter only before scanning a map
that keeps no bucket table. The filter keeps two of its 64 bits per key,
so it rejects few absent keys once a map has a bucket table, and the
probe's first bucket read answers those.
The map layout and the runtime lookup are unchanged. Corpus 8331 covers
integers the compiler tracks as integers on hashed maps and absent keys on
small bucket tables, and matches Node's output.
A promise converted into a union's promise arm is a new promise, so two
optional slots set to one promise each hold their own conversion, and a
value narrowed out of such a slot may be one too. Union equality compared
those arm payloads by pointer and answered false where Node answers true.
Any comparison between a union with a promise arm and a promise or another
such union now compares both sides as untyped values, where a converted
promise answers with its origin's identity.
JavaScript sources hold node:util's MIMEType and MIMEParams as identity
tokens, so a read such as MIMEType.prototype or MIMEType.name answered
undefined where Node returns the class's member. Those reads now report
SC2020, as the TypeScript spelling does at compile time; in JavaScript
the refusal throws when the read runs.
The node24-mime-errors fixture reads MIMEType.prototype to take the
getters' descriptors, so its expected refusal is now SC2020.
In a worker program, a loop inside another loop of the same function
needs less than its own budget, because the enclosing loop polls at
least once every 64 of its iterations:
- A loop whose counters' proven ranges bound its runs, and those of the
loops in its body, to 64 x 64 emits no poll. This covers loops with
constant bounds, such as mandelbrot's lane and iteration loops. The
bound uses the counted and two-counter loop ranges, or the range of a
counter that the update alone steps by a constant where the condition
reads it. Loops whose counted form needs a run-time guard don't count.
- An innermost counted loop is strip-mined like a counted loop over
typed arrays, whatever its body. A run of at most 64 iterations never
polls, and the budget's countdown leaves the body. A run of at most
16384 iterations is one block and polls once as it leaves. A longer
run polls between blocks of 64, but not after its last block.
Between two polls there are then at most 64 iterations of the enclosing
loop, and in each of them at most 16384 unpolled iterations of each
strip-mined inner loop and 64 x 64 runs of each bounded nest. A loop
that no other loop of its function encloses polls as before.
New corpus program: native-worker-terminate-bounded-loops.ts.
The paired nest reads its bounds masked to [0, 2^25 - 1], which turns a
-0 start into +0, and a term can tell the two apart (1 / i on the first
row). The bounds test now requires 1 / b > 0, which -0 fails, so such a
nest runs as written. Corpus 8355 adds row and column starts of -0 with
terms that see the sign.