Commit Graph
6750 Commits
Author SHA1 Message Date
Chris Tate 90d74a3193 Finish bailed speculative iterations outside the ordinary loop copy (#1823) 2026-10-22 08:57:41 -05:00
Chris Tate 142e10a0c4 Run counters that step beside a counted loop's counter as integers
`for (let i = 0, p = 0; i < count; i++, p += 4)` kept both counters as
doubles: the counted-loop match takes a single declaration and a single
update. A counted loop now also accepts further counters declared in its
head and stepped by a constant in its update, when the condition does not
read them and the body does not write them. On body entry k such a counter
holds `start + step * k`, so its range follows from the driver's: k is at
most the driver's span over its step.

When that range can pass 2^53, the versioned loop tests
`|start| + |step| * span / |driver step| <= 2^52` at run time, and an
unproven start is tested for an exact safe integer, as the driver's is.
Nested update blocks (`i++, j += 2, k--`) are flattened. Byte windows and
dense loops still take only single-counter loops.
2026-10-22 08:55:08 -05:00
Chris Tate 8aac437ed2 Keep reduce accumulators and callback argument counts as Node runs them
Array reduce typed its accumulator by the call's checked type, widened
with undefined only when the callback could return a missing value or a
receiver without an initial value could have holes. Three runtime values
fell outside it and threw "not representable in the target union":

- an initial value read from a missing element;
- a callback result of another primitive (`missing[0] ?? "fallback"`);
- a callback wrapped in parentheses, which the analysis and the callback
  lowering did not see as the arrow it wraps.

The accumulator now also takes undefined from a possibly missing initial
value, and the primitive arms the callback's result can have. Array
callbacks are taken without their parentheses.

`arguments.length` in a fixed-arity function expression folded to its
declared parameter count. When the expression flows into a fixed-arity
function type with more parameters, as array callbacks do (value, index,
array, and the accumulator for reduce), every call passes that type's
count, and the length is that count.
2026-10-22 08:53:50 -05:00
Chris Tate 6e75e59f75 Check each speculative site of a loop in the third-copy emission test
Since dense-array loops bounded by an invariant limit are versioned, the
test's loop has a speculative site in each dense copy. The test paired
every resume point with the first site's labels; it now checks each
site's resume points against that site's third and fast copies.
2026-10-22 08:52:18 -05:00
Chris Tate f9bed65a65 Name the union destructuring fence without printing the union
The fence for a field the arms of a union type differently printed the
whole union, whose arm order follows the order record shapes are
interned in and changes with unrelated compiler changes. It now names
the union without its arms, as the other union fences do.
2026-10-22 08:45:10 -05:00
Chris Tate 01939bc072 Give arrays state bytes only once they hold a hole or undefined
An array starts without per-slot state bytes. While it has none, every
index below its length is a present value in dense storage (len <= cap,
no sparse entries) and its hole-free prefix bound `filled` equals its
length, so its slots take 8 bytes per element (1 for booleans) instead
of 9 (2). The first hole or present undefined below the length, a length
set past the elements, or a sparse entry gives it states
(scr_arr_add_states), which it keeps; SCR_ARR_STATES in `spare` records
it, and the element ops id moves up one bit.

Runtime readers take scr_arr_dense_state or scr_arr_scan_states, and
writers keep the elements contiguous from 0 or give the array states
before raising its length. A length set on an empty array with inline
slots reserves its slots and states in one allocation.

Emitted reads keep their shape: they load a state only at or past
`filled`, which an array without states never reaches. A store past
`filled` tests the flag and, without states, appends at the length; push
and pop write a state byte only when the array has states. map, fused
map and slice pipelines that keep holes, and concat's element copy
reserve the result's storage, fill it, and set its length afterwards,
so a result without holes is filled by appends and gets no states. The
typed-view cache refresh moves the flag with the storage it swaps, and
tagged-template arrays carry no state bytes.
2026-10-22 08:41:08 -05:00
Chris Tate 8db39e6c63 Fence non-Buffer chunk parameters on byte-mode stream callbacks again (#1810) 2026-10-22 08:40:52 -05:00
Chris Tate 2f3e25b431 Accept Buffer-compatible chunk annotations and keep super(undefined) in byte mode 2026-10-22 08:35:56 -05:00
Chris Tate d41e5c45e1 Version typed-array loops whose index adds an unranged invariant term (#1822) 2026-10-22 08:34:17 -05:00
Chris Tate 14694925c7 Bind a class expression's name used only in a shorthand property, and stop keeping local class prototypes alive
- A named function-local class expression whose only reference to its own
  name is a shorthand property (`static wrap() { return { Packed }; }`)
  now gets its name binding. The self-reference check read the shorthand
  name's property symbol, so the class lowered without the binding and the
  property held a placeholder function instead of the class.
- A function-local class's prototype takes its name from the class
  object's interned name instead of a registered copy. The registry kept
  every such prototype alive until the process exited, so evaluating a
  local class repeatedly kept each evaluation's prototype and the method
  state it captures: a program that evaluates a local class 200,000 times
  and prints each instance peaked at 121 MB, and now at 2 MB.
  Reusing a freed object clears its name.
2026-10-22 08:32:24 -05:00
Chris Tate 74c7c4b1ac Merge main 2026-10-22 08:31:12 -05:00
Chris Tate d406f633bd Keep the receiver index within what the native frontend lowers
The receiver index read its methods or members table by a computed key and
iterated generic method names through `?.keys() ?? []`. The native frontend
refuses dynamic keyed reads of records and cannot convert the iterator-or-array
union, so the complete frontend no longer lowered. The table is now selected
with a conditional and the generic method names are iterated under a check.
2026-10-22 08:18:18 -05:00
Chris Tate 7259f10ad4 Keep running maximum and minimum updates as branches
With a running maximum in a register, LLVM turns `if (v > max) { max =
v; at = i; }` into selects, so each element waits on the previous one's
`maxsd`. The update gets rarer as a scan goes on and its branch predicts
well, so such an `if` now carries branch weights that say it is rarely
taken, and LLVM keeps the branch.

An `if` qualifies when it has no `else`, its condition is one numeric
`<`, `<=`, `>` or `>=` between a local and another expression, and it
assigns that expression to the local. The local must be an accumulator:
inside a loop the `if` is a statement of the innermost loop's body, the
local is declared outside that loop, and the loop writes it only in this
`if`; outside a loop the `if` is a statement of the function body and the
local is a binding the function captures and writes only there, as in a
callback. A clamp of a value read in the same iteration, or a sum clamped
after each addition, is left alone: its branch goes whichever way the
data goes. So is an update nested in another branch, whose kept branch
can stop LLVM from turning the enclosing, data-dependent branch into
selects.
2026-10-22 08:16:36 -05:00
Chris Tate 712b4e84a1 Support the env option of spawnSync
`spawnSync(cmd, args, { env })` replaces the child's environment with
the given record, as spawn and execFileSync already do, alone or with
the timeout, killSignal and stdio options. The fallback typings declare
the option.
2026-10-22 08:12:12 -05:00
Chris Tate fbb4fbc5b0 Encode posted records from their fields, and queue a message in one block
postMessage of a typed record boxes it as a live record capsule, and the
message encoder materialized the capsule's checked-dynamic view to read
it: it built the view object, listed its own keys, tested each one for
enumerability and read it back, then released the view and the key list.
On an object-only run of bench/channel-messages that was about half of
the encode time.

- A live record capsule whose view was never created (no undeclared
  properties, no writes through an untyped view) now encodes from the
  record's field hooks (ScrDynTypedRecordOps), which answer exactly the
  view's keys, order and boxed values. Nested records encode the same way
  to a depth of 32, then through their views as before. Capsules with a
  view, published values, and every other value encode as before.
- The mail event that carries a message lives in the message's block, so
  a queued message is one allocation instead of two.

Corpus 7139 and 7360 post typed records on a MessageChannel and between a
worker and the main thread: optional fields, nested records, a record
reached twice, a chain deeper than the recursion limit, a parsed record
with undeclared keys, and a function-valued field.
2026-10-22 08:07:37 -05:00
Chris Tate e4a9940e5b Finish bailed speculative iterations outside the ordinary loop copy
A speculative integer loop's failed check used to continue at the failed
statement inside the ordinary copy. That gave the ordinary copy a second
entry, so LLVM saw an irreducible cycle and did not hoist its invariant
loads or keep its values in registers. A loop whose check fails on every
entry, such as an inner loop over fractional fields, then ran every
iteration in that unoptimized copy.

A failed check now continues in a third copy that is entered only from the
fast copy: it re-executes the failed statement, the rest of the body and
the update, then jumps to the ordinary copy's header. The ordinary copy has
a single entry and stays a natural loop.

Each loop site also counts its entries in a row whose fast copy bailed. A
fast copy that completes a speculated write and leaves the loop clears the
count, and after 8 bailing entries in a row the site runs the ordinary copy
only. The count is a hint, read and written with unordered atomics: both
copies give the same results.
2026-10-22 08:07:12 -05:00
Chris Tate 866c0bfea5 Version typed-array loops whose index adds an unranged invariant term
A row-major dot product, `const base = r * cols; for (let j = 0; j < cols;
j++) s += w[base + j] * v[j]`, kept one index test per `w` read: the
typed-array loop version guard covered an index `k * counter + terms + c`
only when every loop-invariant term had a proven integer range, and `base`
has none when `cols` is a parameter.

The guard now also accepts a pure loop-invariant term without a proven
range. It evaluates the term once and requires an exact integer (not -0)
between -2^32 and 2^32, then checks the first and last index as for any
affine index. In the fast copy the term has that range, so the index is
integer arithmetic and the reads take no index test. Any other value runs
the checked copy.

Adds a Float32Array matrix-vector product with four accumulators per row
to the typed-array-kernels workload.
2026-10-22 08:05:45 -05:00
Chris Tate f0c48e1b39 Bound compiled regex reads by the characters left in the subject
The 16-character searches (the first-character search, the start filter
and greedy class loops) and literal runs checked `i + n <= len` in 32-bit
arithmetic. Subjects may be up to 2^31 - 2 characters, so near that length
the sum wrapped negative, passed the check and read past the subject. Each
check now compares the characters left (`len - i`, which cannot wrap)
with the characters it reads, and advances the position only after it
passes.
2026-10-22 08:03:32 -05:00
Chris Tate 12563f720e Compile conditional spreads of several properties and 2026-10-22 07:58:53 -05:00
Malte UblandClaude Opus 5.5 45146d4256 Keep declaration order and override placeholders in record spreads
A fixed-shape spread into an index-signature record copied its fields
alphabetically; they now enter in declaration order, as the object's own
keys do. A field whose type does not fit the index slot is valid when a
later plain property overrides it ({ ...tags, status: String(tags.status) }):
a placeholder of the slot's type holds the key's position until the
override writes. New corpus program 6916.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:54:57 -07:00
Malte UblandClaude Opus 5.5 423a70a182 Compile zod 3 natively
- An untyped JavaScript receiver whose declaring file assigns it an own
  toString (the namespace downlevel's errorUtil.toString) calls that
  function instead of the string-typed toString dispatch: zod's number
  checks threw a failed string check on its undefined result.
- Number.parseInt with one JavaScript argument that only the global
  parser's forms lower compiles through those forms.
- A JavaScript then whose declared result has no mapping settles with
  the handler's value as a checked-dynamic value.

New npm cases: zod3 (real zod 3.25.67, v3 ESM build and types), and
js-own-tostring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:51:16 -07:00
Chris Tate a023c43410 Keep the handle tags in name order and update two tests for current main (#1811) 2026-10-22 07:47:32 -05:00
Chris Tate dd3ea7c00c Narrow filter results through written type predicates with checked conversions
`.filter` with a written type predicate (`(p): p is Cat => ...`, or a
guard function passed as a value such as `filter(isDog)`) was refused with
SC1090, because TypeScript does not check the predicate. An inferred
predicate that keeps several union arms (`v => v !== undefined` over
`(string | number | undefined)[]`) was refused too.

The filter loop now keeps the receiver's element type, and the result
converts to the predicate's type element by element
(filter-predicates.ts):
- a union element to one of its arms, or to a union of some of its arms,
  by its tag;
- a checked-dynamic element by dynCheck;
- a class instance to a subclass by its class;
- a tuple or record to one with the same fields, field by field.

An element the predicate's type does not describe, kept because the
predicate returned true for it, throws a catchable TypeError: "a kept
element is not a 'Cat' (a type predicate returned true for a value of
another type)". The diagnostics fixture filter-narrow.ts, which pinned
the three refusals, compiles now; corpus 7007 covers its cases.

Fixes #1680.
2026-10-22 07:47:03 -05:00
Chris Tate 6e6267934d Report JSX-lowered files to the checker server as changed (#1817) 2026-10-22 07:46:17 -05:00
Malte UblandClaude Opus 5.5 164174f811 Keep literal key order for records a narrowing throw arm types
The key-order witness pass read every libCall typed as a record as an
opaque producer of that record, which pins the declaration order. A void
runtime call typed by its use site, such as error.nodeThrow in the
ternary that narrows a possibly-undefined binding before a member read,
never produces a value. A module-level accumulator enumerated in its
interface's order instead of its literal's. New corpus program 6915.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:46:08 -07:00
Chris Tate 906f9911a1 Keep a callback's captured scalars in registers in loops that call it
A call through a binding that the constant-callback census resolves
keeps a fallback through the closure's code pointer. In a loop that
fallback is an unknown call that takes the closure, so the closure's
code pointer, its captures and a captured `let` were reloaded, and the
`let` stored back, on every iteration of `let s = 0; a.forEach((v) => {
s = (s + v) | 0; })`.

A for, while or do-while loop that calls such a binding now tests once,
before the loop, that the binding's closure has the known code pointer.
The copy where the test holds calls the function directly with no
fallback, and the other copy is the ordinary loop. A closure's code
pointer is set only when the closure is built and the census resolves
only bindings the function never rebinds, so one test covers the loop.
Only bindings declared before the loop qualify, and loops are versioned
up to a size bound.

Number and boolean box payloads get an alias category of their own, so
a store to a captured `let` no longer aliases element loads and other
memory in the loop. With the closure built in the caller, the test folds
after inlining and the captured `let` stays in a register.
2026-10-22 07:45:48 -05:00
Chris Tate 21c4359540 Merge main 2026-10-22 07:44:22 -05:00
Chris Tate bcd849a149 Version dense array loops bounded by an invariant limit (#1816) 2026-10-22 07:42:33 -05:00
Chris Tate c297971f13 Report JSX-lowered files to the checker server as changed
The program the lowering compiles serves each file that has JSX at its
own path, with the JSX rewritten to calls (host.addVirtualFile). The
host reported such a path to the checker server as created whenever it
was not virtual yet. The files exist on disk and the server had already
read them for the program that checks them as written, so tsgo kept
their original text, and lowering met the JSX syntax: SC1090 "syntax
'JsxFragment' is not supported yet" in 6822, 6824 and 6825 under
SCRIPTC_CHECKER=tsgo, while tsc-ts reread the files.

A virtual file that replaces a file on disk is now reported as changed,
and both checkers give the same LLVM IR and diagnostics for the JSX
corpus programs.
2026-10-22 07:41:13 -05:00
Chris Tate 3800a7d311 Version dense array loops bounded by an invariant limit
A counted loop whose limit is an invariant value other than the array's
length, such as `j < n` after `const n = a.length`, gets the same fast copy
as a loop to `a.length`: one test before the loop bounds the limit by
2^31 - 1 and by each indexed array's hole-free prefix, for every array the
loop provably never resizes, and the fast copy reads those slots directly.

The length proof accepts number and boolean writes to class fields, which
run no user code, and borrowed element reads in a fast copy load the slot
without the prefix test or the runtime call. The fast copy's loop body may
hold up to 160 IR nodes.
2026-10-22 07:37:58 -05:00
Malte UblandClaude Opus 5.5 2e297caf99 Log hot-shots' failed final flush through an unknown value
Programs whose Error subclasses define accessors still compile the shim's
close logging.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:33:34 -07:00
Malte UblandClaude Opus 5.5 8c9f5cfc9f Free hot-shots' unclosed udp and tcp transports at exit
The socket's error listener captured the transport, so a client that is
never closed (mock mode) kept a cycle the RC audit reports at exit; it
now captures only the listener list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:33:30 -07:00
Malte UblandClaude Opus 5.5 88f9ba91eb Format the shims and scope the chalk declaration-merge lint exception
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:33:29 -07:00
Malte UblandClaude Opus 5.5 284517accb Call chalk builders with one string through a typed method
A callable class's %call1 method takes a single string argument directly,
skipping the variadic closure's checked-dynamic call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:33:29 -07:00
Malte UblandClaude Opus 5.5 6de7864697 Record order baselines for the new chalk and hot-shots npm cases
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:33:29 -07:00
Malte UblandClaude Opus 5.5 eef238eb94 Pin chalk builder values and buffered hot-shots against Node
A callable class instance in a narrower function slot crosses the checked
function boundary. New npm cases: chalk5-builder, hot-shots-buffered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:33:29 -07:00
Malte UblandClaude Opus 5.5 cab61904fd Send hot-shots uds metrics through a native Unix datagram socket
Where unix-dgram is installed beside hot-shots, the shim's entry sends
through runtime AF_UNIX datagram calls with the addon's semantics; without
it, uds reports the addon missing as hot-shots does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:33:28 -07:00
Malte UblandClaude Opus 5.5 750c817649 Compile hot-shots 8 from a built-in typed StatsD shim
udp over node:dgram (TypeScript may use the send completion-callback
form), tcp over node:net, mock mode, child clients, tags, checks and
events; uds reports the missing unix-dgram addon as hot-shots does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:33:18 -07:00
Malte UblandClaude Opus 5.5 51b9bdca11 Compile chalk 5 from a built-in shim with a callable builder class
A class whose merged interface carries a call signature stores its call
behavior in a '%call' field; calls and function-slot coercions read it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 05:33:18 -07:00
Malte Ubl 313dcae7c2 Merge remote-tracking branch 'origin/ni/1-libnode-loader' into ni/13-libnode-packs 2026-10-22 05:31:04 -07:00
Malte Ubl fe81bd0ddc Merge remote-tracking branch 'origin/ni/1-libnode-loader' into ni/3-hosted-loop 2026-10-22 05:31:01 -07:00
Malte Ubl d5a2fac92b Merge remote-tracking branch 'origin/main' into ni/1-libnode-loader 2026-10-22 05:30:31 -07:00
Chris Tate 92fb038be5 Support WebCrypto wrapKey and unwrapKey (#1800) 2026-10-22 07:26:29 -05:00
Chris Tate d4c5893f1b Answer binary requests in the literal-values test transport (#1815) 2026-10-22 07:23:13 -05:00
Chris Tate e6115f87e8 Merge main 2026-10-22 07:22:32 -05:00
Chris Tate 930ee4e63a Answer binary requests in the literal-values test transport
Object replies are read through binary() since the checker objects are
resolved from their ids, so the test transport answers both forms.
2026-10-22 07:20:38 -05:00
Chris Tate 87162a491d Index dispatch receivers by name instead of testing each one
ClassDynamicDispatch.process tested every property receiver for every
property dispatch and for every method name a dynamic call names, in every
pass: on the flat tsc-ts bundle, 9.2 million receiver tests per build. It
now indexes each receiver list once by the method, generic method,
accessor and field names along each receiver's base chain, and tests only
the receivers listed under a name, plus the ones whose emitter, stream or
EventTarget root answers names of its own, in list order. Names that the
test decides on every receiver (compiler names, constructor, stack) and
emit-override specializations still test every receiver.
SCRIPTC_CHECK_DISPATCH_CANDIDATES=1 checks each selection against a test
of every receiver.
2026-10-22 07:20:14 -05:00
Chris Tate f87a04fa06 Build typed 'message' listener parameters from the message
A 'message' listener with a typed parameter received each message as a
checked-dynamic value: the port decoded the message into dyn objects, the
listener's thunk matched the value against each union arm, read every
field again to build the record, and the dyn objects were then released.
For bench/channel-messages that was about a third of the delivery time.

- A closure registered with on, once, addListener, prependListener or
  prependOnceListener for 'message' gets a typed message entry when its
  parameter is a number, boolean, string, a record of those without union
  fields, or a boxed union of those arms with null or undefined. The
  entry builds the parameter from the message's nodes and calls the
  closure.
- The entry handles a message only when it has the parameter's exact
  layout: the union arm the checked conversion picks, and a record's
  declared keys in declaration order with no others. Any other message
  runs through the thunk on the decoded value, as before, so undeclared
  keys, other orders and mismatches behave as they did.
- Ports, workers and parentPort deliver a message through the entries
  when no value in it is reached twice and it holds no ports or native
  handles. Listeners without an entry share one decoded value, made when
  the first of them runs.
- The message readers are runtime entry points, so the runtime ABI is now
  27.

Corpus 7130 and 7131 check typed listeners on MessageChannel ports, a
worker and parentPort against Node: exact layouts, extra keys, shared
values, nested records, untyped listeners beside typed ones, once
listeners and a transferred port.
2026-10-22 07:08:26 -05:00
Chris Tate e07f1be5c2 Keep aliases of evolving empty arrays on the array they alias
An unannotated `const xs = []` inside a function takes the element type
its reads agree on, and is checked-dynamic when they disagree. An alias
taken between two writes of different element types (`const alias = xs`
after `xs.push(1)`, before `xs.push("two")`) took the element type the
checker gives it at that point, so it held a copy and missed the later
writes: Node prints `1,two 2` for `xs.join(","), alias.length`, the
compiled program printed `1,two 1`. An unannotated alias of such a binding,
and an alias of that alias, now takes the binding's checked-dynamic type,
so every name holds the one array.

A write that stores a value the agreed element type does not hold
(`xs.push("t")` after reads typed `boolean[]`, or `xs[1] = "one"`) now
makes the binding checked-dynamic too. Those programs were refused with
SC1090 "'string' values where 'boolean' is expected".

Fixes #1641.
2026-10-22 07:07:34 -05:00
Chris Tate 01dd4f6796 Keep the handle tags in name order and update two tests for current main 2026-10-22 07:05:30 -05:00