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.
Node for macOS on arm64 is compiled by clang, which contracts a multiply
and an add within one expression into a fused multiply-add. Its sin, cos,
tan, asin, acos, atan, sinh, asinh, acosh, atanh, exp, expm1, log, log1p
and log10 differ from the unfused results in the last bit for some inputs,
so corpus 7717 and 7718 failed on macOS. scr_ieee754.c now sets
FP_CONTRACT ON for Apple arm64 targets. Other targets keep it off and
compile to the same code as before.
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.
A build with an empty cache type-checked scriptc's own declaration files,
the TypeScript libraries and scriptc's declarations, before reusing the
answer in later builds. That check was 18 ms of a 105 ms first build of
hello world.
- build-native-cli.mts has the built compiler build a module without
imports and a module with a type error, each in an empty cache, and
records the content keys of the declaration file sets the checker
reported no diagnostics in: the checker release, the compiler options,
and each file's name and sha256. They ship in
lib/declarations/lib-diagnostics.json, named by the toolchain manifest's
lib_diagnostics.
- A build whose cache has no answer computes the files' content key and,
when the shipped answers hold it, uses no diagnostics for those files
and stores that answer in the cache. The shipped answers apply only
while the checker is the distribution's own tsc-ts and its package
version is the release they were recorded with; a changed file has
another key.
- A compiler the build host cannot run ships without them.
declareTypedArguments appends the hidden positional slots of a
typed-arguments function expression to the parameter list that
declareParams returns. Its parameter was typed
`{ localId: string; name: string; type: IrType }[]`, while the list is an
`IrParam[]` (which also declares `owned?: true`). The native compiler
passes an array whose element type has another layout as a converted copy,
so the slots were appended to the copy and the lifted function kept no
parameters. Building tests/corpus/6380 to 6384 (variadic wrappers that
forward `arguments`) with the native compiler then stopped with SC9001
"closure ...: func type does not match target signature" in 6380, 6381,
6382 and 6384.
The parameter is now typed `IrParam[]`, the type of the list the caller
passes, and the native compiler builds all five programs with output that
matches Node.
The Linux helpers linked LLVM 22.1.8's release archive. The helper's code
generation is LLVM's code, so building that LLVM from its release sources
with ThinLTO and with a profile of the helper's own work makes code
generation 5-9% faster than the helper as released, with identical objects.
- native/llvm-codegen/llvm.profdata.gz is the profile, collected by
scripts/refresh-llvm-profile.mjs: it builds LLVM with IR instrumentation
and an instrumented helper, and runs that helper on tsc-ts, five Effect
fixtures, the runtime benchmark workloads and every 50th corpus program
(scripts/llvm-helper-inputs.mjs). It follows LLVM's sources, so it needs
a refresh when LLVM is bumped.
- scripts/build-llvm-dist.mjs builds LLVM with that profile and ThinLTO,
and packs it with a checksum. The llvm-dist workflow, run by hand, builds
it for x64 and arm64, requires a helper linked against it to give the
same objects as one linked against the release archive
(scripts/check-llvm-dist.mjs), prints both helpers' times and the
profile's mismatch count, and attaches the archive to the LLVM_DIST_TAG
release of this repository.
- The release workflow's Linux jobs, and the wasm32-wasi job, which also
uploads the x64 GNU helper, download that archive and build the helper
with SCRIPTC_LLVM_THINLTO=1: clang, lld and ThinLTO, and the profile for
the helper's own sources. Without the archive, or if that build fails,
the helper links the release archive as before.
- The four Linux helper packages share their build in
native/llvm-codegen/linux-helper.mjs. Without SCRIPTC_LLVM_THINLTO it
builds the same executable as before.
A checked cast of a dynamic value to a record or array (value as T) built a
separate native copy: writes through the result missed the source and every
other holder, and === was false, where Node's as does nothing at run time.
Every such cast now runs the declaration boundary's adoption leniently
(dyn.adoptShared): each source object or array something else still holds
becomes, in place, a live capsule of the native value built from it. A node
that is not plain data, or that the value holds twice, keeps the copy. A
source nothing else can hold converts outright, which keeps the fused
JSON.parse(text) as T: a fresh JSON.parse or response.json(), workerData read
at one site, the payload of the program's one message listener, and a cast
whose result serves one primitive member read.
Data-only object and array literals in untyped slots the program declares
(unknown or any variables, fields, parameters, returns, unknown[] elements),
thrown literals and their members are built as dynamic objects, so their
later casts adopt them too.
A boxed native value cast to another native layout keeps its cached copy
(object model decision D3); the harness test pins it.
Ported from the fix/dyn-record-identity prototype.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release workflow publishes each package and moves on once npm accepts
the upload. A package whose processing on npm stalls was visible only on
npmjs.com.
A new npm-available job runs after "Publish to npm". It checks each package
of the release with `npm view <name>@<version> version --prefer-online`
every 2 minutes (scripts/wait-npm-available.mjs) until every package
resolves, and reports each package's time from upload to available in the
log and the job summary. When a package still does not resolve after 2
hours, the job fails and names each missing package and version. It only
reads from the registry.
The publish job lists every package of the release and records the time
npm accepted each upload, keeps those times with its upload receipts, and
passes both to the new job. Create GitHub Release now waits for the new
job, so the release is created and marked complete only once every
package resolves; a later release run checks again and skips uploads npm
already accepted.
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.
The keyed read of a record of functions is now `r[k] ?? inherited(r, k)`.
A pass-through `??` over a pointer keyed read whose stored values are never
unit lends the hit like the plain read: canBorrowReceiver and
ReferenceWrites.readKeys see through it, a default that only returns
interned function values or undefined needs no owner (so `const h = r[k]`
still aliases the entry), and a bound default is owned by a statement-frame
slot whose release skips the call while it stays empty. The miss helper
stays out of line, and the impls avoid code that would make every indirect
call look like a heap writer. An emission test pins that function-table
dispatch neither boxes, retains nor looks up through the runtime.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Stage L0 of the localized-failure design, report-only: no lowering changes.
A type with no static lowering poisons the slot that holds it, and the
poison rejects the declaration around it (a class, a signature, a
declaration statement); each later use then fails on its own line. This
records, on the committed diagnostics, where each type failure sits, whether
a checked dynamic slot would keep the program's meaning, and which rejected
declarations each failing statement used. coverage --explain-dynamic turns
that into the would-be dynamic slot list with the cascade each slot clears,
and the counts of errors that would disappear through stages L2 and L4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
The single object model keeps an object's layout id in the high half of its
rc word, beside the program-lifetime and frozen bits. Class ids already live
there; this names the encoding and moves the runtime's tests that may see an
object with an id onto the count:
- scr_rc_layout_id reads the 30-bit layout id; scr_rc_class_id is now that id.
- scr_rc_immortal tests the count, and scr_rc_frozen and Object.freeze of a
record use it instead of comparing the whole word with SIZE_MAX.
- Publication writes only the count of any object with a layout id, so an
immortal object keeps its id, as a class instance keeps its class id.
- classIdRcWord refuses a class id outside the layout id range.
No object other than a class instance carries an id yet, so behavior and
emitted code are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`scriptc types patch <package> <selector> <type> [--range] [--dry-run]`
finds the installed package from the working directory, shows every
declaration the selector matches (declaration files, JSDoc, @types
package) with its type before and after, and adds the patch to the
nearest package.json: into an entry whose range covers the installed
version, or under ^<version>. Nothing is written when the selector
matches nothing. `scriptc types list` shows each patch and what it
matches. Both CLI hosts run the same implementation with their own
parser. The TypeError note for a wrong declaration now names the command.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>