Commit Graph
2845 Commits
Author SHA1 Message Date
Chris Tate 5195e2b63a Merge main 2026-10-22 17:19:37 -05:00
Chris Tate 5f5cf8f5be Keep aliases of evolving empty arrays on the array they alias (#1812) 2026-10-22 17:12:17 -05:00
Chris Tate cdc7b384d4 Convert doubles below 2^63 to int32 with one conversion (#1871) 2026-10-22 17:01:09 -05:00
Chris Tate b2bb7be0b4 Support the env option of spawnSync (#1825) 2026-10-22 17:00:17 -05:00
Chris Tate 159740c685 Convert doubles below 2^63 to int32 with one conversion
ToInt32 and ToUint32 of a double that no range proves an integer took the
i64 conversion only up to 2^53 in magnitude, and decoded the double's bits
above that. Every double between 2^53 and 2^63 is an integer, so the i64
conversion is exact there too, and its low 32 bits are the result. The bit
decoding now handles only NaN, the infinities and magnitudes from 2^63.

Multiplicative hashes such as `((h ^ b) * 16777619) >>> 0` and
`(h * 1000003 + b) | 0`, whose products usually pass 2^53, convert on the
fast path.
2026-10-22 16:57:09 -05:00
Chris Tate 57c10fc767 Build Sets, Maps and weak collections from checked-dynamic iterables (#1856) 2026-10-22 16:53:29 -05:00
Chris Tate 631d4dcc30 Keep record types acyclic when only result-slot stores write their fields (#1865) 2026-10-22 16:50:21 -05:00
Chris Tate 74a958408f Compile chalk 5 and hot-shots 8 from built-in shims with callable class instances (#1836) 2026-10-22 16:49:19 -05:00
Chris Tate 9df9cfc82f Keep literal key order for records a narrowing throw arm types (#1837) 2026-10-22 16:48:21 -05:00
Chris Tate fa62abce4d Keep record types acyclic when only result-slot stores write their fields
The backend gives every function that returns a complete record literal a
result-slot entry, `%into.<fn>(...params, dst)`, which stores the literal's
fields into caller frame storage. The cycle analysis counted those stores
as writes after construction, so a record type that refers to itself and
is never written after construction carried the cycle collector's header,
its live count and its candidate checks in release builds, even when
nothing called the entry. Dev builds, which add no entries, left it
acyclic.

The destination is frame storage of a caller that only projects the value
(stack-objects.ts): no object can point to it, so its fields cannot lie on
a cycle. computeCycleMutability now skips those stores, under the same
condition emit-statements.ts already uses to emit them as initializations
(resultSlotStore in the new result-slots.ts). Every other store still
makes its field mutable. The same facts mark such fields as never
replaced, so reads of them borrow instead of retaining.

A program that rebuilds a tree of 2^21 - 1 object literals 12 times runs
in 1.19 s instead of 1.35 s and peaks at 68 MB instead of 84 MB: each node
is 32 bytes instead of 40.
2026-10-22 16:45:06 -05:00
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 f4365b7eea Cache each constant method name's id in a slot
A checked-dynamic method call looked its name up in the method table on
every call: a strlen, a hash and a byte comparison of the name.

The compiler now gives each method-name constant an i32 slot that starts
at -1 and passes it with the call (scr_dyn_invoke_prepared_id, and the
Buffer method entries, which forward it). The first call stores the
name's id in the slot, and later calls read it. Ids stay the runtime's
own, so the slot holds whatever this runtime's table says. Names are
still matched by content on the first call, and the slot is written with
a relaxed atomic store, since every thread writes the same id.

The runtime ABI is now 28.
2026-10-22 15:02:41 -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 8f08cbbec5 Build Sets, Maps and weak collections from checked-dynamic iterables
`new Set(x)`, `new Map(x)`, `new WeakSet(x)`, `new WeakMap(x)` and
`Object.fromEntries(x)` now accept any checked-dynamic iterable, as in
Node: a native iterator (`map.keys()`, `set.values()`), a compiled
generator, an object with a Symbol.iterator method, or a Set. Before, a
Set seed that was not an array or a string threw "is not iterable", and a
Map, weak collection or Object.fromEntries seed that was a generator or a
collection other than a Map refused with SC2020.

The runtime drains native iterators, packs arrays, strings and
collections, and steps every other iterable through its iterator
(scr_dyn_seed_values). A program that builds these from an untyped seed
registers the compiled generators' next and return methods with the
runtime, as destructuring rest already did, so a generator seed can be
stepped.

The seed's values are all taken before the first one is added, so when an
entry of a Map seed is not an object the TypeError comes after the
iterable is exhausted; Node closes the iterator at that entry. The
limitations page says so.
2026-10-22 13:09:01 -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 fd40a8c84a Try each PATH entry with exec when spawning with a replacement env
The PATH lookup for spawns with an `env` option picked the first
executable regular file and stopped there, so a script whose interpreter
is missing ended the search with ENOENT. Node's libuv runs execvp in the
child, which moves on to the next PATH entry when an exec fails with
ENOENT, ENOTDIR or EACCES. Each candidate now runs through posix_spawn in
order with the same rule, and relative entries are resolved by the child
in its cwd.
2026-10-22 11:18:30 -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 ccfca67aec Fix chalk builder field calls and hot-shots send failures and tcp completion 2026-10-22 10:30:56 -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 3a3610d12e Look up spawn and execFileSync commands on the replacement environment's PATH
spawn and execFileSync with an `env` option searched the parent's PATH
for a command without a '/'. They now resolve it the way spawnSync does,
as Node (libuv) does: on the replacement environment's PATH, or the
default path when it has none, with relative entries read from the cwd
option's directory.
2026-10-22 10:15:10 -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 0078dc2c46 Check the elements a spread push stores into an evolving empty array
The evolving-array check made the binding checked-dynamic for any spread
write, so `xs.push(...values)` of an array whose elements match the
binding's agreed element type turned a typed `number[]` into a
checked-dynamic array, and the spread call through it was refused with
SC1090 "spread arguments in calls through 'unknown' values". A spread now
stores the source array's element type, or each of a tuple's element
types, and the binding stays typed when they match. A spread of another
iterable still makes it checked-dynamic.
2026-10-22 10:10:59 -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 c9ae4d8019 Look up spawnSync commands on the replacement environment's PATH
With an `env` option, spawnSync searched the parent's PATH for a command
without a '/'. Node (libuv) searches the PATH of the child's environment,
or the default path when that environment has none, so a command only on
the child's PATH ran as ENOENT and one only on the parent's PATH ran
where Node reports ENOENT. The lookup now follows libuv: each PATH entry
in order, an empty entry as the current directory, and EACCES when a
candidate exists but cannot run.
2026-10-22 09:55:30 -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