Commit Graph
4098 Commits
Author SHA1 Message Date
Chris Tate 8bc8fc54ca Bound string reads of while loops by their counter tests
With while-loop versions, `while (pos < end && s.charCodeAt(pos) === 32)
pos++` took a version whose guard could never hold. The counter scan
records only typed-array accesses, so the string read fell back to the
statically ranged check. For a counter held in an int32 slot, that check
asks for a length above 2^31 - 2. The loop paid for the guard and ran its
checked copy.

The counter scan now records `s.charCodeAt(i)` and `s.charAt(i)` of a
string like typed-array reads. Their guard compares the counter's bound
with the string's length, as for a typed array. Corpus 7302 adds while
loops over counters below a string's length.
2026-10-22 17:47:26 -05:00
Chris Tate edb2755a9b Merge main 2026-10-22 17:46:30 -05:00
Chris Tate d8d74dab97 Keep declaration order and override placeholders in record spreads (#1834) 2026-10-22 17:32:44 -05:00
Chris Tate 6132fcf7c2 Dispatch checked-dynamic method calls on method ids (#1851) 2026-10-22 17:25:35 -05:00
Chris Tate 4f6c55a8e6 Narrow filter results through written type predicates with checked conversions (#1819) 2026-10-22 17:21:41 -05:00
Chris Tate 4082dd0189 Check integer-or-NaN indexes without a double round trip (#1872) 2026-10-22 17:21:21 -05:00
Chris Tate 3b3d78f5d3 Emit function bodies on forked workers (#1852) 2026-10-22 17:21:00 -05:00
Chris Tate 5195e2b63a Merge main 2026-10-22 17:19:37 -05:00
Chris Tate 3520eaa09d Merge main 2026-10-22 17:17:27 -05:00
Chris Tate 81bf6cb0c7 Check integer-or-NaN indexes without a double round trip
An array or typed-array index that is an exact integer or NaN, such as an
element read that may miss (`t[i]!`), a `charCodeAt`, or a sum of those, is
a valid index when it is ordered and below the length. The index check now
tests that with one ordered compare and the integer, taken from the value's
32-bit view or from one conversion, instead of converting the index back to a
double and comparing it.

Number locals that are declared once with an initializer, are not captured
or boxed, and are only written such values carry the same fact, so
`const c = t[i]!; table[c]` and `(h * 31 + c) | 0` take the integer paths
that the inline read already takes.
2026-10-22 17:14:28 -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
Malte Ubl 69cbdc2d94 Merge remote-tracking branch 'origin/main' into up/key-order-spreads 2026-10-22 15:11:56 -07:00
Chris Tate 5772f58cad Skip emitter analysis walks whose answers are already known (#1861) 2026-10-22 17:04:12 -05:00
Chris Tate 9b0ec269ac Select AST children by wire slot in the generated getters (#1870) 2026-10-22 17:01:35 -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 8bc4586b9a Select AST children by wire slot in the generated getters 2026-10-22 16:56:13 -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 b03cb004ad Send an http client request's head and body in one write (#1867) 2026-10-22 16:50:41 -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 36db0613cc Bound the compiler's cyclic garbage to twice its live heap (#1863) 2026-10-22 16:46:32 -05:00
Chris Tate 7a5fdae1bd Send an http client request's head and body in one write
request.end(body) on a request whose head was not sent yet wrote the
head and then the body. On a socket that was already connected (a
reused keep-alive connection), each write is sent at once, so the two
went out as separate segments, and the server could handle the head and
answer before the body arrived. Node sends the head together with the
body. When the client and the server run in one program, the server's
'end' and 'close' for the request and the client's reading of the
response then depended on which socket the poller reported first: corpus
5409-http-held-request-body printed the client's line before the
request's 'end' and 'close' in a macOS dev build.

The head and a body known at end() now go out as one write, as Node
sends them (the server's response already did).
2026-10-22 16:45:59 -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 e74bd658fd Inline Map retains and Map.size in release builds (#1862) 2026-10-22 16:24:56 -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 4d049fbb8c Merge pull request #1850 from vercel-labs/ctate/prepare-v0-4-37aaa7
Prepare v0.4.2 release
v0.4.2
2026-10-22 15:09:47 -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 f7ef188d96 Give the sanitized bootstrap seed job swap (#1864) 2026-10-22 14:34: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 f5a3faf18d Bound the compiler's cyclic garbage to twice its live heap
The cycle collector widens the growth fraction that schedules full passes
up to 16 times the heap left by the last pass while passes find little.
Lowering a large program leaves hundreds of megabytes of cyclic garbage,
and whether a scheduled pass reclaims it before the build's peak depends
on where the growth rule fires: small changes to the compiler moved the
peak of a tsc-ts build between 1.66 and 2.05 GB.

The native compiler now sets the growth cap to 2 at startup, as
SCR_CYCLE_GROWTH_CAP=2 does, so a full pass runs once the heap has grown
to three times its size after the last one. scr_cyc_set_growth_cap sets
the cap for a program from its own code; the environment variable still
takes precedence, and child processes are not affected.
2026-10-22 14:25:55 -05:00
Chris Tate 83b2b34b6e Inline Map retains and Map.size in release builds
Release builds no longer import runtime bitcode, so program code called scr_map_retain_v for every Map read out of a container and scr_map_size for every size read. A Map retain now uses the emitted increment helper, as string retains do, and Map.size reads the live count through an internal helper.
2026-10-22 14:19:04 -05:00
Chris Tate fbe4067b3b Skip emitter analysis walks whose answers are already known
The LLVM emitter's constructor walks the whole module about 33 times.
Several walks repeat work another already did, or run where the result
is provably empty:

- computePublishedTypes and computeCycleMutability run once and are
  shared (may-throw, immutable fields and traced shapes took their own).
- The WebCrypto-asymmetric and Ed25519 runtime-feature conditions join
  the feature scan's one walk instead of walking the module again.
- int32 slots: the fixpoint skips the write walk of a function without
  a candidate write, dynamicFieldStores stops once every field name is
  dynamic, and dynamicallyStoredFamilies returns early when no f64
  field can be named by a dynamic store.
- stableGlobals stops once no candidate is left, the second pass of
  findAppendedArrayLiterals and the target resolution of
  findConstantCallbacks run only when there is something to find,
  boxedUnionIds needs unions, and observesStacks walks for call sites
  and the "stack" literal only when a dynamic key asks.
- everyModuleNode visits type trees without a closure per node.

The emitted LLVM IR is unchanged.
2026-10-22 14:15:29 -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 27bc23b3c2 Include the refreshed optimization profile in v0.4.2 2026-10-22 13:24:44 -05:00
Chris Tate 8d4ee22009 Refresh the native compiler's optimization profile (#1858) 2026-10-22 13:23:05 -05:00
Chris Tate e39ddf3d00 Refresh the native compiler's optimization profile 2026-10-22 13:18:04 -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