A frozen record's rc word carries SCR_RC_FROZEN beside its count. The
shared shape-union release compared the whole word, so the last reference
to a frozen record took the shared-reference branch: it decremented the
count and never freed the record or its fields. When an arm is freezable,
the release now compares the count without the frozen bit, as the
record's own release does.
flow follows a while body's statements and applies each increment it
meets as a statement. An increment inside an expression, such as an if
condition's comma expression `if ((i++, i > 0))`, lowers to a statement
nested in the expression; whileBound accepted it as an increment, but
flow never applied it, so a read after it kept the pre-increment bound
and lost its bounds check. whileBound now gives no bound when any
expression in the body holds a write of the counter.
A slice whose every reader only borrows it is written into a stack buffer
instead of a heap allocation. A slice stored in a record field counted as
kept, so a tokenizer's `const text = source.slice(start, i); const t =
token(kind, text, start); sum += weight(t);` allocated and freed a string
per token even though `t` is read only for `t.text.length`.
A slice local stored directly in a field of a stable record local, built
by a record literal or by a factory that only returns a record literal of
its parameters, is now lent when the record is only projected: its field
reads are lending reads, and it is passed only to record parameters whose
bodies read that field only in lending positions (a fixpoint, like the one
for lent string parameters). A record that is returned, stored, captured or
passed anywhere else keeps the slice on the heap. includes, indexOf and
lastIndexOf on string arrays now lend their search argument.
findBytesBounds proved element reads only inside for loops, and only for
`counter + c` indexes. A while loop such as
while (i < len) {
c = line[right - i]!; // right = len - 1
i++;
if (c === 10) {
if (i === len) break;
c = line[right - i]!;
i++;
}
...
}
kept a bounds check on every read. The out-of-bounds path also produced a
NaN that LLVM threaded into a following switch, which then stayed an
indirect jump instead of becoming a lookup table.
- A while condition that compares an integer counter with a typed array's
length (`i < len`, `i <= right`, `len > i`) starts a bound. The body may
write the counter only by statements that add a positive literal, and
may not write the receiver. Calls in the body are allowed: they cannot
write either binding (both are uncaptured locals), and a typed array's
length is fixed at construction.
- The body is followed in order. An increment raises the bound and the
counter's minimum by its step. An if condition that compares the counter
with the length narrows the bound in its branches (`if (i === len) break`
restores `i < len` after an increment). Branches join to the weaker bound,
and a statement that advances the counter inside a nested loop or
labeled block leaves the counter unbounded after it.
- Indexes of the form `L + d - counter` (`len - 1 - i`, `right - i`) are
proven for both for and while bounds, from the counter's upper bound and
its minimum.
- A function whose whole body is `return [a, b]` or `return { a, b }`,
with number, boolean or string fields, is now inlined where its result
is only destructured (`const [head, tail] = split(text, at)`). The
parameters are declared from the arguments, then each field straight
into its destructured binding, in the literal's order; an unread field
still evaluates into a binding of its own. No record is built, and a
slice field read only through its length or char codes becomes a
scalar slice that copies nothing. Arguments may own their value, but
as for numeric records nothing inside an argument may hold a
temporary of its own.
- A scalar slice of a proven-ASCII string computes its range inline:
the bounds clamp over the byte length exactly as scr_str_slice_range
does, and no boundary can split a surrogate pair. Other strings keep
the runtime call, which gains the same ASCII fast path.
- value-records' tuples section, `[head, tail] = split(...)` read only
through lengths, goes from two heap slices per step to none.
A function that reads arguments.length and is called through a value
reads the count its adapter stored, then clears the slot. That read ran
after the parameter prologue, so a parameter binding that threw would
leave the slot set for the next direct call. The count is now taken, and
the slot cleared, before any parameter binds.
A loop nest that sums one term per row,
for (let i = start; i < end; i++) {
let t = 0;
for (let j = from; j < to; j++) t += term;
out[i] = t;
}
where the term only reads numbers, the counters and typed-array
elements, now runs two rows per pass when both row bounds are int32,
the start is at most the end, and the stored array shares no byte with
any array the term reads. Each row keeps its order of additions, so each
sum has the bits of the loop as written; an odd row count runs its last
row as written, and any other case runs the nest as written. Element
reads that do not depend on the row are read once per column for both
rows. With two independent sums in the inner loop, neither waits on the
other's addition, and LLVM computes both rows' terms in one vector.
bytes.disjoint compares two typed arrays' element ranges in memory.
A member read on a `new F()` instance looked up the name only among the
fields of F's prototype objects. A method stored under a computed key
(`F.prototype[name] = fn`) is kept in the prototype's unknown-key field,
so the read found nothing: the method's body was never visited, and the
objects it writes stayed constant in the report. The read now also takes
the prototype's unknown-key field, as reads of the object's own fields
do.
A reference in `export { m }` resolved to the export's alias symbol, not to
the local binding, so the inference did not see the export and typed the
collection from its local uses. An importing module then used it as an
untyped Map and failed to compile (SC9001). An unrenamed export specifier
now resolves to the binding it exports, which refuses the inference as
`export const` does. Corpus 7985 adds `export { listed, renamed as alias }`.
The constant-graph pass treats a function whose call edges all run once
at initialization as init-only. Some calls recorded no edge or a
once-only one:
- `new F()` of an ES5 constructor and `f.apply(thisArg, args)` bound the
arguments without a call edge, so a function also called directly once
was init-only even when a loop constructed or applied it.
- Callbacks of Array.from, toSorted, replace and replaceAll run once per
element, comparison or match, but those methods were missing from the
repeating callback set.
- JSON.parse revivers, JSON.stringify replacers and toJSON methods run
once per value; their edges at the JSON call site now count as
repeated, including toJSON calls resolved after the call's handling.
Objects these functions write or allocate were reported as constant, and
their reads as foldable. constant-graphs.test.ts covers each case.
The match report counted the functions with profile counts after the
whole pipeline, so an annotated function that inlining or dead code
elimination later removed was not counted, while the annotation's
mismatch and missing-function warnings were. A pass-instrumentation
callback now counts the annotated functions right after
PGOInstrumentationUse runs, in the one-module and the partitioned
pipelines.
A new test builds helpers that reach the annotation and are then
inlined into main and deleted, and checks that the report counts them.
The exactness test for a product's 32-bit form multiplied the operands'
magnitude bounds in a double and accepted a result of at most 2^53. A
bound of 2^53 + 1 (3 * 3002399751580331) rounds to 2^53 there, so a
product the double rounds took the integer multiply's low bits:
`(3 * 3002399751580331) | 0` printed 1 where Node prints 0. The test is
now strict: a bound below 2^53 is exact, and no bound of 2^53 or more
rounds below it.
scriptc_ts7_json_value_end searched for the next quote again after each
backslash escape in a string, so a string with many escapes was scanned
once per escape. The quote it found now stays the candidate end until an
escape skips it, and each byte of a string is scanned once by each
search.
A test scans strings of 2,000,000 newline, backslash and quote escapes,
alone and inside an object, and compares the ends with jsonValueEnd's;
before the change it does not finish within 60 s.
SCRIPTC_DEBUG_SHAPES=const reports the constant object graphs shape
inference finds (DESIGN-constant-objects.md, stage 1): objects created by
code that runs at most once (module top level, IIFEs, functions reached
only through one call site from such code) whose fields are written only
there, that never meet a reflective or computed write, and that never
escape. Namespaces (`var ns = ns || {}; ns.util = {}`), revealing
modules, `module.exports` tables, and ES5 constructors' prototype objects
qualify. Each near miss names its rule: allocated repeatedly, created by an
init factory (one object per call site), escapes, reflective or computed
write, literal form, written after init, or lazy cache. The report counts
the member reads and calls a later stage can fold.
The analysis now models ES5 constructors (a function's prototype object,
instances reading through it) and links CommonJS require() and export
bindings to the analyzed module's exports, which it previously read as
nothing. Code generation is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The copy of a dense-array, byte-window, counted, callback or typed-array
versioned loop that runs when its guard fails no longer gives its loops
speculative integer or int32 kind copies, which emitted each loop again for
a path that rarely runs. Proven integer loops, which emit one copy, are kept
in it.