- A string held in a binding or returned by a call can be a missing element
read (undefined), so `head + PAD[j]` and `at(i) + PAD[j]` still add to NaN
when both are missing. Only string literals, templates and sums with such
an operand now count as always a string.
- `readdirSync(path, { recursive })` folds only the `true` and `false`
keywords. Any other flag expression, including one typed as a literal,
evaluates at run time for its effects and exceptions.
A speculative integer loop's fast copy exits to a copy that resumes the
failed iteration, and both read the header fields of the views they
access. GVN's load PRE moved the fast copy's read of a view's data
pointer ahead of the exit branch, to make the resume copy's read
redundant, and put a copy on the exit edge. The loop then kept its first
iteration in the loop, so the overflow checks of a value whose range is
known after one iteration stayed in every iteration, with a reload of the
data pointer.
A view held by a const local or a parameter that the loop does not rebind
is the same view in every iteration, and its length, data pointer and
shared storage never change. The loop's entry now reads them once, and the
element accesses of every copy use those values.
The store a constant choice emits for its arms used the first arm's
index expression, with the ranges proven inside that arm. When the index
is the compared local, `j === 0` narrows it to [0, 0] there, so the one
store, which also runs on the final else's path, converted the index to
an int32 from that range. For `j` = 4294967297 that conversion has no
defined result, and the bounds check that follows could accept it: an
out-of-bounds write could reach a valid element.
The store now reads such an index through the first comparison's
operand: the same local, read before the chain branches, so its range
holds on every path. An index that is another local is not narrowed by
the comparisons and is unchanged.
An if/else-if chain that compares one number local with integer literals
and stores a number literal to the same typed-array element in every arm,
including a final else, such as
if (x === 97) a[i] = 0;
else if (x === 116) a[i] = 1;
else if (x === 99) a[i] = 2;
else a[i] = 3;
is now emitted as one store of an entry of a constant table indexed by
the local's value (constant-choices.ts). On input whose cases do not
repeat in a pattern, such as the bases of a DNA sequence, the branches
mispredict; LLVM keeps them, because cases this far apart are too sparse
for its own switch tables.
The chain needs at least three distinct values within 256 of each other.
The table covers the cases' span, or the local's proven integer range
when that range has at most 256 values, in which case no test is
emitted. Otherwise a value that is not an exact integer in the table's
span (NaN, a fraction, or a value outside it) stores the final else's
literal, chosen with a select. The first arm wins for a repeated
value, and -0 selects the arm for 0, as strict equality does. The store
is the arms' own (the same receiver and index), converted to the
element's type as each arm's would be. Debug builds keep the arms.
k-nucleotide's transform of the input sequence uses this form: its
median wall time drops from 2.71 s to 2.21 s.
bridgePlainUnwindCalls rewrites the module text after emission on Linux.
It split the text with a lookahead regular expression, ran a regular
expression over every line to find mentions of raising functions, and
split every function that has calls into lines to test each one.
- The text splits before each `define` line with indexOf.
- Mentions of raising functions are found by scanning for `@` and reading
the name that follows it.
- A function without a plain call of a raising function rewrites only the
lines that hold `call `, marking them nounwind. A function with such a
call takes the line-by-line path as before.
The emitted text is unchanged.
A limited pass (scr_cyc_collect_set, which freeing a LifetimeRegion runs
over the objects the region owned) takes only the candidates in its set.
Its walk can still reach other candidates, and one that is referenced from
outside is blackened by scan and promoted to the next generation while it
keeps its slot in its old generation's buffer. scr_cyc_on_dead finds a
candidate's buffer by its generation, so when that object died it removed
another buffer's entry and left its own slot pointing at the freed block,
which the next pass then read.
Such a candidate is no longer promoted: it keeps the generation of the
buffer it is in until a pass takes it.
The frontend reads the same children of the same nodes many times: on a
tsc-ts build, `expression` is read 29.6M times over 253K nodes, `name`
16.4M times over 176K and `text` 26.8M times over 456K. Each read went
through AstFile.namedChildAt (the file check, the child mask, the packed
child table) and AstFile.node (the node table), and `text` through the
wire's kind switch and string table.
- AstNode.childNodeAt keeps the child nodes it has read, by wire slot, in
a per-node array sized to the node's highest named child.
- AstNode.text keeps the text it has read.
- AstFile's packed child table is a Uint32Array from the start (empty
until the first named child is read), so namedChildAt no longer
retains and releases an optional value on every call.
Cold tsc-ts build with the native compiler, 16 interleaved rounds: lower
31.68 -> 30.46 s, wall 78.81 -> 76.56 s (faster in every round); peak RSS
+105 MB, which the frontend-dispose heap trim keeps below main's peak.
The element-independent callback paths read the slot after the callback:
- filter appended the slot as it was after the predicate;
- find and findLast read the element only once the predicate held.
A predicate that overwrote or removed the current slot therefore kept or
returned the new value, where JavaScript keeps the element it read before
calling the predicate. An unfused filter over proven-present elements also
stored the placeholder it passed to a predicate with an unused element
parameter.
- filter, find and findLast keep a snapshot of the slot, read before the
callback runs (arraySlotSnapshot): the value, or the dynamic value of an
element outside the element type. filter appends that snapshot as it
was stored, and find returns it, with the checked conversion for such an
element. Fused loops take the same snapshot before a stage whose
callback ignores the element.
- Leaving an ignored element unread now applies only to arrays that can
hold elements outside their element type (elementMayBeForeign) and are
not proven present. Every other array reads the element as before, so
its filter stores the element it read.
A presence test of a literal key that a record shape does not declare
read the receiver as the record and answered undefined. In an optional
chain that conversion runs before the chain can short-circuit, so
`missing?.["a;b"] === undefined` threw a TypeError for a missing value
stored dynamically, where Node answers true. An optional read keeps the
optional chain's lowering, which reads a nullish receiver as undefined
and the undeclared key of a present one as undefined.
- A deferred class with static field initializers is reached where its
statement evaluates: its file's %init retries it there
(retryDeferredAtDeclaration), so its initializers lower in place. A failed
retry reports nothing, restores the types it degraded, and leaves the
class deferred; no later flush registers it, so it reports at its
reference instead of registering after its initializers' place. Before, a
class a later reference registered never ran its static initializers.
- A generic class that registers on retry registers the units of its
static methods. Before, a call to one was an undeclared function
(SC9001).
- A statement that lowers again after degrading a type keeps the
diagnostics of the function-local class bodies it lowered the first time:
the retry reuses those bodies instead of lowering them again.
- retriableDiags and failedTypes are a Set and a Map owned by the lowerer
instead of weak collections, and the degraded types of a mode are copied
without an empty-array fallback, which the self-hosted compiler could not
lower.
- The coverage test of a class rejected behind a receiver probe pins its
refusal under --tier=static-only, as the diagnostics corpus does; under
localized failure the class compiles.
The implicit-toolchain probe includes every <...> header the runtime and
vendored sources name, each behind __has_include. clang ships its x86
intrinsic headers for every target, so on arm64 macOS __has_include finds
<emmintrin.h> (included by scr_bytes.c and scr_string.c), and including it
there is an #error. The probe guarded only <cpuid.h> and <immintrin.h> by
target, so on arm64 macOS it failed, native cache warming reported that it
could not validate the compiler toolchain, and builds ran without the
persistent cache. The probe now guards every x86 intrinsic header family
(*mmintrin.h, <x86intrin.h>, <x86gprintrin.h>) and <cpuid.h> by target.
ts7-process.test.c defines _POSIX_C_SOURCE, which hides mkdtemp on macOS;
it now also defines _DARWIN_C_SOURCE there, as host.c does.
- A comparison records the pairs it assumes equal in order, and when it
fails, withdraws every pair assumed since it began. Before, a failed
attempt (one union arm against a non-matching arm) stayed assumed, and a
later comparison of the same pair answered true.
- Nominal objects carry their type arguments, normalized lazily, and
compare them; canonKey prints them. Before, instantiations of one
generic class compared equal by name, so a union of arrays of two
instantiations lost an arm.
A dev build splits a large program into LLVM shards, compiles them in
parallel and merges the objects with ld -r. On a native Linux host the
merge then ran objcopy --keep-global-symbols to turn the shards' promoted
symbols back into locals. A sanitized build of three.js's node materials
merges into a 660 MB object with 278k sections, and objcopy spent 425 s
of a 616 s build on it. localizeElfObject, which cross-ELF builds already
use, does the same demotion in about 2 s.
- Program-shard merges for executables, library archives and packed
libraries use localizeElfObject after ld -r on native Linux. Library
runtime localization keeps objcopy.
- localizeElfObject reads and writes extended section numbering: with
SHN_LORESERVE or more sections the counts go in section 0, and symbol
section indices from SHN_LORESERVE up go in .symtab_shndx. Before, it
truncated e_shnum and read such indices as reserved values.
- The Linux shard-merge cache identity drops objcopy.
- The intern key names bound type parameters by identity, not by name, so
environments that bind two parameters called T to swapped types no
longer share a node.
- Type equality (intersection constituents, union arms) is a structural
comparison, canonEqual, that compares signature parameters, index and
construct signatures at any depth and closes on recursive types.
canonKey no longer serves as equality, and now prints parameter types,
construct and index signatures.
- An object with index or construct signatures is not the empty object of
a narrowed parameter, so a dictionary intersected with named properties
keeps its index signature.
- Arrays and tuples enter the intern table before their elements are
normalized, so recursive array and tuple aliases close on themselves.
- Tuple elements read their optional and rest flags from the tuple's
element flags.
The test kept one live 48-byte block every 1,000 allocations, which on a
system with 64 KiB pages leaves a live block on every page, so no page held
only free blocks and the resident-memory check failed. It now keeps one
block per four pages of blocks, from the system's page size.
In a program with workers, every element of a view that may be backed
by a SharedArrayBuffer was read and written with relaxed atomics, so that
a loop which reads one element again sees another thread's write. LLVM
does not vectorize loops with atomic accesses: a byte loop over a shared
Uint8Array ran at 8.5 ns per element in a program with workers and 0.14
ns in one without.
In a counted loop that polls for cancellation between blocks of
iterations, an element read or written at the loop's counter, outside any
loop nested in the body, is now a plain load or store, like a
thread-owned view's. The counter steps by a nonzero constant and the body
does not assign it, so every iteration reaches a different element and
no read is merged across iterations or moved out of the loop. Within one
iteration, a racing write from another thread can be seen or missed, the
documented gap of non-atomic shared accesses. Other accesses, Float16
elements and Atomics keep their atomic paths.
Corpus 8332 runs these loops over shared views of each element type on a
worker and the main thread, and matches Node's output.
The native compiler's frontend-dispose sweep frees about 450 MB on a cold
tsc-ts build, but the size-class allocator keeps the freed blocks on their
classes' free lists, which only later blocks of the same size reuse. Code
generation allocates other sizes, so its pages stacked on top of the freed
heap and set the build's peak resident memory.
- scr_heap_trim (scr_alloc.c) returns the pages of the calling thread's
size-class slices that hold only free blocks (MADV_DONTNEED) and the
system allocator's free pages (malloc_trim). In the single-threaded
allocator the blocks of a returned page leave the free list and the
class carves past its frontier instead; the remaining free blocks are
relinked in address order. Worker runtimes return the calling thread's
arena pages as a parking thread does.
- scr_collect_cycles_phase, the full sweep after a phase's data died,
calls it; the native compiler calls that sweep when it disposes the
frontend.
- test_heap_trim.c checks that the pages go back, that the blocks still
allocated keep their contents, and that new blocks overlap none of
them, with and without worker arenas (another thread allocating and
freeing remotely throughout).
Cold tsc-ts build, 12 interleaved rounds: peak RSS 1407 -> 1247 MB
(-160 MB, -11.4%, every round); wall 87.1 -> 85.9 s (median).
A record that converts into several arms of a union by a width conversion
preferred the unique arm whose fields the record has or that are
undefined-only, even when that arm holds the record only by dropping fields
and another arm keeps every field but declares an optional field the record
lacks (`adapts?: true`). The record was wrapped as the dropping arm:
- In a discriminated union the value kept its discriminant but had the other
arm's tag, so narrowing on the discriminant stopped the program with
"a narrowed value does not have the type its narrowing proved".
- Without a discriminant the dropped fields were lost: `"fnName" in value`
was false and JSON.stringify left them out.
The native compiler hit the first case on
tests/corpus/2910-overloaded-callable-alias.ts: the closure that a value read
of an overloaded function alias builds is a record that is not typed by
IrExpr, and it converted into the `selfRef` arm.
The undefined-only preference no longer selects an arm that drops fields
when another candidate keeps every field, so a discriminated union selects
the arm by the record's discriminant. The lossless-arm preference still
prefers the unique arm with exactly the record's fields; when there is none,
it accepts the unique arm that keeps every field and declares optional
fields the record lacks, which complete as absent.
`scriptc build --profile-use` prints one line on stderr with the number of
functions the profile annotated, the number whose control flow changed since
it was collected (mismatched), and the number it has no record of. When more
than 10% of the functions the profile describes are mismatched, it also prints
a warning to collect the profile again. The build still succeeds.
Both the compiler library's build and the native compiler's build report the
match. Cached builds report the match of the build that produced them: the
native codegen and object caches store it beside the cached objects, and the
executable cache stores it in its entry.