With --exceptions=rare, recursive functions and functions up to three
calls above a throw also propagate exceptions by unwinding on Linux, so
calls to them need no pending-flag test. Functions with a throw of their
own keep the flag. The default, common, keeps the current rules, and
builds without the flag emit the same code as before.
The option is part of the executable cache key; the bootstrap's cached
route leaves builds with it to the full compiler, and coverage and
library builds refuse it.
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.
A block-count file annotated every function the profile left without
counts, including functions the profile has no record of. The recovered
count could then exceed the mismatched functions it annotated and hide the
stale-profile warning for mismatched functions left without counts. Only
functions the profile annotation tagged as mismatched are annotated now, so
the recovered count stays among the mismatched ones.
A profile annotates a function only while its control flow hashes as it did
when the profile was collected, so compiler and source changes leave the
changed functions without counts. A --profile-use build now reads a
block-count file beside the profile (app.blocks beside app.profdata). Right
after the annotation before inlining, the helper aligns each uncounted
function's blocks with its recorded blocks by label kind and successor
count. Paired blocks take their recorded counts and their edges the recorded
edge counts, by target or, between targets whose labels changed, by
position. LLVM's flow inference fills in the rest, avoiding the edges the
function's own llvm.expect weights make unlikely. The helper then sets the
entry count, branch weights and hot or cold attribute as the profile
annotation does.
SCRIPTC_RECORD_PROFILE_BLOCKS names the file a --profile-use build writes
for the functions its profile annotates; such a build reuses no cached
artifacts. The block file keys every cache, the build report counts the
functions it annotated, and the staleness warning counts only the functions
left without counts. A single-module build with a context-sensitive profile
no longer counts annotated, mismatched and missing functions a second time
after inlining. scripts/refresh-cli-profile.mjs records the file in the
second phase and build-native-cli.mts reads it beside the profile.
In JavaScript, a Response or Request body answered getReader, cancel and
locked, and every other ReadableStream member was undefined: tee,
pipeTo, pipeThrough, values and for await threw TypeErrors where Node's
body is a ReadableStream.
In programs that link the web stream classes, the body's handle now
stands for a ReadableStream once any other member is used
(sf_stream_web_view): a stream with a zero high-water mark whose native
source reads the body (scr_webstream_readable_over). From then on every
member goes to it, the class's method values and getters run on it
through the alias's view (scr_dyn_handle_class_receiver), and text(),
clone(), new Response(body) and fetch uploads read the body through it,
so its lock and bodyUsed follow it as in Node. A body locked when the
view is made keeps the view locked until the lock is released. Once
only the view's source holds the body, the body lets the view go.
The compiler links the classes into a program whose JavaScript reads a
ReadableStream member that the fetch profile lists as unsupported by the
native stream, or iterates a ReadableStream with for await, and that
names none of the classes: %main calls webstream.install first.
Programs without web streams build to the same binaries as before.
Adds corpus programs fetch-body-web-stream-linked.mjs,
fetch-body-web-stream-unlinked.mjs and fetch-body-web-stream-network.mjs,
with their corpus costs and preflight/order baselines.
Typed records checked from frozen untyped values stay frozen, as the
objects they copy are. A sloppy-mode function called with undefined or null
as this receives the global object that globalThis reads.
The record check reads the source's integrity with a query that answers for
every value: a proxy answers as its target, and values without a modeled
integrity level, which Object.freeze refuses, are not frozen. Object.isFrozen
keeps its refusal of those values.
A dev executable was one partition up to 1 MiB of LLVM text, so every edit
compiled the whole program again. Unoptimized partitions are compiled on
their own, with nothing imported across them, so dividing a program does
not change its code's quality, and the helper reuses each partition an edit
does not change from the partition cache.
nativeProgramPartitions now takes whether the program is optimized. Dev
executables are divided from 64 KiB of LLVM text, with at least 32 KiB per
partition: four partitions up to 128 KiB, then at least 8 as before. Release
and speed executables keep their partition and code generation counts.
In JavaScript sources, WritableStream with its default writer and
controller, TransformStream with its controller, pipeTo and pipeThrough,
and TextEncoderStream and TextDecoderStream join the native web stream
classes, following Node 24's webstreams step by step: sinks and
transformers, backpressure and the writers' ready and closed promises,
abort with the controller's signal, the pipe's shared write requests and
its abort signal, and UTF-8 encoding and decoding across chunks settle in
Node's order. The streams take own properties, which a pipe does not use
for its own reads and writes, and the global object answers the classes
by computed name in programs that name them.
A JavaScript class that extends one of these classes keeps the SC1090
refusal, and TextDecoderStream refuses encodings other than UTF-8.
The node24-web-stream lifecycle, errors, queues, transforms, pipes,
pipe-errors, pipe-abort, tee and encoding cases now match Node.
A JavaScript ReadableStream given to Response, Request or fetch as a body
was converted to the string "[object ReadableStream]". The fetch unit now
reads such a stream through a reader of its own. A locked or disturbed
stream gives Node's TypeError, and the chunks must be Uint8Array
when the body is consumed ("Received non-Uint8Array chunk").
instanceof against a web stream class held in a variable or passed as an
argument walked no prototype for handles and answered false. It now starts
at the class prototype of the handle. The fetch unit's own streams, readers
and controllers, such as Response.body, are instances of the JavaScript
classes, so `response.body instanceof ReadableStream` is true again.
These paths, and the fetch reader functions' hand-off to web stream
readers, are in SCR_HANDLE_CLASSES variants of scr_json.c and scr_fetch.c,
which only programs with JavaScript web streams link; other programs that
use fetch build to the same size as on main.
The server's command line puts --cwd and --callbacks between --api and
--listen, so `pkill -f 'tsc-ts --api --listen'` matched no server that a
build started. The docs now give `pkill -f 'tsc-ts --api .*--listen'`, and
name the flags instead of writing them as one command.
Builds on Linux and macOS now connect to a shared `tsc-ts --api --listen`
server by default, which keeps the files it parsed and bound, so a later
build in the same working directory does not parse the TypeScript libs and
scriptc's declaration files again. SCRIPTC_CHECKER_SERVER=0 turns it off.
When CI is set to a value other than 0 or false, builds use their own tsc-ts
process unless SCRIPTC_CHECKER_SERVER=1.
- The server exits after 600 seconds without a build (was 300).
- Its name also digests the compiler executable's identity, so each scriptc
installation and build connects only to servers it started.
- The server process runs in the root directory, so it does not keep the
first build's directory in use; its --cwd argument is unchanged.
- When the connection to the server fails after it answered, through a
failed transfer or an ended stream, the native CLI runs the command again
in a new process with SCRIPTC_CHECKER_SERVER=0 and exits with its status.
- SCRIPTC_CHECKER_SERVER no longer changes the frontend cache key: the
server's answers are the build's own checker's.
The CLI docs describe the background process, its socket directory, when it
exits and how to stop it.
In JavaScript sources, ReadableStream, ReadableStreamDefaultReader,
ReadableStreamDefaultController, CountQueuingStrategy and
ByteLengthQueuingStrategy (globals and node:stream/web exports) are class
values over a new runtime unit, scr_webstreams.c, which follows Node 24's
webstreams step by step: underlying sources, queuing strategies, default
readers, tee, ReadableStream.from and async iteration settle their promises
in Node's order. scr_dyn_handle_class.c gives a dyn handle tag a
constructor value with a prototype, brand checks and Node's constructor
errors. Stream state carries cycle headers, so the collector reclaims
streams whose callbacks close over their controllers.
Promise steps run as native promise reactions, a variant of scr_async.c
that only programs with web streams link; every other program links the
default object. TypeScript sources keep their static web stream lowerings.
Byte streams, BYOB readers, pipeTo and pipeThrough throw SC2020.
node24-web-stream-readers and node24-web-stream-readable-lifecycle now
match Node.
- `scriptc build --static` (and the `staticLink` compile option) links glibc
executables as static PIE on the build host's own glibc target; musl
executables are already static and accept the flag.
- An executable that keeps glibc's NSS module loader (host, user and group
name lookups) is refused with SC3002, since a static glibc executable
loads those modules from the run host.
- Other targets, cross-compiled glibc targets, --sanitize and non-executable
output are refused; static and dynamic builds use separate cache entries.
The constructor copies the array it is passed into the instance, and
other reads of the field give a new array over the elements, while under
Node.js the field holds that array itself. The contract check now proves
the two agree, and reports SC7009 where it cannot:
- The arrays fields hold are never written. A whole-program graph of the
places arrays are held (locals, parameters, results, class and record
fields, and the elements of arrays, Maps and Sets by where those are
held) follows every array a field read may give, and reports a write to
one, or one that reaches a dynamic value, a call through a function
value or a library call other than printing and JSON.stringify without
a replacer. Functions that return their argument give each caller its
own argument back. The report names the field read.
- The array passed to the constructor is one nothing else keeps: a new
array (a literal, a copy, or the result of a function that returns only
such arrays or its argument), a local array at its last use (after calls
that only read it or write it in place, or in a loop that then assigns
the local a new array), a parameter whose every caller passes such an
array, an array a field already holds, or a const array that is never
written, which is then followed like a field's.
The review fixes from before stay: `new` evaluates its arguments into
locals unless they are all locals and constants, so that the allocation
reads the length after every argument has run; the constructor uses the
array parameter only to store it; and the dynamic view of an instance
reads the field as an array of its elements.
Optimized executables with at most 1 MiB of LLVM text were compiled as one
module on one thread. With more than 128 KiB of LLVM text they now pass
several outputs and `--split codegen` to the LLVM helper. The helper
optimizes the module whole, as for a single output, then divides its
functions in module order into runs of about the same instruction count and
generates code for each run on its own thread.
- The output count is 4 or 8 and depends only on the module's size
(nativeCodegenParts), so artifacts never vary with the host. Node and
native compiler builds pass the same outputs.
- The helper writes the optimized module to bitcode once with its use-list
order. Every output after the first parses it and turns the other
outputs' definitions into declarations; the first output does the same to
the optimized module in place. Each function's machine code matches a
single-output build except for jumps to a definition that another output
also uses.
- Only local definitions that another output uses become hidden. Dead
constant users are dropped before unused declarations are removed, so no
output names a thread-local variable it does not use.
- When one run would hold more than three quarters of the instructions, the
first output is generated from the optimized module as for a single
output and the others are empty objects.
- Dev builds, partitioned builds and program-object validation are
unchanged.
A large program's hot code spans hundreds of 4 KiB pages, more than the
instruction TLB holds. tsc-ts has 12 MB of code, and 90% of its
main-thread samples at --checkers 1 fall in 274 distinct code pages.
`scriptc build --large-pages` links the new runtime unit
scr_text_pages.c, whose constructor moves the executable's code onto
2 MiB transparent huge pages before main. It copies the executable
segment, from its first to its last 2 MiB boundary, into anonymous memory
advised MADV_HUGEPAGE, makes the copy read-only and executable, and moves
it over the original range with one mremap, so addresses and instructions
stay the same. The move runs only on Linux, only when THP is "always" or
"madvise", and only in executables. If a step fails, the code stays
file-mapped. Like Node's --use-largepages, it is off by default: without
the flag the unit is not linked, and executables are unchanged.
The move costs about 3 ms per 10 MiB of code at startup, and all of the
moved code stays resident as private memory. The flag is part of the
executable cache key and the runtime-pack selection, and is refused for
non-executable output, like --detached-exit.
An async generator whose body suspends only at its own awaits and yields
now compiles to an LLVM switched coroutine, as async functions of that
kind already do, instead of running on a fiber with its own stack. Its
steps run on the stack that resumes it (the loop's after an await), its
awaits suspend through scr_coro_await_prepare and scr_coro_hop_prepare,
and its yields store OUT through scr_coro_yield_* before suspending.
- scr_async_gen_new_stackless creates the generator. The stackless hop
settles an async generator's state as the fiber hop does, and a step
that would start below the stack guard throws a RangeError, which
rejects its request.
- Calling an async generator's next, return or throw no longer counts as
parking the caller: they queue a request and run the body to its next
suspension, which returns to the caller. Bodies that consume async
generators can now be stackless too.
- An untyped await (async.awaitDyn) has a native coroutine form,
extracted by scr_coro_await_dyn_value, so async bodies that await
untyped values can be stackless.
- An async generator's return through a finally region takes its await
turn through the coroutine when the body is one.
Debug builds, libraries, worker programs and Windows executables keep
fibers, as for stackless async functions.
`scriptc build --profile-use` prints one line on stderr with the number of
functions the profile annotated, the number whose control flow changed since
it was collected (mismatched), and the number it has no record of. When more
than 10% of the functions the profile describes are mismatched, it also prints
a warning to collect the profile again. The build still succeeds.
Both the compiler library's build and the native compiler's build report the
match. Cached builds report the match of the build that produced them: the
native codegen and object caches store it beside the cached objects, and the
executable cache stores it in its entry.
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>