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.
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).
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.
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.
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.
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.
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.
A method call on a value of unknown type (untyped JavaScript, `any`)
picked its builtin by comparing the method name with strcmp against each
candidate, in long ladders per receiver kind: a string method could test
dozens of names before it matched.
- The name is now looked up once per call in a generated hash table
(scr_dyn_methods.h, from scripts/gen-dyn-methods.mjs), and each ladder
compares the resulting id. The table verifies the name's bytes, so a
name is matched by its content, as before.
- The worker event methods compare the first character before strcmp.
- bench/dynamic-methods calls string, array, Map, Set, number and own
methods from untyped JavaScript.
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.
tsgo has no getContextualTypes or getResolvedSignatures. Each prefetch
asked for them anyway, and a failed chunk was split and retried down to
single nodes, so a tsgo build paid about two failed requests per
prefetched node. The checker facade now remembers a batch method the
server answers with "unknown API method" and asks those queries node by
node from then on.