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 borrowed Map.get local holds the entry's own reference, so every use
must run before anything that can release the entry. Two cases escaped
that proof:
- A local declared from the borrowed local (`const copy = held`) is a
stable alias of it, with no reference of its own. Its reads after a
write to the map were not counted as uses. Reference locals declared
from the local, at any depth of the statement that declares them, now
count as its uses.
- A read from a map that no local holds (`fresh().get(k)`) borrowed an
entry of a map released at the end of the declaration. The map must
now be a local or a field chain rooted at one, whose changes the proof
sees as edge changes.
Both keep the owned read. Corpus 8181 covers the two cases, and the
emission test checks that their reads retain the value.
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.
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 partitions of a program build have no locals for ThinLTO to promote:
symbols that other partitions refer to are already hidden. What changed
the modules after importing, and with them the object cache keys, of
partitions an edit did not touch was the numbering of struct types. Every
partition declares the program's types under the same names, and the thin
link loads the importing partition and the partitions it imports from into
one context, which tells same-named types apart by appending a counter
that runs across the whole link. An edit that adds a type anywhere shifted
the counter.
Partitions now drop the names of their struct types before their summary
is written; importing matches unnamed types by their bodies. The
promoted-locals option, the per-partition source file name and the
partition index in the summary key are removed.
Each deferred stream step (afterWrite, emitReadable, resume and the
others) queued a ScrStreamTick and a process.nextTick entry, both from
libc calloc and free. They now come from the runtime's small-block
allocator, as do the nextTick entries scheduled through
scr_next_tick and scr_process_write_callback.
The stream unit emitted each event with a newly allocated name string.
The names it emits ("data", "end", "drain", "finish" and the others)
are now immortal static strings, so an emit allocates no name.
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.
The run-time resolver parsed package.json as it read it, so a file that
starts with a UTF-8 byte order mark failed to parse and computed require
and require.resolve ended with the invalid-package-config refusal. Node's
reader skips the mark; the resolver now does too.
The corpus program writes the package with the marked package.json to a
fresh directory at run time and resolves it by its absolute path, so no
committed file contains the mark.
- A binding that a local class captures counts as written: the class's
methods may assign it while the loop runs, so the call keeps its helper.
- Copied bodies rename the labels of switch and for-of statements and the
binding of for-of loops.
- The known-union and record-temporary cleanups rewrite switch cases and
for-of bodies, where they left declarations of removed locals.
- Copied locals keep their source bindings. In programs that write
contract types, a parameter with a source binding keeps its own local,
so an Int32 parameter is checked against the elements the loop passes.
- 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.
createPrivateKey decoded a string passphrase as UTF-8 whatever the
options' encoding, so a correct passphrase given in that encoding failed
to decrypt, and an encrypted export read its passphrase the same way.
Both now decode it with the encoding, as Node does.
An RSA-PSS key whose parameters name a digest the runtime does not sign
with, such as sha512-224 or sha512-256, was imported without its digest,
MGF1 and salt restrictions, so it allowed operations the key forbids. It
now throws a coded SC2020 error that names the digest. Parameters that
are malformed still leave the key unrestricted, as OpenSSL reads them.
The packing path for `(T | undefined)[]` rest parameters saved plain
arguments to temporaries but read tuple fields only when it built the
pack, after every later argument had run. A later argument that wrote
the tuple changed values already spread into the call. Each tuple field
is now saved where its spread appears, in argument order.
A server with requestCert read a runtime rejectUnauthorized through
JavaScript truthiness, so null, 0 or "" let clients without a valid
certificate through, and tls.connect read it the same way. Both now treat
only false as off, as Node's options.rejectUnauthorized !== false does; a
standalone TLSSocket keeps Node's truthiness test.
An X509Certificate's methods read the record's raw Buffer, so writing
into the Buffer that certificate.raw answers changed later checks, the
PEM and the public key. The record now keeps a hidden copy of the DER
that the methods read, and raw stays the same Buffer at every read, as
in Node.
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.
- Checked conversions from dynamic values key their builders and matchers
by the type with `?` after every `(T | undefined)[]`, at any depth.
`number[][]` and `(number | undefined)[][]` share a typeKey, so the
second conversion reused the first one's builder and rejected undefined
elements.
- The dynamic matcher of a `(T | undefined)[]` takes undefined elements,
so a union with such an arm matches arrays that hold undefined.
- An optional-element rest parameter packs tuple-spread values and plain
arguments together as present undefined elements, each evaluated once in
argument order, instead of converting each value to T.
- The element-state scan counts an argument in a rest position that may be
undefined, and a spread tuple with undefined elements, as undefined
elements of the rest array, so reads in the callee check element states.
- A union index read keeps the receiver in a temporary when the key can
assign it, so `a[(a = other, 0)]` reads the array `a` held before the key.
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 native build of a program that installs the C scanner but decodes no
reply, such as the self-hosted source parser and CommonJS checks, failed
at startup with "Cannot access 'valueEnd' before initialization": the
build left the module variable uninitialized because its only other use
was the assignment in useNativeJsonValueEnd, which checks it first.
The scanner is now a property of a constant object that the decoders
and useNativeJsonValueEnd read, so it is initialized in every program.