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.
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.
for await reads the value of a done result only through the
async-from-sync wrapper; an async iterator's done result is not read.
The source's generator read it for both, so an async iterator whose
done result has a throwing value getter rejected the pipeline. It now
reads and awaits the final value only when the iterator is sync.
Closing a checker process checked for its exit once and then slept 10 ms
before checking again, although the process exits a fraction of a
millisecond after its input closes. The checks now start 0.1 ms apart and
double, so the process is reaped as soon as it exits. The total wait before
the kill stays at about 50 ms (51.1 ms).
With SCRIPTC_CHECKER_SERVER=1 on Linux and macOS, the native compiler's
checker connections go to a shared `tsc-ts --api --listen` server, which
keeps the files it parsed for the next build. When the variable is unset,
nothing changes.
checkerServerName names the server by a digest of the tsc-ts executable's
identity, the working directory, the file system callbacks, the timing
flag and the TSC_TS_* environment. scriptc_ts7_attach takes the server's
lock file with a non-blocking flock, connects to its socket in this
user's private server directory ($XDG_RUNTIME_DIR/scriptc or
/tmp/scriptc-<uid>), and starts the server when none answers: in its own
process group, with its standard streams on /dev/null and no other
descriptor of the build, with --idle-exit 300 and TSC_TS_LISTEN_MEMORY=2048
unless the environment sets it. A build that finds the server in use,
cannot start or reach it, or gets no answer to a ping checks with its own
tsc-ts process, as without the variable; so does a tsc-ts without
--listen, which exits as it starts. The syntax-only source parser, which
reads no libraries, always uses its own process and leaves the server to
the program's checker.
The paired copy's test read the stored and read arrays' headers whatever
the row range, so a nest whose arrays the program assigns only for a
nonempty range dereferenced their NULL headers on an empty range, where
the loop as written never reads them. The overlap tests now run only
when the nest has at least two rows, and an array that is not there
answers that it is not disjoint, so the loop runs as written.
- 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.
for await over a sync iterator awaits the value of the result that ends
it too, so a sync iterator that returns a rejected promise rejects the
pump. The source's generator skipped that value and reported success.
It now awaits a final value that is an object (a promise included)
before it stops, without closing the finished iterator.
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.
To key an implicit-any function's instance, storedImplicitArgumentType
lowers a JavaScript arrow or function-expression argument to read its
function type, then discards the value; the call's argument list then
lowers the closure again. Every such closure was lowered twice, and a
closure nested in another one four times, and each lowering lifted its
own copy of the body.
The first lowering now keeps its value (Lowerer.speculatedClosures), and
the argument's own lowering in the same function and with the same type
bindings takes it instead of lowering the closure again. A lowering that
reported diagnostics is not kept.
Preflight admits a createRequire loader's require.resolve call with a
computed request inside a try block, but lowering refused the call, so
in JavaScript source the whole statement became a runtime fence. The
fence threw before the request expression ran, and before anything else
in the statement: `try { where = load.resolve(candidate(name)); }`
skipped candidate()'s side effects, so the catch clause saw a different
state than in Node.
Such calls (a computed request, another argument shape, or
runtime-computed options, on require.resolve and require.resolve.paths)
now evaluate their arguments in order and then throw the fence, as a
computed require call does. The addon-probe fixture checks that a
computed candidate path runs before each failed lookup.
A hoisted call runs before the rest of its write. An argument such as
`i++` in `acc = acc * 3 + i + tally(i++)` would then move that write
ahead of the write's earlier read of `i`, so such a call stays where it
is and the write is not speculated.
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.