Commit Graph
1220 Commits
Author SHA1 Message Date
Malte Ubl fd5615a274 Return from immortal releases on the inline path
The inline half of a class or record release sent NULL, last and
immortal references to the out-of-line slow release. Static literals
were the only immortal objects then; a published graph is released as
often as ordinary objects (tsc-ts checking a published program spent
more time in the no-op calls of Node releases than in any checker
function). The rarely taken branch now tests for immortal first; the
common decrement path is unchanged.
2026-10-10 00:57:58 -05:00
Malte Ubl 3487d17348 Publish deeply frozen plain-data graphs to workers without copying
A postMessage or workerData graph whose objects and arrays are all
frozen, ordinary and hold only primitives, strings and such containers
is now shared by pointer: every node and string becomes immortal
(rc == SIZE_MAX, so retains, releases and the cycle collector skip it
and nothing frees it), strings cache their key hash first, and the
nodes leave the sender's cycle buffers. Posting an already published
root is O(1). Anything else is cloned as before; SCRIPTC_PUBLISH=0
turns publication off and SCRIPTC_PUBLISH_TRACE=1 reports each choice.

With the receiver freezing what it receives (Node's clone is not
frozen) and no identity comparison across messages, the behavior
matches Node's structured clone. Published nodes leave the RC audit's live counts, since they are never
freed, and the small-integer dyn cache becomes process-wide (filled
before main): a published graph can outlive the thread that created its
nodes, and the thread-local table died with its thread.

The new corpus program pins that
contract both ways, including a worker-published graph read after the
worker exited and a shallowly frozen graph that must still be cloned.
2026-10-10 00:57:58 -05:00
Malte Ubl 33b040d9d7 Merge pull request #812 from vercel-labs/perf/workers/01-worker-tax
Worker tax 1.47x -> 1.04x (Linux), 1.91x -> 1.07x (macOS): per-thread allocator, TLS blocks, one exception alert
2026-10-10 00:57:54 -05:00
Chris Tate 1767fe7fa0 Merge main into perf/workers/01-worker-tax 2026-10-10 00:55:35 -05:00
Chris Tate cbbdf1560c Merge pull request #806 from vercel-labs/perf/tscts/10-checker-hot-paths
tsc-ts -8.6%: borrow checker hot paths (walks, wraps, casts, borrowed returns)
2026-10-10 00:33:08 -05:00
Chris Tate 69b1523466 Merge main into perf/tscts/10-checker-hot-paths 2026-10-10 00:24:43 -05:00
Chris Tate e74dac7d5c Make the worker stack-overflow fixture independent of event order (#816) 2026-10-09 21:21:48 -05:00
Chris Tate f388612dcd Make the worker stack-overflow fixture independent of event order
A worker's messages and its uncaught 'error' event reach the parent on
separate channels in Node, so their relative order is not specified and
can differ under load. The fixture now records both and reports them when
the worker exits, which Node and native executables deliver after both.
2026-10-09 21:19:20 -05:00
Chris Tate 198f27eea9 Keep the native compiler independent of weak keys (#810) 2026-10-09 20:37:13 -05:00
Chris Tate ead81b8788 Give each native helper build a private LLVM input (#811) 2026-10-09 20:34:49 -05:00
Chris Tate d08f94b7c3 Give each native helper build a private LLVM input
Concurrent builds of different entries with the same file name share
the default .ll path in a common output directory. The helper read that
shared file in place, so one build could compile the other program's
LLVM text. The helper now always reads a private copy written in large
batches, as before partitioning.

The partition debug-information test also matches the Mach-O
__debug_info section name, so it checks the same property on macOS.
2026-10-09 20:32:08 -05:00
Chris Tate 34cb02d6d1 Keep the native compiler independent of weak keys
Compiler caches keyed by programs and lowering passes move onto the
objects that own their lifetime, and the field-creation pass recognizes
its own writes by their receiver temporaries. A source test keeps weak
collections out of the compiler. Records and arrays that reach an
untyped slot through a union now keep their identity, and empty object
literals are keyed by their native object, so both work as WeakMap and
WeakSet keys instead of failing at runtime.
2026-10-09 20:26:57 -05:00
Chris Tate bca2cf0f2a Optimize and generate native code in parallel partitions (#809) 2026-10-09 20:10:58 -05:00
Chris Tate 3ea231d70d Optimize and generate native code in parallel partitions
Large executables were simplified and inlined as one module before the
optimizer split them, so most of a release build ran on a single core,
and any edit recompiled the whole program.

The LLVM helper now divides the program before optimization. It splits
the emitter's assembly text directly, so partitions also parse
concurrently, and falls back to LLVM's module splitter for any input
outside the emitter's grammar. Internal functions referenced by a single
function stay with that function, and globals stay with their first user.
Optimized partitions follow the ThinLTO model: each is simplified and
summarized on its own thread, a thin link imports small functions across
partitions for inlining and returns unexported hidden definitions to
internal linkage, then each partition is optimized and compiled on its
own thread. Unoptimized partitions compile independently, so dev builds
of large programs are parallel too.

A private per-helper cache directory stores simplified partitions under
the hash of their source and objects under the hash of the module after
importing, so a rebuild recompiles only the partitions an edit can
affect. To keep unchanged partitions identical across edits, each
partition numbers its metadata in first-use order, lists only the debug
compile units it uses, and orders declarations by name, and interned
string constants are named by their content rather than first-use order.

Partition counts are powers of two that depend only on the module size,
up to 64, so output stays the same on every host and functions keep
their partition across ordinary edits. LLVM text that arrives in parts is
written in large batches and handed to the helper in place instead of
being written a second time.
2026-10-09 19:51:47 -05:00
Malte Ubl 37d19e2a42 Compile ELF worker runtime units with the initial-exec TLS model
Worker runtime variants link only into executables, whose own TLS block
holds every runtime thread-local, but they were compiled with the
default general-dynamic model. The linker relaxes each __tls_get_addr
call to a %fs-relative address, yet the compiler had already treated it
as a call: hot runtime leaves saved and restored extra registers around
every thread-local access (scr_mg_visit pushed five registers instead
of one). With -ftls-model=initial-exec each access is one %fs-relative
operand. Darwin, Windows and wasm variants are unchanged.

tsc-ts self-check, Linux sandbox, Worker build vs non-worker build:
1.04x -> 1.03x (9 interleaved runs).
2026-10-09 17:43:51 -07:00
Malte Ubl c3c59ccf1c Keep the collector's per-thread state in one thread-local block
The collector's thread-locals in worker executables (disposal queue,
candidate buffers, pass vectors, generation limit and scheduling state)
were twenty separate variables. scr_rc_destroy touched four of them, and
the release and walk paths several more; on Darwin each costs a
_tlv_get_addr call per function. Worker builds now keep them in one
ScrCycThread block (macros keep the sources unchanged); ordinary and
library builds keep separate variables.

tsc-ts self-check, macOS, Worker build vs non-worker build: 1.10x ->
1.08x.
2026-10-09 17:43:51 -07:00
Malte Ubl f6ca589404 Keep the allocation paths' per-thread state in one thread-local block
In worker executables the small-object allocator state, the collector's
live and old-freed counts and the weak dispose hook were four separate
thread-locals, and every allocation or free touched them all. On
Darwin each distinct thread-local costs a _tlv_get_addr call per
function, so a runtime free made four such calls.

They now share one ScrThreadHot block (macros keep the runtime sources
unchanged; ordinary and library builds keep separate globals). The
emitted inline paths address the fields at fixed offsets from a single
llvm.threadlocal.address, which LLVM shares across a function.

tsc-ts self-check, macOS, Worker build vs non-worker build: 1.12x ->
1.10x.
2026-10-09 17:43:51 -07:00
Malte Ubl b4a961b36d Read and write thread-owned typed arrays inline in worker programs
Worker programs sent every typed-array element read and write through
scr_bytes_get/scr_bytes_set, which take the shared-memory lock guard
even for buffers no other thread can see (4% of a Worker build of the
tsc-ts self-check). A view's `shared` storage is fixed before script
code can see it, so the emitted access now tests it: SharedArrayBuffer
views keep the locked runtime call, every other view takes the ordinary
inline path of non-worker programs.

The new corpus program reads and writes own and shared views through
the same functions (clamping, wrapping, float narrowing, out-of-range
and negative indices) on both threads.
2026-10-09 17:43:51 -07:00
Malte Ubl d56a31aa9b Treat throw-only and storage-only operations as bounded in workers
A worker function polls for termination on entry (and so becomes
may-throw, with checks after every call to it) unless all of its
operations are on the bounded list. The list missed operations that run
no script code and cannot block: compiler-resolved Node errors
(error.nodeThrow, in 3,986 tsc-ts functions), runtime fences, array
truncation, typed-array stores and reads, Map and Set construction over
literal entries or static containers, typed-array construction from
static sources, caught-value tests, module TDZ checks, and string and
number library calls whose arguments are primitives (a dynamic argument
could reach a user conversion).

tsc-ts: entry polls 6,903 -> 4,447 of 11,464 functions; the remainder
are almost all members of call cycles.
2026-10-09 17:43:50 -07:00
Malte Ubl 03ae137be2 Test a process-wide exception alert in worker executables' polls
Every emitted pending check of a worker executable loaded the
thread-local active exception cell and the thread-local stop signal.
On Darwin each thread-local access is a call (_tlv_get_addr), and LLVM
does not share them across a function's checks: compareTypes made 61
such calls, and the single-threaded tsc-ts self-check spent 19% of its
time in them (a Worker build ran 1.67x slower than the same program
without Worker on macOS).

The checks now load one ordinary global, scr_exc_alert, which is
nonzero while any script thread may have an exception pending in its
active cell or must stop. Zero proves nothing is pending anywhere, so
the check falls through; otherwise the out-of-line scr_exc_pending
answers for the calling thread, observes termination and settles the
thread's share. Each thread holds at most one share: raised by throws,
rethrows, the termination sentinel, fiber switches and payload moves
into the active cell, dropped by take, clear, moves out of the active
cell and the slow path. A share is only changed by its own thread, so a
thread's raise is visible to all of its later checks. worker.terminate()
holds one more reference until the worker's script has ended, so the
worker reaches its slow path and stops. An exception in flight in one
thread costs the others slow-path calls only until it is caught.

The stop signal is runtime-private again; the active cell stays
exported for ordinary executables' inline checks.

tsc-ts self-check, macOS, Worker build vs non-worker build: 1.67x ->
1.18x (check phase 1.65x -> 1.09x).
2026-10-09 17:43:50 -07:00
Malte Ubl 188a642609 Give ELF worker executables the executable TLS models
Every thread-local of a worker executable lives in the executable's own
TLS block: the program's internal ones and the runtime's external ones
alike. The emitter declared them with LLVM's default general-dynamic
model, which the linker relaxes to `mov %fs:0` plus an add at each
access. On ELF targets the program's thread-locals are now local-exec
and the runtime's initial-exec, so each access is one %fs-relative
address that LLVM folds into the load or store. Darwin (one model),
Windows, WASI and shared-library thread instances are unchanged.

Linux tsc-ts in a Worker build: 5.01 s -> 4.96 s (7 interleaved runs,
non-worker build 4.55 s); the 16.5k relaxed sequences became direct
%fs accesses.
2026-10-09 17:43:50 -07:00
Malte Ubl 3b17338ab2 Give every worker script thread its own small-object allocator
Worker executables compiled the small-object allocator out and sent
every allocation and free through the system allocator, because
several script threads would race on one set of free lists. Each script
thread now owns an arena: scr_sa is thread-local in worker builds and
describes one of 32 equal slices of a single process-wide reservation,
so the owning arena of any block is address arithmetic. A thread claims
an arena on its first allocation and parks it, free lists and bump
pointers intact, when it exits; the next thread continues from there.
Memory is never unmapped, so blocks a worker leaves behind (published
immortal objects) stay valid after it exits. A block freed by another
thread goes to its owner's lock-free remote list, drained on the owner's
slow path; realloc moves foreign blocks into the caller's arena.

The emitted constructor and release fast paths are inlined for worker
programs too, addressing the thread-local allocator state, live count
and dispose hook through llvm.threadlocal.address.

The allocator unit test gains a worker build that hands blocks across
threads and reclaims a parked arena, also under ThreadSanitizer.
2026-10-09 17:43:49 -07:00
Malte Ubl 7ecc428bc8 Specialize int32 class fields in worker executables
Worker builds skipped the whole-program int32 field analysis, as library
builds do, so a Worker-using tsc-ts kept every flags field a double and
isSimpleTypeRelatedTo tripled in size. Unlike library instances, worker
instances never cross a native boundary: they reach other threads only
through postMessage and workerData, whose dynamic views read and commit
fields like every other dynamic reader the analysis already handles.

worker.new and the ArrayBuffer/SharedArrayBuffer constructors take
dynamic arguments but never store into a live class capsule (none of
their runtime paths reaches scr_dyn_typed_ref_commit), so they join the
read-only helpers instead of disqualifying every field a dynamic store
could name.
2026-10-09 17:43:49 -07:00
Malte Ubl 6faa762e87 Keep throwing checked projections in discarded statements
Borrowing checked-cast aliases taught canBorrowCallArgument to accept a
checked projection: the always-throwing `error.nodeThrow` call, a
ternary whose failing arm throws, and the narrow of its successful arm.
A borrowed use still evaluates them, but emitDiscarded also used that
predicate to skip a discarded sequence's result as a pure read, so a
statement like `String.prototype.trim.call(undefined)` lost its
TypeError (corpus 3040, 3042, 3043 and 2113 on CI).

Give the discard path its own predicate that only skips effect-free
reads, as before.
2026-10-09 17:40:49 -07:00
Chris Tate ec897a3f13 Give the sanitized self-hosting compiles room on loaded runners (#808) 2026-10-09 18:34:44 -05:00
Chris Tate 2606fadf31 Give the sanitized self-hosting compiles room on loaded runners
Two self-hosting tests compile large parts of the compiler under the
sanitizer, and on loaded macOS runners they regularly come close to their
limits. Backend analysis ranged from 165 s to over 300 s against a 300 s
limit with no code change behind the spread (134 s locally, base and
current alike), and the native-frontend compile ranged from 643 s to over
900 s against a 900 s limit. Backend analysis now allows 600 s, and the
native-frontend compile 1500 s within an 1800 s test limit.
2026-10-09 18:32:36 -05:00
Chris Tate e455490158 Merge pull request #807 from vercel-labs/ctate/give-async-bodies-2f4c4f
Give async bodies and generators 512 KiB stacks
2026-10-09 17:59:40 -05:00
Chris Tate 6d8eeb27be Keep the engine stack budget at half the fiber stack
Embedded engine calls entered from a fiber get a stack budget documented
as half the fiber stack, leaving the other half as margin. The budget
stayed at 128 KiB when fibers grew to 512 KiB, so it is now 256 KiB,
which restores that ratio.
2026-10-09 17:54:40 -05:00
Chris Tate bee2981308 Keep the stack guard out of localized library archives
The stack guard was a tentative definition, which some targets emit as a
common symbol. Localized library archives deliberately leave common
symbols shared, so on macOS the archive defined scr_stack_guard alongside
its declared exports. An explicit initializer makes it an ordinary
definition that localization hides like the rest of the runtime.
2026-10-09 17:40:54 -05:00
Chris Tate d1e7f9088c Give async bodies and generators 512 KiB stacks
Async functions and generators run on their own fiber stacks. The new
stack-overflow guard keeps a quarter of each stack as a reserve for
unwinding and error reporting, which cut the depth they could recurse to
from 256 KiB to 192 KiB of frames. Moderately deep recursion inside an
async body or generator then threw RangeError where Node runs it, for
example 3000 frames of an unoptimized build on arm64 macOS.

Fibers now get 512 KiB, which leaves 384 KiB after the reserve. Pages are
still committed lazily, so idle and shallow fibers cost address space, not
memory. The corpus pins recursion inside async functions and generators
next to deeper recursion on the main stack.
2026-10-09 17:33:59 -05:00
Malte Ubl d54f705540 Keep the borrowed convention for rebound parameters with an owner slot
A reference parameter that the body rebinds made the whole parameter
owned: every caller retained the argument and the callee released it on
exit, even when the rebinding never ran. tsc-ts's isTypeRelatedTo starts
with `if (isFreshLiteralType(source)) source = ...regularType` and was the
largest single source of Type releases.

When every write is a plain statement assignment, the parameter now keeps
borrowing the caller's argument; each assigned value moves into a
separate owner slot that the next rebinding releases and replaces and
that the function scope releases on every exit. Reads use the parameter
slot, so whole-value uses still retain. Expression-position writes,
captures, loop and catch bindings, suspending bodies and string
parameters (whose in-place append needs a uniquely owned binding) keep
the owned convention. Unchanged-parameter users (stable call arguments,
aliases, frame-long element borrows) still exclude these parameters.

The call-lifetimes package test now expects `snapshot` to keep its
borrowing body: the alias saved before the rebinding still owns its value.
2026-10-09 15:03:24 -07:00
Malte Ubl 5133738a83 Return walks of borrowed parameters without a reference
An edge-preserving function whose every return is a walk of its borrowed
parameters (accessors such as `node.children`, `t.types`, parent walks,
or a call to another such function) now returns its result at +0 from the
borrowing body. The result stays reachable from the arguments, so direct
callers that consume it as a receiver, a walk, or a borrowable argument
use it in place; every other direct caller tests the pending exception
and then retains it, before the argument snapshots that keep it reachable
are released. The owned adapter retains a present result (a throwing body
returns a null dummy, which string and array retains do not accept) before
releasing its parameters. Candidates borrow every reference parameter,
have no try statements and return a plain reference (instances, records,
arrays, strings, nullable-pointer unions).

Implementations of a vtable slot that borrows parameters are excluded:
virtual dispatch reaches their borrowing body (or its virtual adapter)
directly and its callers own the result. Without this, corpus 4545's
virtual accessors hit a heap-use-after-free in the sanitized lane.

The set grows to a fixpoint, since a call to a borrowed-return function
with walk arguments is itself a walk; walk facts are then recomputed with
the final set, admitting only parameters whose convention borrows.
Walk locals now cover every plain reference type, not only instances.

In tsc-ts, compareNodes -> sourceFileOf -> getSourceFileOfNode and
maybeTypeOfKind's loop over typesOf(t) no longer touch reference counts.
2026-10-09 15:03:23 -07:00
Malte Ubl 6ff0b7c2df Load the active exception cell once per synchronous body
Every inline pending-exception check in an executable loaded the
runtime's active-cell pointer and then its kind: two dependent loads
after each call that may throw, because LLVM must assume any call can
change the pointer. Fiber switches restore the active cell before control
returns to a synchronous frame (stack switches, eager spawns and
generator resumes all swap back to the caller's cell), so the pointer is
one value for the whole invocation. Synchronous IR function bodies now
load it once in the entry block and each check reads only the kind.

The flag lives on the body's BlockBuilder, so adapters and runtime
helpers keep the per-check load. Async functions, generators and worker
executables (whose checks also fold in the context stop signal) keep the
existing form; libraries still test through scr_exc_pending.
2026-10-09 15:03:13 -07:00
Malte Ubl 294b443b32 Keep the borrowed convention for parameters rebound to walks
A parameter that is rebound only by statement assignments of walk
projections (`source = (source as LiteralType).regularType`) used to
become an owned parameter: every caller retained the argument, the callee
released it on exit, and each rebinding retained the new value and
released the old. In an edge-preserving function its incoming value is the
caller's borrow and every later value is reachable from the parameters, so
it can stay borrowed like any walk local.

The walk analysis now runs once per module before call lifetimes, which
merge these parameters into the borrowed set. Unchanged-parameter users
(stable call arguments and aliases, frame-long array element borrows)
still only see parameters that are never rebound.

tsc-ts's isTypeRelatedTo, which normalizes fresh literal types this way
on entry, no longer touches reference counts for its type arguments.
2026-10-09 15:03:13 -07:00
Malte Ubl 713f947024 Borrow locals that walk pointers in edge-preserving functions
A local whose every definition projects an unwritten parameter or another
such local through plain field reads, casts, nullable narrows/wraps and
checked ternaries (the parent walk `p = p.parent`) now holds a borrowed
pointer: no retain at the definition, no release when it is rebound or
leaves scope. This is sound only in functions whose whole body preserves
heap edges (ReferenceEffects.functions): nothing they run removes a field,
element or global reference, so every object reachable from a parameter
at entry stays alive until the function returns. Whole-value uses still
retain their own copies.

The unreachable-completion fence after a `for (;;)` loop throws a fresh
constant error and preserves edges like `throw`; it used to make every
such function look mutating.

tsc-ts's getSourceFileOfNode loop did two increments and two full
decrements (with cycle-candidate checks) per parent step; it now only
loads and tests the parent pointer.
2026-10-09 15:03:13 -07:00
Malte Ubl 12f1966f06 Borrow constant aliases created by checked casts
`const x = a as LiteralType` lowers to a ternary whose failing arm always
throws and whose successful arm narrows and casts the unchanged source.
The alias projects the same stable owner, so it can borrow like a plain
copy of the binding instead of retaining at the declaration and releasing
at scope exit. Call arguments of the same shape borrow too.

tsc-ts's sameLiteralValue (two casts per call, on the relation hot path)
loses all of its reference counting.
2026-10-09 15:03:12 -07:00
Malte Ubl a07b97f798 Borrow nullable wraps, element reads and preserving virtual calls
A wrap into a nullable-pointer union is the payload pointer itself (or an
immortal unit constant), so it can borrow exactly when its payload can,
as a receiver and as a call argument. tsc-ts widens many `Type` and
`Node` parameters to `T | undefined` (they may receive unchecked array
reads), so `compareTypes(c, t1, t2)` wrapped `t1` for every identity test
and call and retained and released it each time.

Required array element reads may borrow as receivers while the borrowed
array owns the element and the index preserves heap edges (array
operands already borrow; this borrows the element itself).

Reference-effect analysis now sees through switch statements, class
references, primitive formatting and virtual calls: a virtual call
preserves edges when every implementation reachable from its static class
(the inherited one plus every override below it) does. Runtime classes
keep dispatch opaque.
2026-10-09 15:03:12 -07:00
Chris Tate c0208fb6d9 Throw a catchable RangeError on deep recursion (#805) 2026-10-09 17:01:46 -05:00
Chris Tate 3bcf761f42 Throw a catchable RangeError on deep recursion
Native executables used to terminate with a signal when recursion
exhausted the stack. Functions that can take part in unbounded recursion
now compare the stack pointer against a per-stack guard on entry and
throw Node's `RangeError: Maximum call stack size exceeded` through the
ordinary exception path, so catch handlers, finally blocks, worker error
events and the uncaught-error exit code behave as they do under Node.

- The checked set is every function on a cycle of the call graph, where
  operations that can reenter compiled code indirectly (indirect calls,
  callbacks, dynamic and library operations) reach closure targets and
  class members. Non-recursive functions, including leaf callbacks, stay
  check-free, and the may-throw analysis accounts for the new throw.
- The runtime installs the guard per context (main thread from the stack
  rlimit, workers from their thread attributes) and per async fiber, with
  a reserve for the throw, unwinding and error reporting. Worker threads
  now get an explicit 8 MiB stack.
- Scalar call results pass through the unwind path as its dummy value, so
  calls in tail position keep tail-call and recursion elimination.
2026-10-09 16:55:35 -05:00
Chris Tate ecc664cbf9 Keep shared read and format helpers out of source line tables (#804) 2026-10-09 14:54:04 -05:00
Chris Tate 860f9339ec Keep shared read and format helpers out of source line tables
Indexed reads that can miss lower through shared helpers (`%arr.idxOr`,
`%bytes.idxOr`), and printing a value that can be undefined goes through a
shared formatter (`%util.fmtv`). Each helper took the location of the read
or print that first created it, so its body carried that line's debug
locations. A breakpoint on such a line then resolved to every helper as
well, and the debugger stopped inside helper code where the user's
variables are not in scope.

The helpers are compiler code, so they now get a location without a
source position (helperLoc). They emit no debug info, while each call
still carries the user's location, so a breakpoint on the line stops only
at the read or print itself.
2026-10-09 14:50:28 -05:00
Chris Tate 1254caaba9 Read unassigned class fields as undefined during construction (#803) 2026-10-09 14:34:32 -05:00
Chris Tate dd6b8d4ad9 Read unassigned class fields as undefined during construction
Class-typed fields that construction may read before assigning them now
observe undefined instead of reading an empty slot. A small analysis walks
field initializers and the constructor in evaluation order and stops at the
first point that can hand the instance to other code (a method call, an
argument, a callback, a base constructor that does); private helpers that
only assign fields are followed. Fields not assigned by then take the
deferred-init representation:

- the slot starts empty and reads check it inline, so assigned reads stay
  borrowed and only an empty slot reaches the throw;
- equality, `typeof`, truthiness, `== null`, optional chaining, `as`
  casts, undefined-admitting destinations and console output observe the
  undefined itself;
- member reads, method calls, updates and writes through an unassigned
  field throw Node's TypeErrors in Node's evaluation order.

Fields assigned before any exposure keep their plain representation.
Definite-assignment-asserted fields assigned through such private helpers
no longer need the deferred form either.

Corpus 4471 pins inheritance, callbacks, initializer order, private helpers
and member updates; unit tests cover the analysis and the emitted checks.
2026-10-09 14:27:41 -05:00
Chris Tate 28935d6c89 Accept borrowing vtable entries in inheritance codegen tests (#802) 2026-10-09 13:43:38 -05:00
Chris TateandClaude Opus 5.5 6ffcdf96ff Accept borrowing vtable entries in inheritance codegen tests
Virtual slots whose implementations all borrow a parameter now store the
borrowing body (`@sc_bf_...`), or a virtual adapter, instead of the owned
entry (`@sc_f_...`). Three inheritance codegen tests still required the
owned entry. They now accept either entry for the same implementation,
so they keep checking which method fills each slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:40:02 -05:00
Chris Tate 219738931b Keep the emitter inside the self-hosted subset (#801) 2026-10-09 13:36:24 -05:00
Chris TateandClaude Opus 5.5 ff7d687cec Keep the emitter inside the self-hosted subset
The compiler stopped compiling itself. Two recent emitter additions use
forms the self-hosted subset refuses:

- LlEmitter tracked immortal temps in a WeakMap field keyed by block
  builder. That field has no lowering, so LlEmitter itself was not
  lowered. The set now lives on BlockBuilder, which owns the temp names.
- mentionsLocal probed IR nodes with `"localId" in node` and
  `"captures" in node`. It now narrows by node kind, covering every
  expression and statement kind that names a local or captures one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:32:55 -05:00
Malte Ubl 946698d293 Merge pull request #799 from vercel-labs/perf/tscts/09-rc-borrows
tsc-ts -5%: borrow field-store receivers, virtual parameters and array projections
2026-10-09 12:20:52 -05:00
Malte Ubl 60e2a3e563 Merge pull request #795 from vercel-labs/perf/tscts/08-array-temporaries
tsc-ts -8%: drop temporary arrays and array reference-count traffic
2026-10-09 12:20:24 -05:00
Malte Ubl 98654192e9 Merge pull request #794 from vercel-labs/perf/tscts/07-ascii-strings
tsc-ts -6%, runtime suite -6%: cache proven-ASCII strings and inline length/charCodeAt
2026-10-09 12:15:08 -05:00