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.
A value that contradicts a type an npm package declares used to throw
"expected number at $, got undefined" and nothing else. Such a check now
names the declaration and the type patch that admits the value:
profilekit@3.1.0 declares Profile.email as string (index.d.ts:7)
if that declaration is wrong, patch it in package.json: ...
Origins are found for values a package function returns under a written
return type, for properties of records typed by a package-declared type
(record projections into such a type are keyed by it), and for
declaration-boundary casts, whose origin the projection records from the
declaration-world checker. Generic declarations get none: their types
come from the program's own instantiation.
Checked conversions carry dynCheck.origin, an index into
IrModule.declOrigins; the site's existing cold exception block calls
scr_type_origin_note (new runtime unit behind the typeOrigins feature),
which appends the note to the pending TypeError and derives the selector
suffix and the admitted kind from the failure. Union extractions get the
note per arm at compile time. The hot path is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A TCP socket's write() answered nothing in typed code and true through
untyped values, and a 'drain' listener never fired. Node 24 answers
false when the bytes the write leaves unsent reach the socket's
65536-byte high-water mark (writeOrBuffer computes it after the socket
writes), sets needDrain, and emits 'drain' when the write buffer
empties; writes to an ended or destroyed socket answer false.
The runtime's write entry points now return that answer, record
needDrain, and fire the socket's 'drain' listeners from the flush that
empties the buffer, unless the write half is ending or the socket is
gone. writableLength reads the unsent bytes and writableNeedDrain the
flag. on('drain') and once('drain') register on the typed path and
through untyped values, and socket.write() may be used as a value.
The fallback node:net declarations type write() as boolean, add the
'drain' event and both properties.
RUNTIME_ABI_VERSION goes from 43 to 44: the socket write entry points
return a boolean.
Emitted functions report an exception by returning with the thread's
pending flag set, and every caller tests the flag after each call that
may throw. On Linux executables (glibc and musl), functions reached only
by direct calls now propagate exceptions by unwinding the native stack
instead:
- unwindAbiFunctions selects the may-throw functions that are not async
or generators, constructors, virtually called or overridden methods,
closure targets, the entry, library exports, module initializers or
callees of coroutine bodies.
- A native unwind costs about 2 µs a raise and 0.1 µs a frame, so functions that
exceptions are likely to cross keep the flag: those with a `throw` of
their own, those up to three calls above one, and recursive functions,
counting calls through methods, closures and callbacks.
- Their propagating exits call scr_unwind_raise (scr_unwind.c), a forced
unwind through _Unwind_ForcedUnwind with the exception still in the
active cell.
- Direct calls to them are `invoke`s without a pending check. The
landing pad runs the frame's usual unwind path: it releases the frame's
values and then enters the enclosing try handler or resumes the unwind.
- Remaining plain calls to them (wrappers, adapters, runtime-facing entry
points) become invokes whose landing pad continues on the pending flag,
or resumes when the wrapper is itself invoked. Every other call is
marked `nounwind`, so inlining a raising function at an invoke keeps its
calls plain.
- When emitted text breaks these rules (a raising function's address is
taken, or an invoked function returns with the flag pending), the
module is emitted again with those functions on the pending flag.
Emitted code does not declare scr_unwind_raise `noreturn`, so sanitized
builds do not run the sanitizer's no-return hook, which needs stack
bounds that fiber stacks do not report. Instead, scr_unwind_raise clears
the shadow of the frames it unwinds.
Executables link a static unwinder: libgcc_eh with the host toolchain and
the glibc runtime packs, and Zig's libunwind with Zig and the musl packs.
macOS, Windows, Android, WASI, libraries and debug builds keep the
pending flag everywhere and emit the same code as before.
The throw-chains runtime workload measures exceptions caught 2, 5 and 20
recursive calls above the throw, through direct calls and a callback,
and a parser that backtracks by throwing.
A pipeline whose first stage is a checked-dynamic value now checks it at
run time, as Node's stage ladder does: a stream is piped as before, and a
string or a value with a Symbol.asyncIterator or Symbol.iterator method is
pumped into the next stage through an async generator over its iterator.
A typed Set, Map, generator or async generator object as the first stage
takes the same path. This covers the callback and stream/promises forms,
with or without function stages and pipeline options.
A write whose callback fails synchronously no longer sets needDrain, so no
'drain' follows the error and a pumped source stops at the failed write,
as in Node.
RUNTIME_ABI_VERSION goes from 37 to 42 for scr_stream_dyn_iterable_source.
In JavaScript dependencies, a createRequire require call with a computed
specifier, and require.resolve called with a computed request or read as
a value, now compile. The new runtime unit scr_require.c resolves the
specifier with Node 24's CommonJS algorithm (builtins, relative and
absolute paths with Node's extension search, package "main", "exports"
and self-reference, node_modules and NODE_PATH lookup) when the call runs:
- require.resolve returns the id or real path Node returns, and throws
Node's MODULE_NOT_FOUND error when nothing resolves.
- require throws the same MODULE_NOT_FOUND error when nothing resolves,
so packages that probe for optional addons, such as pi-tui, take their
absent branch as in Node. When the specifier resolves to a native addon,
a builtin or another file, Node would load that module: the program
prints an SC2020 error naming it and exits with status 1, without
running the catch clause around the call.
Coverage lists these calls as deferred to runtime. TypeScript sources keep
the compile-time refusal.
Refs #1777.
Object.freeze, Object.seal and Object.preventExtensions of a class
instance held in an untyped value threw SC2020 at run time. Node changes
the instance itself: writes through its typed references then throw the
read-only TypeError in strict mode code, adding properties through any
reference fails, and Object.isFrozen, isSealed and isExtensible report
the state through either reference.
- In a program that freezes, seals or prevents extensions of an untyped
value that is not a fresh literal, every class hierarchy that can be held
there gets the integrity word that static Object.freeze already uses, and
with it the existing write checks.
- The runtime applies the operation to the instance's checked view and
commits the view. The commit of such a hierarchy stores the view's
integrity state in the word (scr_dyn_class_view_integrity) and makes the
instance's property bag not extensible with it. A hierarchy without the
word still throws SC2020.
- These commits replace and remove the bag's field entries whatever their
attributes, so a sealed or frozen view does not leave stale field values
in the bag.
- The analysis behind records and arrays now starts from these calls only,
rather than from every untyped freeze the lowering emits, such as the
frozen strings array of a tagged template.
- Programs without such calls compile as before. The runtime ABI moves to
v38.
Corpus 7407 covers freeze, seal and preventExtensions through unknown of
classes and subclasses, writes through typed and untyped references and
through methods, properties added before and after the operation, and the
integrity queries, and matches Node.
Refs #1374
Co-authored-by: blurname <50226652+blurname@users.noreply.github.com>
A codec stored in untyped storage (hono's StreamingApi keeps
`this.encoder = new TextEncoder()` in a JavaScript class field) had no
dynamic representation and threw SC1101 at run time. The codec records
now cross 'unknown' slots as handles of two new tags that hold the record
itself, so identity and a decoder's streaming state survive the round
trip, and checked casts unbox the same record.
The handles answer encode, encodeInto and decode (with options objects),
the encoding, fatal and ignoreBOM properties, method values with Node's
ERR_INVALID_THIS, instanceof, String() tags and JSON's {}. The unit
scr_text_codec_dyn.c links only into programs that convert a codec to or
from an untyped value (the textCodecDyn runtime feature, found with the
dgram carrier walk, now shared).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>