Commit Graph
2824 Commits
Author SHA1 Message Date
Chris Tate 5dacb5bed3 Keep running maximum and minimum updates as branches (#1826) 2026-10-22 16:42:08 -05:00
Chris Tate d7717ff1e9 Type empty array literals by their slots, and report package diagnostics accurately (#1799) 2026-10-22 16:40:40 -05:00
Chris Tate 753c64febc Test for-in key presence in the overflow map instead of a fresh key list (#1859) 2026-10-22 16:35:49 -05:00
Chris Tate bb34d61b55 Read optional elements at a proven integer index without a round trip (#1845) 2026-10-22 16:32:32 -05:00
Chris Tate ee865bcb0d Support the legacy createCipheriv modes and crypto.checkPrime/generatePrime (#1832) 2026-10-22 16:32:12 -05:00
Chris Tate 48a08dce8e Store arrays of ProgramLifetime instances in 4-byte slots on Linux (#1847) 2026-10-22 16:29:42 -05:00
Chris Tate d2408a09ad Match Node for reduce accumulators and callback arguments.length (#1828) 2026-10-22 16:27:50 -05:00
Chris Tate f45d8ba6dd Version while loops over typed arrays on their counters' bounds (#1841) 2026-10-22 16:27:30 -05:00
Chris Tate 7ef356137f Read the union payload inline when comparing it with a number or boolean (#1839) 2026-10-22 16:26:44 -05:00
Chris Tate 41af221ed1 Version functions on number parameters that bound their loops (#1853) 2026-10-22 16:21:01 -05:00
Chris Tate 5aae5531e0 Give the sanitized bootstrap seed job swap
The sanitized seed compiles the compiler's 700 MB LLVM module with one clang
process under AddressSanitizer. The compile peaks near 14 GB while the Node
process that emitted the module still holds about 6 GB, which exceeds the
runner's 16 GB of memory. The job now enlarges swap to 24 GB before it builds
the seed, as the sanitized emitter job does.
2026-10-22 14:30:37 -05:00
Chris Tate d83c676705 Store compact values of string-key maps in 4-byte slots on Linux 2026-10-22 14:08:00 -05:00
Chris Tate b074a5e20f Test for-in key presence in the overflow map instead of a fresh key list
for-in over a pure index-signature record (`Record<string, number>`) skips
keys deleted during the walk by testing each snapshotted key for presence
before visiting it. The test built the overflow map's whole key list and
searched it with includes() on every visit, so a walk over n keys took
O(n^2) time: 200 walks over 2000 keys took 8.7 s, against 31 ms under
Node. The test is now recordOvfHas, the overflow map's hashed presence
check, which answers the same question in constant time.

The new benchmark workload dictionary-walks walks, prunes and merges
Record<string, number> dictionaries with for-in and Object.keys/values/
entries. It takes 7.1 s before this change and 23 ms after (Node 116 ms).
Corpus program 7011 pins for-in's visit order and its handling of keys
deleted, re-added and added during the walk.
2026-10-22 13:49:49 -05:00
Chris Tate 6681b9407e Version functions on number parameters that bound their loops
A number parameter that every call site proves an int32 reaches the
function's i32 entry. One that some caller passes as an unproven number,
such as a value read from a worker message or parsed from text, kept its
double, so every loop it bounds compared an integer counter with a double
limit and the values derived from it stayed doubles.

When such a parameter bounds a numeric loop (arithmetic, element and
field reads and writes, Math calls, no allocation or other calls), the
function now gets a copy of its body. Its own body first tests
each such parameter for an int32 that is not -0 and, when all pass, calls
the copy with `p | 0`, the same double. The copy's only caller proves
those parameters int32s, so it gets the i32 entry and integer loops.
Other values run the body as before.

Only plain synchronous functions of at most 2048 IR nodes are versioned:
not class members or generated helpers, not capturing, not the entry,
not dynamic-tier, not calling themselves, and at most 16 per module. A
function holding the only call site of another function is kept whole
unless that callee only computes a number or boolean from scalar reads:
a second call site can stop LLVM from inlining the callee and leave the
records it builds in memory. Library builds and modules that observe
call stacks are left as they are.
2026-10-22 11:46:21 -05:00
Chris Tate 972e44a11a Hold any later value in JavaScript properties initialized to undefined or null
A JavaScript object literal's property whose initializer is `undefined` or
`null` (`[{ oldPos: -1, lastComponent: undefined }]`, `{ user: null }`)
mapped to the unit-only union, so a later write of another value threw:
the diff package's diffLines failed with "a '{ added: boolean; count:
number; previousComponent: unknown; removed: boolean }' value is not
representable in the target union" when it stored its first path
component. Nothing checks a JavaScript file's writes, so such a property
is now checked-dynamic and holds whatever the program stores. TypeScript
properties keep the unit-only union, and `__proto__: null` still sets the
prototype.
2026-10-22 11:40:01 -05:00
Chris Tate 4d7de03054 Store arrays of ProgramLifetime instances in 4-byte slots on Linux 2026-10-22 10:46:16 -05:00
Chris Tate b774307a4d Merge main 2026-10-22 10:46:00 -05:00
Chris Tate f691918e27 Merge main 2026-10-22 10:29:26 -05:00
Chris Tate 5bbf2c97b8 Encode posted records from their fields, and queue a message in one block (#1824) 2026-10-22 10:25:40 -05:00
Chris Tate 08081ca1a2 Encode each field of a posted record completely before the next
A typed record encoded from its field hooks queued a plain-object field on
the visit stack and went on to the next field, so getters on later fields
ran before the properties of earlier ones were read. Node's serializer
finishes each field, nested properties included, before the next.

The record path now drains the visit stack down to where it started after
each field, so properties are read in Node's order. Corpus 7139 posts a
record whose fields hold getter-bearing objects and prints the read order.
2026-10-22 10:22:22 -05:00
Chris Tate de6ef6590f Read optional elements at a proven integer index without a round trip
An element read that may be undefined, lowered inline from `%arr.idxOr`
(`a[i]` as `T | undefined`, and the read in `a[x] = a[y]!`), took an
integer index only from a loop's integer slot. Any other index, such as a
double counter with a proven integer range or `k | 0`, was converted and
tested for an exact integer on every read. The read now takes the same
exact integer as a borrowed read does (`exactInteger`), on 64-bit targets.
The loop-induction treatment above the hole-free prefix still applies only
to a loop's integer slot.
2026-10-22 10:12:31 -05:00
Chris Tate affe386bbf Throw for every object on the right of an arrow instanceof, and give blocked bodies their this
- An arrow function or method on the right of an untyped `instanceof`
  has no prototype. Node throws "Function has non-object prototype
  'undefined' in instanceof check" for any object, but the runtime threw
  only for plain objects, arrays and functions, and answered false for
  typed arrays, native handles, promises and typed references. It now
  answers false only for primitives.
- Coverage lowers the body of a signature-blocked function for its
  diagnostics. A declared `this` parameter now gives that body its
  receiver, as lowerFunction does, so `this.label` reports nothing.
  Before, each use reported SC1080.
2026-10-22 10:09:43 -05:00
Chris Tate e4dc0900d7 Fence arguments.length in function expressions that call themselves by name
A function expression stored in a function type with more parameters
folds `arguments.length` to that type's parameter count, the count every
call through the slot passes. A named expression that calls itself
through its own name passes only its declared parameters there, so no
constant answers both calls. Such a read now reports SC1090 with the
reason, and expressions that do not refer to their own name keep the
fold.
2026-10-22 10:09:08 -05:00
Chris Tate 03495c2198 Index dispatch receivers by name instead of testing each one (#1814) 2026-10-22 10:07:29 -05:00
Chris Tate ca579641a4 Index quoted get: and set: method names under their own keys 2026-10-22 10:04:30 -05:00
Chris Tate 44bd4e1d6e Run counters that step beside a counted loop's counter as integers (#1829) 2026-10-22 10:00:28 -05:00
Chris Tate aaedce7b4a Keep a callback's captured scalars in registers in loops that call it (#1818) 2026-10-22 09:59:36 -05:00
Chris Tate 0ec1d47b4d Build typed 'message' listener parameters from the message (#1813) 2026-10-22 09:51:04 -05:00
Chris Tate 8487742019 Read the shared decoded value after an untyped listener, and skip discriminated unions
- Once a 'message' listener without a typed entry has decoded the
  message, the listeners after it read that decoded value, so a typed
  listener sees what an earlier untyped listener changed, as in Node.
- A union with a literal discriminator gets no typed message entry: its
  checked conversion also tests the discriminator, which the message
  readers do not.

Corpus 7361 checks a typed listener after an untyped one that changes
the message, and a listener on a discriminated union.
2026-10-22 09:48:41 -05:00
Chris Tate b80baf92ef Filter regex start positions by their leading character sets (#1784) 2026-10-22 09:45:21 -05:00
Chris Tate 2d6ef06fcb Version while loops over typed arrays on their counters' bounds
Typed-array loop versions applied only to for loops, so a scanner written
with while loops, such as `while (pos < end && bytes[pos] === 32) pos++`,
kept one bounds test per access. A while loop counter is now a number
local that the loop changes only by positive integer increments (`c = c +
k`, `c++`, `c = (c + k) | 0`) written as statements of its body, blocks,
branches and nested while loops. An access at `counter + d` after a
condition compared the counter below a loop-invariant bound, with no
increment since, is in bounds for the whole loop when the guard finds the
counter's entry value an exact integer of at least -d and the bound at
most the length minus the access's extent. A counter with a `| 0`
increment also needs its loops' bounds at most 2^31 minus its increments,
so it never wraps.

A fixed check (a statically ranged index) now needs an end below 2^31.
The counter of a loop to a number[]'s length whose length the loop may
change reaches 2^32 - 2, and its typed-array accesses got a guard no
typed array passes and a second copy of the loop.
2026-10-22 09:40:22 -05:00
Chris Tate 091f280cec Read the union payload inline when comparing it with a number or boolean
`u === 0`, `u !== true` and `Object.is(u, x)` against a scalar arm tested
the union's tag inline but read its payload through scr_union_get_f64 or
scr_union_get_bool. The call passed the union's stack box to the runtime,
so LLVM could not promote the box to registers, and an element read of a
`number[]` compared with a number kept its value, tag and box in memory.

The comparison now loads the payload slot directly, as other union
extractions already do, and the box no longer escapes.
2026-10-22 09:32:19 -05:00
Chris Tate 20f4d1ff12 Support the legacy createCipheriv modes and crypto.checkPrime/generatePrime 2026-10-22 09:08:35 -05:00
Chris Tate 60c51b9535 Merge main 2026-10-22 09:08:21 -05:00
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 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 74c7c4b1ac Merge main 2026-10-22 08:31:12 -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 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 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 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 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
Chris Tate e6115f87e8 Merge main 2026-10-22 07:22:32 -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