- toString("latin1"): counts the high bytes eight at a time, copies
words of ASCII bytes, and writes other bytes from a table of their
UTF-8 pairs without branches. ASCII-only results are marked ASCII, and
other results record their UTF-16 length (one unit per byte), so
`.length` does not walk the string.
- Buffer.from(text, "latin1" | "ascii" | "binary"): copies text known to
be ASCII, copies words of ASCII bytes, and decodes eight bytes of one-
and two-byte sequences in one word, storing each output byte without
branches. Longer sequences take the previous path.
- Buffer.from(text, "hex"): reads digit values from a table.
- toString("hex") and toString("base64"/"base64url") mark their results
ASCII, so a later decode, length or index read skips the scan for
non-ASCII bytes.
The inference reads types while it runs: resolving a candidate's
constructor through a destructured require (const { TextDecoder } = util)
asks for the namespace's type, which asked for the inferred collection
types again and restarted the inference until the stack overflowed. The
lowerer now stores an empty table before inferring, so reads during the
inference see no inferred types, and stores the result after.
A typed array such as a number[] reaches untyped code as a capsule of the
native array. A keyed read `xs[i]` there missed the inline cache, which
answers only dyn arrays, and went to the member helper, which converted
the index to a string; the capsule's key lookup then parsed the string
back into a number for every element.
Keyed sites now read an element of a native number[] capsule in place
when the index is an integer below the array's hole-free prefix, and
`length` sites read the native array's length. Other capsule elements
(holes, out-of-range and fractional indices, arrays of other element
types) are answered by the keyed probe through scr_dyn_index_get without
forming the key, or by the helper as before.
Node's afterWriteTick sets afterWriteTickInfo to null before it runs the
callbacks, whichever tick it is. The runtime cleared the queued batch
only when the running tick was that batch, so a write made from an
earlier tick's callback joined a later queued tick with the same
callback and completed ahead of nextTicks queued between them. Any
afterWrite tick now closes the batch, and such a write queues its own.
The guard proves the views disjoint for one entry of the loop. An
enclosing loop may pass other views on its next pass, such as two
buffers swapped after every pass, so one pass's write and the next
pass's read can be the same bytes under different scopes. Without a
declaration, a scope holds across the whole function, and LLVM could
apply one entry's no-alias facts to another's accesses once it unrolled
the enclosing loop.
Each fast copy now begins with llvm.experimental.noalias.scope.decl for
each of its scopes. A declared scope holds from its declaration, and
LLVM gives each copy of the declaration its own scopes when it unrolls
or duplicates the code around it.
Corpus 8182 adds ping-pong passes over swapped Float64Array and
Int32Array views; the emission test checks the declarations.
Node's onwrite queues one afterWrite nextTick for a synchronous write
completion and lets later synchronous completions with the same callback
join it (afterWriteTickInfo.count); writes without a callback share one.
The step then checks 'drain' once, calls the callback once per write and
runs finishMaybe once. The runtime queued a stream tick and a nextTick
entry per write, which ran the callbacks of later writes after nextTicks
queued between them. The queued afterWrite tick now counts the writes
that join it, as Node's does.
`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 loop that reads one typed array and writes another kept every element
in memory: separate views can share a buffer, so LLVM had to assume a
store to one could change the other's elements. A loop of eight lanes
held in Float64Arrays (zr[k], zi[k], tr[k], ti[k]) ran as scalar loads
and stores of each lane on every step.
When a typed-array loop version covers at least two views and writes at
least one of them, its guard now also requires every pair of the views'
byte ranges to be disjoint: one ends at or before the other starts. In
the fast copy, each view's element reads and writes carry an alias scope
of their own and no-alias with the others' scopes, so LLVM keeps the
elements in registers and vectorizes across lanes. Views that share
bytes, or one array passed twice, take the checked copy as before. The
scopes are string-named, written inline, and named after the function
and the version, so every copy of a loop has its own. Worker programs
scope only views the guard proves thread-owned.
Corpus 8182 runs such loops over disjoint, adjacent, overlapping and
identical views, views of different element types over one buffer, and
empty views.
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 JavaScript class whose instances track which fields they have copies
the instance's property bag into its view first, attributes included,
and then wrote each symbol-keyed field with an ordinary assignment. Once
the instance was frozen, its symbol entry was read-only and the copy
non-extensible, so any later reflection that rebuilt the view
(Object.isFrozen, a property read) threw "Cannot assign to a read only
symbol property".
String-keyed fields already use scr_dyn_obj_set_field, which replaces a
field's value whatever the attributes copied into the view. The new
scr_dyn_symbol_field_set does the same for symbol-keyed fields, and the
view uses it when it copies the bag or carries integrity attributes.
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).