Commit Graph
3494 Commits
Author SHA1 Message Date
Chris Tate dec6be6ebd Run global substitutions with a literal replacement through build-time replacers
A global replace or replaceAll of a regex literal called the compiled
matcher once per match: each call set up its frame and seek again, and the
runtime then copied the gap and the replacement. A substitution of a
literal inside the compiled subset with an ASCII replacement without $
patterns now gets a replacer built for that pattern and replacement. It
appends each gap and the replacement to the output and continues the
search after the match, as the runtime's loop does (one past an empty
match), without returning. Gaps of up to 16 bytes are one 16-byte copy,
and the search after a match is a 16-byte scan instead of a memchr call.

scr_regex_replace_with runs the replacer on an ASCII subject; a subject
outside ASCII keeps the runtime's loop. Plain-text patterns, which the
runtime searches as strings, get no replacer.

New corpus program: 8351-regex-literal-replacers.ts.
2026-10-25 13:35:04 -05:00
Malte UblandClaude Opus 5.5 25a630a8ca Map intersections of equally represented constituents as that type
An intersection that restates an optional member as required with a
separately written element type (`Opts & { files: Array<{ path: string }> }`
over `files?: Array<{ path: string }>`) gives the member the type
`A[] & B[]`. The checker keeps both element literals, and no intersection
rule mapped an intersection of arrays, so the record was refused (SC2009).
When every constituent maps to one IR type that is not the dynamic value,
the intersection now maps to it. The rule runs only after every other
intersection rule declined, so intersections that already mapped keep their
mapping.

pi's NormalizedBuildSystemPromptOptions is such an intersection; it sits on
the chain from main()'s options to the extension API and refused main().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 09:37:46 -07:00
Chris Tate 8a22e0f515 Answer net.Socket write() with Node's backpressure boolean, and fire 'drain' (#2198) 2026-10-25 11:04:01 -05:00
Chris Tate 44b3efcb01 Read a typed array's length without an invariant header load (#2225) 2026-10-25 10:38:13 -05:00
Chris Tate 853033c8ec Throw Node's TypeError when an asserted callee holds no function (#2224) 2026-10-25 10:06:51 -05:00
Chris Tate ffc7af36c7 Read a typed array's length without an invariant header load
`length` and `byteLength` loaded the typed array's header with
`!invariant.load` (#2016). LLVM may move such a load down to its use,
past the release that frees the view, so `const slots = this.slots;
this.slots = next; return slots.length` returned a garbage number in
release builds. These reads are now ordinary loads that keep the
header's alias tag. Header loads that only bound or address an element
access stay invariant: their values reach only that access, which runs
while the view is alive, so typed-array loops still hoist the length
and data pointer.

Corpus typed-array-length-after-release covers a replaced field, a
deleted map entry and a rebound global.
2026-10-25 09:59:43 -05:00
Chris Tate 37b4540b11 Poll worker cancellation between blocks of typed-array loop iterations (#2203) 2026-10-25 09:54:30 -05:00
Chris Tate f0add87f10 Crypto parity: RSA key generation lengths, RSA-PSS keys and encrypted private keys (#2103) 2026-10-25 09:45:21 -05:00
Malte UblandClaude Opus 5.5 c1c4daed82 Throw Node's TypeError when an asserted callee holds no function
`f!()`, `t[k]!()` and `o.cb!()` on a value that holds undefined threw
scriptc's checked-extraction message ("undefined is not representable in
the target union …"). The non-null assertion is erased in JavaScript, so
Node throws the call's own "<callee> is not a function". The checked
extraction behind an asserted callee now carries that message, with
V8's spelling of the callee: parentheses and non-null assertions dropped
and call results printed as `f(...)`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 07:34:32 -07:00
Chris Tate 16a7ae69c5 Describe socket write state as Node's state.length in the signature table
The comments on net.sockWrite and net.sockWritableLength still described
the unsent bytes; the runtime counts each write not yet completely sent in
full.
2026-10-25 09:32:47 -05:00
Chris Tate 21e4c3683f Keep the budget in typed-array length loops that assign their array
A loop to a typed array's length reads the length again on every
iteration, so a body that assigns the array's binding changes the limit
for the next iteration. The strip-mined form took the length once per
block of 64 iterations, and after the body assigned a shorter array it
ran on to the block's end, reading past the new length.

The loop to a typed array's length is now strip-mined only when the
array is a local of the function that the body never assigns
(stableReceiver on the match); others keep the budget and test the
length on every iteration.

New corpus program: native-worker-length-loop-reassign.ts.
2026-10-25 09:13:52 -05:00
Chris Tate bc3a7301bc Add object literal members through the store caches in untyped code (#2218) 2026-10-25 09:07:14 -05:00
Chris Tate adf97e900f Merge main 2026-10-25 08:50:59 -05:00
Chris Tate c3fc0c17a4 Prove typed-array loop bounds in worker programs when a view has shared storage (#2216) 2026-10-25 08:49:08 -05:00
Chris Tate 9b2b97033a Add object literal members through the store caches in untyped code
Untyped functions built each member of an object literal with
scr_dyn_key_set: a lookup of the key in the new object, a walk of its
empty prototype chain, a second lookup before the append, and a hash of
the key to share its bytes. The first cached read of the object then
computed its shape from its keys, hashing each key again.

A member whose key is a constant string other than __proto__ now owns a
store cache site and calls scr_js_store_probe, which appends the member
through the cached shape transition, takes the key bytes from the shape,
and leaves the object shaped. Members the probe does not take (Error
field names, members past the 64th, a site's first object of a shape)
keep scr_dyn_key_set, as do computed keys and literals in module
initializers.
2026-10-25 08:24:12 -05:00
Chris Tate 3e1af48ea3 Convert a loop's string receiver to UTF-16 at once only when the loop can read enough of it (#2212) 2026-10-25 08:17:39 -05:00
Chris Tate b1a6814796 Prove typed-array loop bounds in worker programs when a view has shared storage
A typed-array loop version in a worker program tested its bounds and
its views' ownership together: every view had to have no shared
storage. A loop over views of a SharedArrayBuffer always failed that
guard and ran the checked copy, testing every element's index.

The guard now tests the bounds first and the ownership after. When the
bounds hold and every view is unshared, the loop runs the owned copy as
before. When a view is shared, it runs a new third copy whose accesses
skip their bounds checks and keep the shared-or-local path, without
alias scopes. Versions with a UTF-16 units copy, and programs without
workers, are unchanged.

Corpus 8350 runs such loops over shared, unshared and mixed views, to
counts within and past the views and over subarrays, with a worker
writing through shared views. The emission test checks the split guard
and the shared copy's accesses.
2026-10-25 08:06:24 -05:00
Chris Tate 4d27b1a000 Call replace callbacks with unannotated parameters directly (#2207) 2026-10-25 08:05:35 -05:00
Chris Tate 63a99cc4d9 Convert a loop's string receiver to UTF-16 at once only when the loop can read enough of it
A versioned loop over a non-ASCII string passes the range of indexes it
reads: a counted loop's start and limit, or a while loop's counter and
bound. The runtime converts the string at once when that range has at
least one unit per 16 bytes of it, and otherwise counts the loop as one
indexed read, so a loop over a line of a long source text no longer
keeps a UTF-16 copy of all of it. The publish call is weighted as
unlikely, since loops re-entered over one string skip it.
2026-10-25 07:37:50 -05:00
Chris Tate 8a08f65ebf Leave timing and cache placement variables out of the frontend cache key (#2211) 2026-10-25 07:25:47 -05:00
Chris Tate 4ba4e68695 Emit dev builds' function bodies on the emit workers (#2206) 2026-10-25 07:14:09 -05:00
Chris Tate b95b07c537 Have tsc-ts read its library files from disk when it can (#2208) 2026-10-25 07:06:21 -05:00
Chris Tate d93a02d89e Build the large compiler tables on first use (#2202) 2026-10-25 06:55:23 -05:00
Chris Tate c80945ec21 Leave timing and cache placement variables out of the frontend cache key
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.
2026-10-25 06:50:19 -05:00
Chris Tate d8f2874860 Keep replace callback parameters checked-dynamic where parseInt or parseFloat expects another type
parseFloat and the text of parseInt take a string, and the radix of parseInt
takes a number. An unannotated offset passed as their text, or a capture passed
as the radix, is no longer bound to its value's type; it stays checked-dynamic,
so the call compiles as it did before the binding.
2026-10-25 06:42:25 -05:00
Chris Tate 24ef53ac23 Have tsc-ts read its library files from disk when it can
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.
2026-10-25 06:32:18 -05:00
Chris Tate ceb29078dc Skip comments when renaming a range's debug ids 2026-10-25 06:31:51 -05:00
Chris Tate 1a0d58d1eb Merge main and move to ABI 45 2026-10-25 06:31:07 -05:00
Chris Tate b8403b7094 Let LLVM merge the comparisons of a three-way string comparator (#1971) 2026-10-25 06:27:31 -05:00
Chris Tate 627ebd86a5 Call replace callbacks with unannotated parameters directly
TypeScript types the parameters of an inline replace callback after the
match `any`. An unannotated parameter that is never reassigned, whose
every read means the same for a string or number as for `any`, and that
receives a capture every match sets, the offset or the subject now takes
that value's type. String-search callbacks get the same treatment for the
offset and the subject.

scr_regex_replace_fn calls the closure directly in more cases:
- checked-dynamic parameters, which receive the boxed value Node passes at
  their position (a capture or undefined, the offset, the subject, the
  groups object, then undefined);
- `string | undefined` capture parameters, which receive NULL when the
  match leaves the capture unset;
- a checked-dynamic result, converted with ToString;
- up to six parameters.

Callbacks of the earlier typed form compile to the same call, with a
kinds argument of 0, and run the same per-match work.
2026-10-25 06:16:32 -05:00
Chris Tate 872c12e247 Emit dev builds' function bodies on the emit workers 2026-10-25 05:59:55 -05:00
Chris Tate edae637526 Support named properties and Symbol.species on typed arrays held as untyped values (#2068) 2026-10-25 05:48:27 -05:00
Chris Tate 0e92be2bf7 Poll worker cancellation between blocks of typed-array loop iterations
In a worker program, every loop without a pending test on each iteration
decremented a cancellation budget in its body and polled the exception
flag every 64 iterations. The poll's early return made each loop an
early-exit loop, which LLVM's loop vectorizer does not vectorize.

A counted for loop (integer counter, invariant limit), or a loop to a
typed array's length, whose body reads or writes a typed-array element
outside nested loops and closures, now runs as blocks of at most 64
iterations: the outer header tests the counter against the limit, the
inner loop runs to the nearer of the limit and 64 steps on, and the poll
follows each block. A typed array's length is fixed, so each block takes
it as its limit. The loop polls as often as the budget did, and the
inner loop's only exit besides break and return is its bound. The block
bound lives in one slot shared by the loop's speculative copies, which
resume iterations in each other's bodies. Other loops keep the budget,
which costs less in loops of a few iterations.

An elementwise Float64Array loop in a worker-enabled program vectorizes
again: 949 ms to 691 ms. A byte-classifying loop over a 100M-byte
Uint8Array takes 13 ms instead of 141 ms.

New corpus program: native-worker-terminate-counted-loops.ts.
2026-10-25 05:41:44 -05:00
Chris Tate cfa9f81cfd Share storage between T[] and (T | undefined)[] (#2080) 2026-10-25 05:40:56 -05:00
Chris Tate 5da6d76085 Run lone array callbacks as loops with their bodies inlined (#1997) 2026-10-25 05:24:07 -05:00
Chris Tate 9abf35f6a2 Build the large compiler tables on first use
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.
2026-10-25 05:23:35 -05:00
Chris Tate f8b1f9ffc3 Keep a spread tuple alive until all its fields are packed into the rest
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.
2026-10-25 05:02:40 -05:00
Chris Tate 256fbdba20 Keep the helper for callbacks that read a local class's captures
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.
2026-10-25 04:54:28 -05:00
Chris Tate c3d778b615 Skip fork discovery in programs that never name node:child_process (#2200) 2026-10-25 04:53:04 -05:00
Chris Tate 7db7b4dd86 Prove int32 array candidates in one analysis that revises dropped ones (#2196) 2026-10-25 04:39:23 -05:00
Chris Tate fa27df8b06 Speculate floor-mods and products by powers of two as masks and shifts (#2199) 2026-10-25 04:34:53 -05:00
Chris Tate 534c08dd2f Pack spread tuple fields together with other spreads into optional-element rests
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.
2026-10-25 04:34:24 -05:00
Chris Tate fa02fcd8ae Report which packages and modules would run in a Node island (#1691) 2026-10-25 04:32:22 -05:00
Chris Tate 03b754e891 Convert dynamic values into nested unions in one pass (#1914) 2026-10-25 04:18:21 -05:00
Chris Tate 0d4df6ff62 Skip fork discovery in programs that never name node:child_process
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.
2026-10-25 04:14:58 -05:00
Chris Tate 8f655df65e Hold a Map.get result as the entry's reference until the map can change (#2099) 2026-10-25 03:59:34 -05:00
Chris Tate 4050c0fb5c Merge main and skip package.json files without a scriptc key 2026-10-25 03:58:49 -05:00
Chris Tate e38f7c329d Speculate floor-mods and products by powers of two as masks and shifts
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.
2026-10-25 03:56:48 -05:00
Chris Tate c1f61c05c4 Merge main 2026-10-25 03:55:15 -05:00
Chris Tate 1e799cb60a Answer net.Socket write() with Node's backpressure boolean, and fire 'drain'
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.
2026-10-25 03:40:30 -05:00