`decoder.decode(view!)` on a `Uint16Array | undefined` (or another typed
array that may be undefined) was refused with SC2020: the assertion keeps
the undefined arm for decode, and the decode input only read the
undefined arm of `Uint8Array | undefined`. The undefined arm of any typed
array that may be undefined now reads as an empty array of the same type,
so a defined input decodes its bytes and an undefined one decodes no
bytes, as in Node.
A module binding whose initializer only allocates data (a function, an
arrow function, a literal or a registry object) is initialized only when
a reached body uses the binding. Only reads and updates counted as uses,
so a binding that reached code only assigned was never initialized, and
the assignment, which checks that the binding is initialized and
releases its value, threw "Cannot access '<name>' before
initialization" where Node runs. Assignments now count as uses.
Corpus 8320 assigns two such bindings, one through an assignment
expression, without reading them.
Every read or write of a symbol-keyed property on a checked-dynamic value
formatted the symbol's address into a new string to find its storage key,
and every conversion of a symbol to a checked value allocated a new box.
Programs that key hot properties by symbols, such as Effect's type ids
checked on each combinator, spent a large share of their time there.
The storage key is a function of the address, so a 256-entry per-thread
cache by address now returns the existing string, released at exit. A
symbol also keeps a weak pointer to its live box: conversions share it,
and the box clears the pointer when it is freed.
The sanitized nodes build compiles one 1.14 GB LLVM module, and its clang
compile peaks at about 16 GB, more than the CI runners have, so the
fixture failed in every san shard that ran it. It now builds without the
sanitizer there, as in the plain lane; every other three.js fixture keeps
it. The fixture goes back under the sanitizer once sanitized dev builds
compile large programs in shards.
JavaScript reads an iterator's `next` when the iterator opens and keeps
calling that function, so a `next` the iterator assigns while it runs is
not called. The for-of loop and the spread drain over an object-literal
iterator read the field on every step; they now read it once after the
open (classIteratorNextRead) and call the function they read. The
checked-dynamic spread drain (%iterator.pack.checked) does the same,
using the stepping the for-of and Array.from drains already use.
`for await` over a synchronous iterator closes it on an early exit the
way the async-from-sync iterator does: return()'s result is read for done
and value, and the value is awaited, so a pending promise settles before
the loop continues and a rejected one rejects the loop (after a throw from
the body, the body's error still wins).
`new kinds[i](args)` over an array of class values read the element and
narrowed it to the class value before the arguments ran, so an index past
the end threw "is not a constructor" without evaluating them. JavaScript
evaluates the arguments first: the construction is now a call of a lifted
helper (checkedElementConstruction) that takes the element read and the
arguments, in that order, then runs the check and constructs. Keeping the
arguments in one call also keeps a spread's argument pack alive until
every argument has read it.
A checked class-value boundary accepted every subclass's class value into a
base class's slot, so construction through the slot could call a subclass
constructor that takes different arguments with the base constructor's. It
now accepts only the subclasses whose class-value constructor takes the
same arguments (classValueAbiEquals, the rule the classval upcast validator
already applied); any other subclass fails the check with a TypeError.
findRecordFieldProducers counted only direct reads of a parameter, so a
factory such as `(text) => ({ text, upper: () => text.toUpperCase() })`
counted as storing `text` only in its field. A caller's slice passed to
it was written into a stack buffer, and the closure kept by the caller
then read whatever the next iteration wrote there. Captures by closures
and classes now count as reads, so such a factory is not a producer.
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.
Reading `ownKeys` from Reflect read as a value (`const r = Reflect;
r.ownKeys`) gave the member function that throws SC1090 when called,
although the spelled `Reflect.ownKeys` read already has a function
value. The namespace object now holds that function value, so calls
through the alias run and `r.ownKeys === Reflect.ownKeys` is true, as in
Node. The spelled read and the namespace member share one lowering
(reflectMemberValue).
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.
The tsc-ts 0.0.9 that scriptc includes rejects --listen, so with it every
build that sets SCRIPTC_CHECKER_SERVER=1 checks with its own tsc-ts
process. The variable's description now says so.