The native frontend cache key included every SCRIPTC_* variable, so
turning SCRIPTC_TIMING on or off, or changing SCRIPTC_CACHE_DIR,
SCRIPTC_CACHE_MAX_MB, SCRIPTC_EMIT_WORKERS or
SCRIPTC_EMIT_WORKER_FUNCTIONS, reran the frontend on an unchanged
program. The frontend's output does not depend on these variables, so
the key now leaves them out. Every other SCRIPTC_* variable still keys
the entry.
Every file tsc-ts reads goes through scriptc's file system callbacks,
including the ~80 files of tsc-ts's own default library directory: one
round trip from the checker to the compiler and back for each. Those
files belong to the installed tsc-ts (the build caches key on its
executable's identity), and no build serves them differently.
When tsc-ts takes --default-libraries-from-disk, the native compiler
passes it to the checker it starts and to the shared checker server, so
tsc-ts reads those files itself; every other path keeps the callbacks.
Released tsc-ts versions reject the flag, so checker-features.ts asks
each tsc-ts executable once (tsc-ts --api --default-libraries-from-disk
with an empty input exits with 0 only when it knows the flag) and keeps
the answer in the native cache, keyed by the executable's identity. With
a tsc-ts that does not know the flag, builds run as before.
Edit builds of small programs through the shared checker server are
about 18 ms faster, and 6-9 ms faster without it.
- fill with a string or Buffer value longer than one byte: writes the
value once, then doubles the filled prefix with memcpy, as Node does.
The value is copied with memmove, so a value that is a view of the
buffer being filled gives Node's result; the previous per-byte loop
read such a view while overwriting it. The loop also computed a
division per byte.
- swap16, swap32 and swap64: one load, byte swap and store per group,
through a local data pointer.
- toString("utf16le" | "ucs2"): four units below 0x80 copy as four bytes,
and other units outside the surrogate range are written inline (a
three-byte sequence as one four-byte store). Surrogates keep the
previous path. The result records its UTF-16 length (one unit per
input unit), or is marked ASCII, so `.length` does not walk it.
A number-keyed Map or Set with keys the direct index allows now moves to it
at its second entry rather than its fifth, so a map over a few small
integer keys (a histogram over 0..3) reads through the inline direct index
instead of the out-of-line linear scan.
The native compiler ran four large module initializers at every startup:
the builtin call signatures (LIB_FN_SIGS), the Node 24 fetch
compatibility profile, and the builtin module function and ambient
surface tables. Together their init functions are about 1.3 MB of code,
and --version and warm cache-hit builds paged them in and built the
tables without reading them.
Each table is now built by a function on its first call and kept in a
module variable:
- libFnSignatures() replaces LIB_FN_SIGS
- node24FetchCompatProfile() and staticFetchMembers() replace
NODE24_FETCH_COMPAT_PROFILE and the STATIC_* member sets
- builtinModuleFns() and ambientSurfaceFns() replace BUILTIN_MODULE_FNS
and AMBIENT_SURFACE_FNS
The @scriptc/compiler entry point keeps LIB_FN_SIGS and
NODE24_FETCH_COMPAT_PROFILE as deprecated constants built from the new
functions when the entry module loads. The native compiler does not load
that module.
The surface manifest's coverage notes read the fetch target through the
profile function, so building the manifest module no longer builds the
profile. The table contents are unchanged.
Disposing a checked-dynamic node released each entry's getter and setter
and the node's prototype and symbol tables through scr_dyn_release, though
almost all of them are NULL: in a zod validation loop, over half of all
dyn releases. Dispose now tests each child first.
A pooled object also walked all its slots again on reuse, to take the key
bytes it kept back from the pool's key budget. It now records that total
when it is pooled, and reuse subtracts it.
A spread tuple's first field carries the statements that evaluate the
tuple into a hidden local, and its later fields read that local. When
lowerArraySpreadItems stored the first field as one item's expression,
the local's frame ended with that store, so a tuple returned by a call
was released before its later fields were read (a use-after-free under
SCRIPTC_SAN=1). The field's statements now join the packing sequence at
the item's position, whose frame lasts until every item is stored.
A lone array callback in a local class's method could read a binding the
method reaches through its class's captures. Those bindings are shared by
every method of the class, so another method can assign one while the
loop runs, but the copied loop read the value once when the call started.
stableTarget now counts a method's class captures as inherited, so such
a callback keeps its helper and reads the current value on every call.
A call that spread a fixed tuple and also an array, a Set or another
iterable into a `(T | undefined)[]` rest parameter took the general rest
path, which converts every value to T, so an undefined tuple field threw
("undefined is not representable in the target union"). Such calls now
pack every item in argument order through lowerArraySpreadItems, which
takes values already lowered (the tuple's fields) beside argument nodes
and keeps undefined values as present undefined elements.
A tuple element typed only `undefined`, as in `...["b", undefined]`, has
a `null | undefined` field. Stored into an array that keeps undefined
elements it is now evaluated and stored as undefined, where it was an
internal compiler error; the checker admits it there only as undefined.
forkTargetPaths asked the checker for the symbol of every identifier
callee in every implementation file, on each root-discovery pass and twice
more in preflight, to find calls of child_process.fork. A call is a fork
only through a binding whose import or alias chain names the module in a
module specifier, or through a '#' specifier a package.json "imports"
field maps.
namesChildProcess reads every program file once per program, outside
Node's type declarations and scriptc's own declaration files, which
declare the module and alias it only as node:child_process. Implementation
files are read from their syntax trees, which discovery fetches anyway;
other files as the host serves them to the server (the new
Ts7Program.getSourceText), without fetching their syntax trees. When no
file holds "child_process" in its text, an escape or line continuation
that spells it, or an "imports" entry that names it, forkTargetPaths
answers no targets without querying the checker.
The discovered fork targets are unchanged.
A typed array whose constructor property is set to another built-in
typed-array constructor now creates that kind from map, filter, slice
and subarray when the program has not evaluated Symbol.species.
In a speculative integer loop, `v - Math.floor(v / 2^n) * 2^n` computed
the floor division as an arithmetic shift, multiplied it back by 2^n with
overflow and -0 checks, and subtracted with range checks. For a safe
integer v and n in [0, 52] the result is the floor-mod, which is v's low
n bits: the fast copy now computes it as `v & (2^n - 1)`, which lies in
[0, 2^n) and needs no check. A product with a power of two (a literal or
`Math.pow(2, n)`) becomes a shift, with one test that the other factor's
magnitude is at most (2^53 - 1) >> n; its zero result is +0.
A getter or proxy trap that runs during a trusted build outside any
visit session now runs with the build's state set aside, as inside a
session. A conversion the getter starts, such as a recursive union
that opens and ends its own session, no longer clears the mark that
keeps the rest of the outer build from trusting its earlier match.
The receiver of a borrowed Map.get is read before its key. When the key's
evaluation replaces the field or local that holds the map
(`this.m.get(this.key())` where key() assigns this.m), the read runs on
the old map, which only the statement still holds, and the borrowed
value outlives it. A borrow now also requires the read itself, receiver
and key included, to preserve reference edges; otherwise the read keeps
its own reference.
Corpus 8181 and the emission test cover a key method that replaces the
field.
Object.defineProperty on a typed-array index reads its descriptor with
ToPropertyDescriptor (scr_dyn_descriptor_fields_read): every field once,
in specification order, through its getter, and a primitive descriptor
throws a TypeError. 7753 now also checks that the stored value is the
value getter's result, read once; that a getter-produced writable: false
is refused after the fields are read in order; and that null, booleans,
BigInts and symbols are refused as descriptors.
A lone array callback whose body has an early return is copied into an
exit block, and its statements are renamed by a separate path from the
single-return case. The new test covers labeled switches, a nested
labeled switch and breaks to both labels in map and some callbacks that
return early. Both calls run as loops, and the IR validator accepts
every renamed break.
Buffer.from(text, "base64" | "base64url") decoded each four-character
group with four lookups and a sign test, then stored its three bytes one
at a time. The decoder now first takes complete groups two at a time:
each character's value comes from a table that holds it already shifted
into place within its group's 24 bits, a single bit test covers all
eight characters, and each group's three bytes go out in one four-byte
store whose last byte the next store overwrites. A group with any other
character (padding, whitespace, an invalid character) takes the
previous path, and the two-group step resumes after it, so line-wrapped
text keeps most of the gain. The destination already holds two spare
bytes, which cover the extra store byte.
selectInt32Arrays proposed int32 array candidates, analyzed the marked
module, and when a candidate dropped, marked the module again without it and
analyzed the result from scratch, until a round kept every candidate it
marked. On tsc-ts the first analysis drops 6 of 8 candidates, and a second
full analysis confirms the other 2.
The analysis now revises the module as candidates drop: a candidate whose
element stores fail (and that keeps no checked stores) has its functions
replaced by the module's functions marked without it, which call the element
readers again, and the fixpoint goes on over the revised functions. The
selection takes the module of the functions the analysis ended with when
marking the module for the candidates it kept proposes exactly those
candidates, in those functions, and the emitter takes over the analysis with
that module's node index, initializer bindings and dynamic stores. A revision
that changes anything else in a function, such as a call of a function no
other call reaches, ends the analysis, and selection falls back to rounds.
SCRIPTC_INT32_VERIFY=1 also runs the rounds and throws unless the selected
module and every fact of its analysis are the same.
The workload reads its file through readline over fs.createReadStream.
The stream and readline runtime units define dynamic-tier symbols on
their own: a program that only reads a file with createReadStream links
the same 32, as log-summary and inventory-report do.