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.
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.
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.
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.
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.
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.
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.
`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.
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.
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.
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.
The split tools ran oxfmt in the checkout they rewrite. oxfmt looks up
its configuration from that directory and from the formatted files'
directories, and runs an oxfmt.config.ts it finds there, so a branch that
rehome-split rewrites could run code with the maintainer's permissions.
The tools now format through format.mjs, which passes the tools
checkout's .oxfmtrc.jsonc with --config and disables the nested lookup,
so no configuration file of the rewritten checkout is read or run. With
no files to format it does nothing, where oxfmt would format the whole
directory.
The split-tools tests cover split-methods and move-state on a checkout
whose root and src directory commit an oxfmt.config.ts that records it
ran.
The merge helper for a fixed-shape spread into an index-signature record
wrote the source's keys in the shape's declared order when it was
lowered. Key-order witnesses can replace that order afterwards with the
order the program builds the source's values in, so a source declared
`{ start, end }` but built as `{ end, start }` merged as `start,end`
where Node gives `end,start`. The source's writes now go into one block
that a shape-order finalizer fills in the settled order, as the other
order-snapshotting helpers do. Corpus program 6916 covers sources built
in another order than declared, with an absent optional field and with
two spreads.
`for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 32) n++`
tested the string's ASCII bit and the index against the length on every
character, and its fallback is an opaque runtime call, so LLVM could not
hoist either test or vectorize the loop.
A typed-array loop version now also covers `s.charCodeAt(index)` of a
string binding the loop does not write, at a counter index plus a
constant or another index the version proves. A string is immutable, so
its bytes, its length and its ASCII bit are fixed when it is built. The
guard requires the ASCII bit, so each UTF-16 unit is one byte and the
length is the byte length, and the fast copy reads each unit with one
byte load. Other strings and indices run the checked loop.
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.
A record conversion behind a written type predicate built the copy in the
target shape's field order and wrote every optional field, so a source
object without an optional key came out with that key present and holding
undefined, and Object.keys listed the target's order. The copy now keeps
the source's key order, as a width copy does, and an optional field the
source lacks stays absent.
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.