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.
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.
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.
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.
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.
- 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.
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.
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.
- 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.
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.
`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.