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.
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.
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.
A callback loop version emits its loop twice. A loop inside either copy
could be versioned again, so a nest of loops that each call a callback
built just before them (`const g = adder(i)`) doubled at every level: a
three-deep nest was emitted seven times per function.
A loop inside a callback version, or the versioned loop itself emitted
again for each copy, is no longer versioned, so a loop nest is emitted at
most twice. The outer loop keeps its version; inner loops keep their
calls as they are.
- 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.
Structural positions started with a file's absolute path, so lowered
names (closures, recursive records and unions, and every name keyed by a
type that holds one) changed with the directory the program was compiled
in. Union arms are ordered by their types' keys, so a program compiled in
a new temporary directory could also get its union arms in another order.
union-retype.test.ts compiles each program in a fresh temporary
directory, and in about one run in ten the arms of the visitor's unions
came out in an order where no retag keeps its tags, which failed "lowered
identity retags share the box".
Positions now name a file by its path relative to the entry's directory
(default library files keep their base name), so the same program gets
the same names wherever it sits.
`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.
`for (let i = 0, p = 0; i < count; i++, p += 4)` kept both counters as
doubles: the counted-loop match takes a single declaration and a single
update. A counted loop now also accepts further counters declared in its
head and stepped by a constant in its update, when the condition does not
read them and the body does not write them. On body entry k such a counter
holds `start + step * k`, so its range follows from the driver's: k is at
most the driver's span over its step.
When that range can pass 2^53, the versioned loop tests
`|start| + |step| * span / |driver step| <= 2^52` at run time, and an
unproven start is tested for an exact safe integer, as the driver's is.
Nested update blocks (`i++, j += 2, k--`) are flattened. Byte windows and
dense loops still take only single-counter loops.
Array reduce typed its accumulator by the call's checked type, widened
with undefined only when the callback could return a missing value or a
receiver without an initial value could have holes. Three runtime values
fell outside it and threw "not representable in the target union":
- an initial value read from a missing element;
- a callback result of another primitive (`missing[0] ?? "fallback"`);
- a callback wrapped in parentheses, which the analysis and the callback
lowering did not see as the arrow it wraps.
The accumulator now also takes undefined from a possibly missing initial
value, and the primitive arms the callback's result can have. Array
callbacks are taken without their parentheses.
`arguments.length` in a fixed-arity function expression folded to its
declared parameter count. When the expression flows into a fixed-arity
function type with more parameters, as array callbacks do (value, index,
array, and the accumulator for reduce), every call passes that type's
count, and the length is that count.
Since dense-array loops bounded by an invariant limit are versioned, the
test's loop has a speculative site in each dense copy. The test paired
every resume point with the first site's labels; it now checks each
site's resume points against that site's third and fast copies.
postMessage of a typed record boxes it as a live record capsule, and the
message encoder materialized the capsule's checked-dynamic view to read
it: it built the view object, listed its own keys, tested each one for
enumerability and read it back, then released the view and the key list.
On an object-only run of bench/channel-messages that was about half of
the encode time.
- A live record capsule whose view was never created (no undeclared
properties, no writes through an untyped view) now encodes from the
record's field hooks (ScrDynTypedRecordOps), which answer exactly the
view's keys, order and boxed values. Nested records encode the same way
to a depth of 32, then through their views as before. Capsules with a
view, published values, and every other value encode as before.
- The mail event that carries a message lives in the message's block, so
a queued message is one allocation instead of two.
Corpus 7139 and 7360 post typed records on a MessageChannel and between a
worker and the main thread: optional fields, nested records, a record
reached twice, a chain deeper than the recursion limit, a parsed record
with undeclared keys, and a function-valued field.
A speculative integer loop's failed check used to continue at the failed
statement inside the ordinary copy. That gave the ordinary copy a second
entry, so LLVM saw an irreducible cycle and did not hoist its invariant
loads or keep its values in registers. A loop whose check fails on every
entry, such as an inner loop over fractional fields, then ran every
iteration in that unoptimized copy.
A failed check now continues in a third copy that is entered only from the
fast copy: it re-executes the failed statement, the rest of the body and
the update, then jumps to the ordinary copy's header. The ordinary copy has
a single entry and stays a natural loop.
Each loop site also counts its entries in a row whose fast copy bailed. A
fast copy that completes a speculated write and leaves the loop clears the
count, and after 8 bailing entries in a row the site runs the ordinary copy
only. The count is a hint, read and written with unordered atomics: both
copies give the same results.
A row-major dot product, `const base = r * cols; for (let j = 0; j < cols;
j++) s += w[base + j] * v[j]`, kept one index test per `w` read: the
typed-array loop version guard covered an index `k * counter + terms + c`
only when every loop-invariant term had a proven integer range, and `base`
has none when `cols` is a parameter.
The guard now also accepts a pure loop-invariant term without a proven
range. It evaluates the term once and requires an exact integer (not -0)
between -2^32 and 2^32, then checks the first and last index as for any
affine index. In the fast copy the term has that range, so the index is
integer arithmetic and the reads take no index test. Any other value runs
the checked copy.
Adds a Float32Array matrix-vector product with four accumulators per row
to the typed-array-kernels workload.
The 16-character searches (the first-character search, the start filter
and greedy class loops) and literal runs checked `i + n <= len` in 32-bit
arithmetic. Subjects may be up to 2^31 - 2 characters, so near that length
the sum wrapped negative, passed the check and read past the subject. Each
check now compares the characters left (`len - i`, which cannot wrap)
with the characters it reads, and advances the position only after it
passes.