The method-replacement census marks a method name as an observed slot
when some write might replace it, and calls of observed methods go
through a checked property lookup whose result converts back to the
declared return type. Two kinds of data writes over-approximated that:
- A computed write into a string-keyed dictionary marked every method in
the program unless the dictionary's values were primitives. Class
instances have no implicit index signature, so such a dictionary is a
class view only when the class declares the index signature, and then
every method must be assignable to its value type. Any value type no
function can inhabit now qualifies: primitives, or non-callable object
types that require a property functions lack, or weak types sharing no
property with functions.
- A named write like `this.literal = token` marked `literal` for every
class. The write can only replace a method when the written property
can hold a function, so property writes are now judged by the written
property's type (prefetched in one batch).
Compiling vercel-labs/tsc-ts failed after the canary promotion: one
`changedProjects[id] = {...}` write marked every method, OptionNameMap.get
and the JSON parser's literal() calls returned unknown, and their Map-typed
results could not convert back (SC1100/SC1101, 61 errors).
Also skip abstract methods when materializing observed prototype slots.
An abstract declaration emits no prototype member, so lookup continues
to the ancestors; building its value instead threw an uncaught
PoisonError out of the reachability fixed point (the tsc-ts crash).
Tests: corpus 3171 (fails with SC1100/SC1101 before this change) matches
Node in the plain and sanitized lanes; the 112 corpus programs matching
abstract|prototype|method|callback pass; class-dynamic-dispatch unit
tests pass. tsc-ts now builds and type-checks itself with output
identical to tsgo 7.0.2.
The TypeScript 7 parity job reported tests/corpus/3170-number-conversion-
fast-paths.ts as missing a baseline. Recorded with
SCRIPTC_UPDATE_BASELINES=1 pnpm exec vitest run
packages/compiler/test/ts7/order-parity.test.ts: the re-recorded file
added only this program (and the later stack's console programs, which
belong to #764), with no existing entry changed. The entry is inserted
at its sorted position in the stack's baseline block so it does not
overlap the #764 entries when the stack is rebased.
node scripts/test-ts7.mjs --baselines-only passes.
The bootstrap-seed build rejected `host.rcHelpers ?? []` in shapes.ts
(SC2011: `Set<string> | never[]` has no static representation) and the
spread of that union into a string[] literal (SC1090). A small helper copies
the set's keys into a plain array (empty when inline RC is off), which both
callers iterate; emitInlineRcHelpers still sorts its copy. Behavior is
unchanged.
The default build's largest size contributor on the stack was H23's
-falign-functions=64 on every x86-64 runtime unit (2-11 KB per program;
results/h45-size-budget). Release and library flavors now keep the
compiler's default function alignment; the opt-in speed flavor keeps the
blanket 64-byte alignment. Vendored archives stay 64-byte aligned
(lre_exec keeps its pinned placement) except libunicode.c, whose small
table helpers were most of the regex programs' padding. The runtime-first
link order is unchanged.
Docs and usage text describe speed without the reverted runtime profile.
The speed posture's inline scr_union_release fast path predates the
runtime change that buffers a surviving union box as a cycle candidate
only when its arm_trace can reach a cycle. It still enqueued every
surviving box: sound (an untraced box visits nothing and the runtime death
path reads the header), but it gave back that change's collector savings
under --optimization=speed. The fast path now loads arm_trace and returns
when it is NULL, exactly like scr_union_release; scr_runtime.h pins the
offset.
Re-lands perf/h02-runtime-lto (6d373317, b62448f3) behind the speed
posture.
- Runtime packs gain an executable `speed` flavor whenever the host LLVM
helper can run. Its static units are emitted through the helper's
`runtime-unit` command (promoted unit-local symbols, object and import
bitcode from one module); its SCR_DYNAMIC variants reference the release
objects. Release, dev, and library flavors and the vendor archives are
built exactly as before (the rebuilt macOS pack's flavors and archives
are identical to the baseline's).
- Speed program builds load the speed flavor's bitcode and pass it to the
helper (`--import-bitcode`); release builds never do.
loadRuntimeBitcode only reads a real speed flavor, never release.
- The helper drops the emitter's inert sanitize_address attributes only
when it imports, so release modules reach the pipeline unchanged.
- Tests: runtime-pack.test.ts covers speed-variant bitcode selection,
verification, and that release-flavor bitcode is never imported;
native-codegen.test.ts covers helper arguments and cache keys.
Release executables stay byte-identical to the stack tip with the rebuilt
helper (14/14 workloads, macOS).
Re-lands perf/h30-inline-rc (00051ca0, 40398773, 07972360) behind the
speed posture. With inline RC on, every mirrored runtime RC family
retains inline and releases inline down to the rc == 1 / new cycle
candidate slow path (shapes.ts INLINE_RC_FAMILIES). Release emission is
unchanged: ShapeHost.rcHelpers is null unless LlvmTargetOptions.inlineRc
is set, and every retain/release (including capture boxes) stays a
runtime call; the %ScrMapRc type is only emitted with inline RC.
- index.ts and native/prepare.ts pass inlineRc for speed executables,
speed object/asm emission, and speed library profiles.
- scr_runtime.h keeps H30's static asserts for the mirrored header and
trace-slot offsets (no code change; the rebuilt macOS pack's manifest is
identical to the baseline's).
- Tests: shapes.test.ts pins release symbols and H30's speed fast paths
and header offsets; emitter.test.ts pins that release IR is unchanged
and speed routes releases through the helpers. The differential harness
gains `// @optimization: speed` (and a SCRIPTC_TEST_OPTIMIZATION=speed
lane); corpus 4032 pins every mirrored family under speed.
Release executables stay byte-identical to the stack tip (14/14
workloads, macOS).
Speed is a third native optimization posture next to release (default,
-O2) and dev (-O0). It is optimized exactly like release at the C/LLVM
level and is the switch for optimizations that trade executable size and
build time for run-time speed, so the default release build stays inside
the binary-size budget.
- backend/optimization.ts: the NativeOptimization type, its -O class, and
cache-key helpers. Release keeps its historical key shapes (absent).
- CLI: build/run/cache warm accept speed; usage text names the trade.
Bootstrap's routed executable cache mirrors the posture.
- Early executable cache keys and native-feature validation include speed,
so release, dev, and speed entries never satisfy one another.
- Runtime packs: an optional executable `speed` flavor (-O2, only beside
release). Speed links it when present and the release objects otherwise;
library modes link their release flavor. The sanitizer lane follows the
same flavor key.
- Library profiles accept "speed".
- WASI release-only linker flags apply to speed too.
- bench-runtime: --baseline-optimization/--candidate-optimization and a
per-contender cold_build_ms.
Release executables are byte-identical to the stack tip for all 14
benchmark workloads (macOS arm64, sha256 of executables built from both
trees).
tests/harness/coverage.test.ts requires every corpus program to compile
100% statically. timers-clear-many.ts cleared `handles[i]!`, and the
non-null assertion does not narrow an indexed element for the static
lowering, so the three clearTimeout calls reported SC2020 ("clearTimeout of
'number | undefined' handles"). Read the element into a local and test it
against undefined instead. Node output is unchanged; scriptc coverage now
reports 46/46 statements static and the native binary matches Node.
Upstream native workers (095b6193) run one runtime instance per thread and
make the timer heap thread-local (SCR_TL). The timer slot index added for
O(1) handle lookups indexes that heap, so it becomes thread-local too;
scr_timers_teardown already frees it during worker context shutdown.
The RegExp constructor cache stores retained templates that live instances
borrow and that are never evicted. Its table is thread-local, and worker
executables leave it empty (constructions compile privately as before),
because a worker context has no teardown point after every borrowing
instance. Ordinary executables are unchanged.
stderr is unbuffered, so the piecewise stdio calls issued one write(2)
per argument, separator and newline (two syscalls per simple line), and
the line was not atomic against other writers sharing the descriptor.
Render the line first and emit it with a single fwrite, as Node writes
one chunk.
clearTimeout/clearInterval, refresh, ref/unref and hasRef scanned the
whole timer heap for the handle, so arming and clearing many timers was
quadratic (1e5 cancels: 1.0 s vs Node 46 ms). An id-to-slot hash kept
exact by every heap write makes the lookup O(1); heap order is unchanged.
exec/test rescanned the whole subject for ASCII (and re-converted a
non-ASCII subject to UTF-16) on every call, so lastIndex-driven exec
loops and sticky tokenizers were quadratic (1e5 matches: 7.9 s vs Node
43 ms). A single-entry cache keeps the last long subject's decoding; it
retains the subject so the address cannot be recycled and the string is
never appended in place. Only exec/test use it (no user code runs while
their input is live).
new RegExp recompiled its pattern on every construction (22x Node). A
bounded never-evicting cache keyed by (flags, source) shares bytecode
through the literal-template mechanism; invalid patterns still throw on
every construction.
`i in arr` with a non-literal number key boxed the typed array into the
checked-dynamic tree and materialized every element per evaluation, so
`for (...) if (i in arr)` loops were quadratic (1e4 elements: 5.3 s vs
Node 0.7 ms). A number key over an array receiver now lowers to arrayHas
(scr_arr_has), the query the literal fold and Object.hasOwn already use;
the key is stabilized first to keep JS evaluation order.
`k in dict` with a runtime string key over an index-signature record
snapshotted and scanned every overflow key per probe (1e4 keys: 436 ms,
1e5: timeout). The helper now ends in recordOvfHas (scr_map_has_str).
Multi-megabyte end(data) and chunked write() bodies under a slow raw
reader force partial vectored sends; the driver checks framing, length,
and digest of every response against Node.
- scr_net: scr_net_sock_writev_native sends ordered slices with one
sendmsg when the bytes would go out immediately (plain socket, nothing
buffered); every other state takes the per-slice write_raw path, so
buffering, transports, and teardown are unchanged.
- scr_http: end(data) before the head, write() before the head, chunk
framing, and the final chunk now leave as one write (head + body +
framing), as Node does. A typical response was a head segment plus a
body segment; it is now one segment and one syscall.
- scr_net: stop reading after a short read (libuv UV_HANDLE_READ_PARTIAL);
the level-triggered poller reports later bytes or EOF. Removes the
EAGAIN read per request (0.7 per request on http-api). TLS transports
keep draining.
- scr_http: Date header formatted once per second; head built in one
pass without snprintf; request headers parsed with geometric array
growth and one shared string when the name is already lowercase.
The bytes on the wire are unchanged (the runner digests every response
against Node).
The bootstrap-seed build rejected the compound `value ^= value >> 30n` and
`value ^= value >> 27n` on bigint in string-key-hash.ts (SC1043), while the
plain `^` form on the next line compiles. Spell both as `value = value ^
(...)`. The hash is unchanged.
On 64-bit little-endian targets ScrStr's capacity word is split into a
32-bit cap and a 32-bit cached Map key hash (header stays 24 bytes).
Heap strings fill the hash on first lookup; every in-place mutation path
(unique concat, split scratch reuse, regrow, spare reuse) clears it.
Immortal strings are never written: compiler literals and the runtime's
one-character/empty statics carry the hash precomputed (string-key-hash.ts
mirrors scr_key_hash; both sides pin the same vectors).
scr_key_hash seeds by length multiplicatively: XORing the length into the
low bits cancelled against the last byte ("ab" and "abc" collided).
Map string lookups inline the probe into each getter and reject linear
entries by hash; set specializes its probe/overwrite by lookup kind with a
shared append path.
Tests: runtime map.test.ts (pinned vectors, statics, in-place append
invalidation), string-key-hash.test.ts, corpus 3141-map-string-key-hash
(plain + sanitized; mutation-checked against both invalidation paths).
The bootstrap-seed build rejected `"receiver" in expr` / `"args" in expr`
on the IR expression union in constant-callbacks.ts (SC1090: `in` on an
object union). A switch over the six data-intrinsic kinds now returns their
operands (receiver first; none for mapNew/setNew, as before) and replaces
the DATA_INTRINSICS set. Behavior is unchanged.
The full emitter inlines the small-object allocator on 64-bit host targets,
so the self-hosting layout fixture's host enables inlineAlloc for 64-bit
requests (as emitLlvmModule does), and the recursive-records case no longer
asserts the width-specific free path (64-bit pushes onto the allocator's
free list; 32-bit calls scr_cyc_free).
(Split from the stack's later "Update self-hosting emission cases for
acyclic proofs and inline allocation"; the acyclic-proof half moves to the
construction-only acyclic change.)
Upstream native workers (095b6193) compile the runtime with -DSCR_WORKERS,
which makes scr_cyc_live and scr_weak_dispose_hook thread-local, and the
small-object allocator is compiled out there. The inline allocation paths
address those as ordinary globals, so worker programs keep the out-of-line
runtime calls, exactly like thread-instanced libraries.
A worker-threads test pins that worker LLVM references neither the
allocator state nor the thread-local counters, and that worker programs
keep the fiber path for async bodies (the entry 07 adaptation).
Emitted record and class constructors called scr_rt_calloc/scr_cyc_alloc
with a constant size, so the size class, the zeroing length and the
collector header setup were recomputed per object behind a call (plus an
empty scr_obj_alloc_note call and a variable-length memset). llvm/alloc.ts
now emits the fast paths inline: pop the class free list (clearing only
the bytes the helper does not store itself) or bump, with the old calls as
the cold slow path. Releases inline the weak-dispose hook check, the live
count, and the free-list push; blocks outside the reservation still go to
scr_rt_free.
The emitted IR does not know which runtime it links against (the sanitized
lane only differs at link time), so the runtime now defines scr_sa in
every build. Where the allocator is compiled out (ASan, SCR_RC_AUDIT,
wasm32, Windows, 32-bit, thread-instanced libraries) it stays all-zero and
the inline paths always take the slow path, which keeps the RC-audit
object notes. ScrSaState's layout is pinned with static asserts. Inline
emission is additionally gated off for 32-bit, WASI, Windows triples and
thread-instanced libraries.
Runtime callers with hot constant-size allocations (union boxes, arrays,
boxes, closures, dyn nodes) use new static inline twins
scr_cyc_alloc_inline/scr_cyc_free_inline; scr_cyc_live is exported.
The ABI guard now recognizes function-pointer extern data
(scr_weak_dispose_hook). New corpus program 4410 pins recycled-block
clearing, size classes, objects above the largest class, cycles and weak
observers.
`new Item()` now calls the borrowed constructor body, so no retain transfers the fresh instance into it: the snapshot body retains twice, not three times.
layouts.ts and classes.ts imported NullableRefFields type-only, so the
self-hosted layout stage never saw the class declaration and rejected
`NullableRefFields | undefined`. They now import the class itself, and the
field record names the object variant directly instead of an intersection
type.
The bootstrap-seed build compiles the compiler with scriptc, which rejected
`for (const index of parameters.get(fn.name) ?? [])` in the emitter: the
`Set<number> | never[]` union has no static representation (SC2011). Read
the set once and iterate it only when present. Behavior is unchanged.
A whole-value read of a nullable-pointer field allocated a heap box per
read, so list walks (`p = p.next`) regressed: 0.07 s -> 0.35 s on a 100k
list walked 200 times. Such reads now retain the instance pointer into
the current frame and wrap it in a private stack box with that
independent owner:
- local union storage accepts nullable field reads and copies of another
binding of the same union as sources (the payload is retained; scalar
and unit arms own nothing; retains branch on the tag because not every
retain entry point is NULL-tolerant);
- arguments to projection-only parameters of calls and constructors.
The list walk now takes 0.05 s (baseline 0.07 s). Corpus 4061 adds a copy
that outlives a store to the field and copies across string, number,
-0 and undefined arms.
A class field typed `C | null` or `C | undefined` (one unit arm plus one
emitted class arm) now holds the instance pointer itself: NULL is the unit
arm. The struct slot stays `ptr`, so layouts are unchanged.
- Teardown, trace and gcFree use the slot's storage type (the class arm);
every class retain/release/visit entry point is NULL-tolerant.
- Projections of a field read (tag tests, narrowing, truthiness, unionDisc,
nullish, borrowed equality operands) use a private stack box over the
borrowed pointer; whole-value reads build an owned heap box (or the
immortal unit instance); generic borrowed receivers get that owned box.
- Stores take the payload of a wrap directly, or the retained payload of
a borrowed union; a store of a union binding is a projection use, so
constructor parameters stored this way are projection-only.
- `new` calls the borrowed constructor body when the lifetime analysis
allows it, so projection-only arguments become private stack boxes.
- The dyn bridges (materialize, commit, symbol fields) convert between
the pointer and a temporary union.
alloc-trees no longer allocates a union box per tree edge. Corpus 4061
pins tests, narrowing, nullish, optional chaining, equality with
mutation, escapes, reassignment, subclass instances, unionDisc, `any`
reads/writes, JSON, console output, cycles, and a 300k chain teardown.
macOS arm64 (H23 runner, --layouts=4 --runs=16, vs perf/harness):
alloc-trees -45.5%, records-sort -72.8%, numeric-kernels -29.4%,
ast-interp -21.3%, word-graph -12.4%; template-render +1.9% and
functional-pipeline +0.8% (both within 2%); geomean -17.6%.
Accepting nullish and unionEq as projection uses also proves parameters
projection-only, so callers pass private stack boxes to them. The narrowed
nullish shape now always borrows a binding (no code runs between the read
and the payload extraction), and unionEq routes proven parameters through
the borrowed receiver path, so a stack box never reaches union RC entry
points. Found by the Linux corpus (free(): invalid pointer in 1535 and
others); pinned by a stack-unions unit test and corpus 4060.
Union values consumed only by projections no longer allocate heap boxes:
- closure bodies get local lifetime facts (parameters stay unproven), so
for-of bindings and optional reads inside comparators use stack boxes;
- unionEq and the narrowed nullish shape are projection uses; their
wrapped-literal, optional array read, and Map.get operands are emitted
as private stack boxes;
- optional array reads consumed by tests/narrowing/comparisons use a
stack box with the inline dense lookup; f64/bool elements are supported
(slot bits copied, runtime state check kept for holes and sparse slots);
- scalar union payload reads are inline loads so LLVM can scalarize the
stack boxes.
Corpus 4060 pins holes, sparse/noncanonical indices, -0/NaN/false,
reference identity, closure locals, and mutation inside ?? defaults.