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 bootstrap test builds its own seed when CI shares none, and that build
still applied the committed function layout, so its LLVM listed functions in
a different order from the native compiler's own emission. Both seed builds
now share one environment, which leaves the layout out.
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.
The native bootstrap compares the seed's LLVM, which the Node-hosted
compiler emits, with the LLVM the native compiler emits for itself. The
seed's build applied the committed function layout and the native
compiler has none, so the two listed the same functions in different
orders. The layout only orders functions, so the seed is built without it.
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.
- 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.
The native toolchain manifest's lib_diagnostics path is relative to the
platform package's dist/bin. installNativeCli copies the manifest into the
scriptc package's bin and relocates each asset path, but lib_diagnostics
was not in the list, so it pointed at a missing file under the scriptc
package and npm installs did not use the prebuilt lib diagnostics. It is
now relocated with the other assets.
Stage N0 of the localized-failure design (part 2, canonical type
normalization), report-only.
canon(type, env) resolves a checker type under generic bindings to a small
set of canonical shapes: primitive, literal, unit, top, object (nominal for
classes and standard-library interfaces, structural otherwise, with
members resolved lazily through the checker), array, tuple, flat union,
unbound parameter, and an opaque leaf. Intersections whose constituents
agree, or that are all object-like, resolve to one object; a branded
primitive is the primitive; T & {} drops T's unit arms. Type parameters
substitute from the environment, so a generic alias spelled over a generic
body's own parameters resolves without asking the checker to instantiate,
and the alias reference keeps the original checker type. Nodes intern by
type and bindings, so recursion closes on the node under construction.
Nothing calls it yet: N1 runs it in shadow mode beside mapType.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A class extending a program class whose own declaration was rejected
(a degradable field type) failed collection with an inheritance error
that carried no cause. The localized-failure report then counted the
derived class, and the uses of it, as remaining blockers instead of the
base field's cascade. The error now names the base's failure group as its
cause.
A shape union's tag helper switched on the record's shape number. Inlined
into a caller's branch on the arm, LLVM folded the two into one jump table
per site, spanning the arms' shape numbers. In the native compiler, whose
IR unions span about 112 shape numbers, that came to about 27,600 tables
and 9 MB of read-only data, most of which a cold build pages in.
A union whose arms span more than 32 shape numbers now maps the shape
number to its arm through one constant table, indexed by the shape number
minus the arms' lowest. An entry is a byte, or a 16-bit word for a union
of 255 arms or more; the all-ones entry names no arm. Callers branch on
the small arm tag. Narrower unions keep the switch, whose folded jump
tables stay small and dispatch in one jump.
tsc-ts 0.0.10 serves its API on a Unix socket with `--api --listen <path>`
on Linux and macOS, reads its default library files from disk with
`--default-libraries-from-disk`, and is built with scriptc 0.4.5 and a
profile collected with it. It depends on @scriptc/threads 0.4.5. The
release-age exclusions admit tsc-ts 0.0.10, its platform packages and
@scriptc/threads 0.4.5.
- Index walks (slice, substring, substr, startsWith/endsWith with a
position, lastIndexOf results) on a non-ASCII receiver of 64 KiB or
more: once they would have paid for one pass over it, the receiver's
index entry keeps every unit's byte offset (a 32-bit base per 64 units
and one byte per unit), and indexes map with two loads. Before that, a
walk within 64 units of the hot cursor starts from it, without the index
search. The index entry stays 128 bytes on 64-bit targets.
- lastIndexOf on non-ASCII receivers tests the needle's first byte before
comparing, as the ASCII path does.
- normalize: a quick check (UAX #15) returns the receiver when every code
point is inert for the form: unchanged when normalized alone, combining
class 0, and, in the ranges checked, never the second element of a
composition. Other text normalizes segment by segment, each starting at
a starter, so runs of inert code points copy as they are and a lone
precomposed letter's result is kept per form. The per-code-point facts
come from unicode_normalize, 256 code points at a time.
collectProgram and the runtime-optional array analysis walk every file of
the program before any function body is lowered. This change removes work
they repeated and moves single checker requests into existing batches. The
emitted IR is unchanged.
- classReferences reads a per-file index of identifiers by text, built in
one walk and kept on ProgramAnalysis, instead of walking every file once
per class.
- widenedType reads its memo before the syntactic position test, and
memoizes nodes without a wide position.
- isJsSourceFileName tests the extensions with endsWith.
- NanCodedReads groups the identifiers of a name by resolved symbol once,
instead of resolving every same-named identifier for every binding.
- Each function's return scan in the optional-array fixpoint is a
SiteDependencies site, so incremental passes rescan only the returns
whose facts changed.
- The fixpoint's site batch also types the receivers widenedType reads and
for-of iterables, and collectDynamicViews prefetches the values it types
in one batch before its walk.
A native record, array or class instance boxed as unknown or any is the same
object: every box of it is one value, untyped writes, optional fields,
deletes and expandos show through typed reads and other boxes, nested
records and arrays are the typed program's own, and the value keys Map, Set
and WeakMap entries as itself. Main already behaves this way since records
and arrays box as live references; this corpus program pins it for the
single object model's later stages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sum of products of integer typed-array reads that only reaches a
bitwise operator, such as `(p[j] * 77 + p[j + 1] * 150) >> 8`, now adds
in 32-bit integers like a sum of the reads themselves: a product whose
every result is a safe integer, and never -0, is an exact integer or NaN.
A NaN-coded read passes its validity bit up through those sums, so the
integer result selects 0 for a missed read without a floating compare,
and a loop over such sums vectorizes without converting to doubles.