Commit Graph
870 Commits
Author SHA1 Message Date
Malte Ubl 88994065c4 Keep supertype-parameter array callbacks static
Since #784, an array whose elements are proven present passes each element
itself to its callback instead of the optional element value. A callback
whose parameter is a structural supertype of the element
(`refs.find((r: { pos: number }) => ...)` over a wider record) has no
adapter from the plain element record, so lowering rejected it with SC2011.
Before #784 the optional element ABI converted it through a union retag.

The optional-read prepass now treats a site as present only when the
callback's element parameter accepts the stored element through a
mechanical conversion. Otherwise the site keeps the optional element visit,
as before #784. Unannotated and generic parameters are unaffected, so every
site #784 made precise stays precise.
2026-10-09 09:29:51 -07:00
Chris Tate 477f97a840 Track optional values precisely through callbacks, captures and pops (#784) 2026-10-09 10:33:07 -05:00
Chris Tate 3e41bcbfd0 Make compile blockers and Node differences actionable (#786) 2026-10-09 10:30:31 -05:00
Chris Tate 96ee89412b Keep exception polls inline in worker programs (#785) 2026-10-09 10:01:27 -05:00
Chris Tate 3a0eda638e Schedule cycle collection by yield and borrow indexed reads (#781) 2026-10-09 07:55:10 -05:00
Chris Tate f5a623b0b2 Observe forwarded optional values and asserted class arms (#783) 2026-10-09 07:49:53 -05:00
Chris Tate 2476844e13 Respect runtime optional values and initialization order in more places (#775) 2026-10-09 00:23:28 -05:00
Chris Tate b26d440906 Read in-bounds string elements without boxing (#776) (#780) 2026-10-08 22:04:05 -05:00
Chris Tate 9131a3498c Release v0.2.7
- Synchronize all published packages and the compiler surface manifest at v0.2.7.
- Record native features, runtime performance improvements, and compatibility fixes since v0.2.6; leave Unreleased empty.
- Verify version and manifest contracts, compatibility output, and cross-target library conformance locally; use CI for full validation.
2026-10-08 21:18:31 -05:00
Chris Tate 8bd17409db Sync runtime and compiler updates into canary
- Merge main's runtime optimizations and CI changes with canary's frontend improvements.
- Preserve adaptive cycle reclamation with inline allocation and deallocation.
- Repair native bootstrap callback narrowing and emitter expectations.
2026-10-08 17:41:20 -05:00
Chris Tate 13633c08b6 Merge pull request #772 from vercel-labs/fix/method-census-data-writes
Fix compiling tsc-ts: keep data writes out of the method replacement census
2026-10-08 16:48:36 -05:00
Malte Ubl 2625ae9c9e Keep data writes out of the class method replacement census
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.
2026-10-08 14:32:33 -07:00
Malte Ubl 3fe49589ee Record preflight/order baselines for corpus 4294-4296
The TypeScript 7 parity job needs baselines for the console corpus
programs this PR adds (4294-console-line-sizes, 4295-console-template-
parts, and 4296-console-glued-long-parts from the overflow fix).
Recorded with SCRIPTC_UPDATE_BASELINES=1 pnpm exec vitest run
packages/compiler/test/ts7/order-parity.test.ts. The re-recorded file
added only these three and #763's 3170, with no existing entry changed.
The 3170 entry is left to #763. These are inserted at their sorted
position, apart from 3170, so the hunks stay independent when the stack
is rebased.

node scripts/test-ts7.mjs --baselines-only now reports only 3170, which
the rebased base branch provides.
2026-10-08 15:51:42 -05:00
Malte Ubl 0cd28f7d45 Pass string-concatenation console arguments as parts
console.log(`${method} ${path} ${status}`) used to build the joined string
first (one string per number, scr_str_concat_parts, then releases), and
the console then copied that string into the line it writes. The LLVM
backend now flattens a strConcat argument with stringParts and passes
each part as a ScrLogArg:

- Later parts carry SCR_ARG_GLUE, so no separating space is added.
- A number conversion passes the raw double as SCR_ARG_NUM (String()
  spelling, so -0 prints "0"; a bare number argument keeps inspect's
  "-0").
- A boolean conversion passes the bool.

The runtime renders the parts directly into the line buffer. Parts are
evaluated in source order through emitStringInputs, which applies the
same borrowing rules as concatenation. Calls that use the new tags go to
new scr_console_log_parts / scr_console_error_parts entry points, so a
program object linked against an older runtime pack fails at link time
instead of misprinting.

Tests: corpus 4295 covers -0/NaN/exponent spellings inside templates
next to bare arguments, empty parts, more than 16 parts, evaluation
order, a later part reassigning an earlier string, union and optional
values, and async code. A new co-located IR unit test checks the
emitted tags and entry points.

Linux sandbox, ns per line (median of 9 launches, 200k lines), prev =
stack tip, cand = this commit (with the previous one). All outputs
byte-identical to Node.

  probe      target  node   prev  cand
  p-str      pipe    4022   1364  1149
  p-str      file    3376    849   744
  p-str      null    2360    330   247
  p-args     pipe    6687   1441  1343
  p-args     file    5135    969   821
  p-args     null    3831    389   292
  p-write    pipe    2866   1244  1168
  p-write    file    2329    839   782
  p-write    null    1622    309   275
  p-err      pipe    4656   3253  1073
  p-err      file    3659   2099   721
  p-err      null    2749    591   219
  log-lines  pipe    5007   1476  1314
  log-lines  file    3949    965   877
  log-lines  null    3145    446   378
2026-10-08 15:51:42 -05:00
Malte Ubl 4c1cc612be Record the preflight/order baseline for corpus 3170
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.
2026-10-08 15:49:53 -05:00
Malte Ubl 66e82da225 Record preflight/order baselines for corpus 4032 2026-10-08 15:46:34 -05:00
Malte Ubl 90d024cd8c Keep inline RC helper enumeration inside the self-hosted subset
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.
2026-10-08 15:46:34 -05:00
Malte Ubl 2076f793f6 Keep 64-byte x86 runtime alignment for --optimization=speed only
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.
2026-10-08 15:46:34 -05:00
Malte Ubl 2d6a239abb Skip buffering untraced union boxes in the inline union release
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.
2026-10-08 15:46:34 -05:00
Malte Ubl 60c30d6ebd Import runtime bitcode into speed programs (H02)
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).
2026-10-08 15:46:34 -05:00
Malte Ubl 213cf287b9 Emit inline retain/release fast paths under --optimization=speed (H30)
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).
2026-10-08 15:46:34 -05:00
Malte Ubl 2372b75ec2 Add an opt-in --optimization=speed posture
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).
2026-10-08 15:46:34 -05:00
Malte Ubl 9cf183e053 Record preflight/order baselines for corpus in-array-runtime-index, regex-exec-long-subject, regexp-constructor-repeat, timers-clear-many 2026-10-08 15:44:26 -05:00
Malte Ubl 93b4994220 Lower runtime-key in over arrays and dictionaries to presence probes
`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).
2026-10-08 15:44:26 -05:00
Chris Tate dc98e40202 Refresh the surface manifest for import-cycle support (#769) 2026-10-08 15:41:53 -05:00
Malte Ubl 4daa362e55 Record preflight/order baselines for corpus 3141 2026-10-08 15:40:53 -05:00
Malte Ubl 30ba03592e Keep the string key hash mixer inside the self-hosted subset
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.
2026-10-08 15:40:53 -05:00
Malte Ubl f7ac13e87f Cache string Map key hashes in the string header
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).
2026-10-08 15:40:53 -05:00
Malte Ubl 520dc20711 Record preflight/order baselines for corpus 4140, 4141, 4142 2026-10-08 15:38:45 -05:00
Malte Ubl ea8f93a99f Keep data-intrinsic operand checks inside the self-hosted subset
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.
2026-10-08 15:38:45 -05:00
Malte Ubl 425386b877 Call array-method callbacks directly and pass plain elements
Array HOF helpers (%arr.map/filter/forEach/flatMap/find*/some/every/
reduce*) called every callback through its closure, bracketed by the
ambient JS receiver stack, with each element boxed as T | undefined.

- Intern a helper per literal callback and admit %arr.* helpers as
  constant-callback consumers: the callback becomes a guarded direct
  call that LLVM inlines into the loop.
- Replace the scalar-only receiver check with an exact module fixpoint
  (AmbientReceiverReaders) over direct calls, constructors and virtual
  dispatch; known callbacks that cannot reach a receiver read skip the
  push/pop.
- For inline arrows over plain element types, lower the callback a
  second time with the element type and split each visit on the array
  state: present values take the fast closure with no union box; holes
  and present undefined keep the original closure and undefined arm.
- Resolve factory-made callbacks (every return yields one function) and
  immutable captured callbacks at their closure creation sites.
- Read and write scalar capture boxes in place instead of calling
  scr_box_get_f64/scr_box_set_*.

Corpus 4140-4142 pin the split visit, receiver brackets on the direct
path, and factory/captured callbacks.

Linux x64 (sandbox, --layouts=4 --runs=16, baseline stack 06 tip):

| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |
| json-records | 192.5 | 193.8 | +0.7% | -2.1% .. +2.4% | neutral | +0.0% | 436.3 | 0.44x |
| regex-logs | 166.4 | 165.5 | -0.5% | -1.8% .. +0.9% | neutral | +0.0% | 243.9 | 0.68x |
| ast-interp | 264.9 | 243.0 | -8.3% | -9.5% .. -6.9% | faster | +0.0% | 232.2 | 1.05x |
| records-sort | 593.2 | 576.4 | -2.8% | -6.6% .. +0.3% | neutral | +3.0% | 210.3 | 2.74x |
| template-render | 69.3 | 68.6 | -1.0% | -3.2% .. +2.6% | neutral | +0.0% | 189.0 | 0.36x |
| functional-pipeline | 215.8 | 111.0 | -48.6% | -49.6% .. -46.3% | faster | +3.8% | 255.8 | 0.43x |
| async-pipeline | 194.8 | 196.8 | +1.0% | -1.1% .. +12.0% | neutral | +0.0% | 85.9 | 2.29x |
| word-graph | 271.8 | 270.1 | -0.6% | -7.0% .. +5.5% | neutral | +3.0% | 517.1 | 0.52x |
| alloc-trees | 314.4 | 316.6 | +0.7% | -0.4% .. +1.9% | neutral | +0.0% | 124.9 | 2.53x |
| numeric-kernels | 46.2 | 45.7 | -1.1% | -2.5% .. +1.5% | neutral | +0.0% | 128.2 | 0.36x |
| validate-errors | 179.7 | 174.5 | -2.9% | -5.4% .. +0.7% | neutral | +0.0% | 711.1 | 0.24x |
| csv-numbers | 343.0 | 338.9 | -1.2% | -7.3% .. +0.7% | neutral | +0.0% | 519.6 | 0.65x |
| log-summary | 166.1 | 168.3 | +1.3% | -4.4% .. +9.1% | neutral | +0.0% | 300.0 | 0.56x |
| inventory-report | 298.1 | 290.1 | -2.7% | -14.6% .. +5.4% | neutral | +1.3% | 421.4 | 0.69x |

Geomean -5.9%; candidate/node 0.715x. macOS arm64 (shared host):
geomean -4.7%, functional-pipeline -44.1%, ast-interp -11.0%,
inventory-report -8.0%; none slower (records-sort -0.5% on a calmer
rerun).
2026-10-08 15:38:45 -05:00
Malte Ubl 88dfc2ebde Record preflight/order baselines for corpus 4410 2026-10-08 15:36:07 -05:00
Malte Ubl 6d1257ba72 Format alloc.ts 2026-10-08 15:36:07 -05:00
Malte Ubl cabfbe1f14 Keep inline allocation out of worker programs
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).
2026-10-08 15:36:07 -05:00
Malte Ubl 439964a693 Inline the small-object allocator into emitted constructors and releases
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.
2026-10-08 15:36:07 -05:00
Malte Ubl efae7de476 Expect borrowed constructor bodies in the call-lifetimes test
`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.
2026-10-08 15:34:37 -05:00
Malte Ubl 01b967f7c8 Keep nullable field metadata inside the self-hosted subset
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.
2026-10-08 15:34:37 -05:00
Malte Ubl 0207eb658c Record preflight/order baselines for corpus 4060, 4061 2026-10-08 15:34:37 -05:00
Malte Ubl 7a5bcb2dab Keep projected-parameter lookup inside the self-hosted subset
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.
2026-10-08 15:34:37 -05:00
Malte Ubl 363a9360fd Keep copied nullable field reads and union binding copies in stack storage
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.
2026-10-08 15:34:37 -05:00
Malte Ubl bdd58b4705 Store nullable class fields as instance pointers and borrow constructor bodies
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%.
2026-10-08 15:34:37 -05:00
Malte Ubl f75c3d4138 Borrow projection-only union parameters in nullish and equality
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.
2026-10-08 15:34:37 -05:00
Malte Ubl 72a9572271 Keep projected optional reads and wrapped literals in stack union boxes
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.
2026-10-08 15:34:37 -05:00
Malte Ubl 0d0a94b86d Record preflight/order baselines for corpus array-sort-runtime-comparator, closure-union-locals 2026-10-08 15:31:04 -05:00
Malte Ubl 5db08546d3 Decide equal-length short string comparisons inline
Equal names are common in records-sort, and equal strings at different
addresses still reached scr_str_cmp. When the first words match and both
strings have the same length of at most 16 bytes, the word ending at that
length covers the remaining bytes: equal words answer 0, and a difference
is ordered by its first byte (ASCII-only for the UTF-16 order).
2026-10-08 15:31:04 -05:00
Malte Ubl 5ad1b976fd Decide string comparisons inline before calling the runtime
records-sort compares interned literals (`key === "department"`, equal
departments) and names that differ in their first bytes, yet every
comparison was an out-of-line call (and memcmp through a stub).

- sc_str_eq: identity, then byte length, then scr_str_eq.
- sc_str_cmp / sc_str_cmp_u16: identity; then the first differing byte
  of one 8-byte word when both strings are that long, otherwise of the
  first bytes; the UTF-16 order is decided inline only for ASCII bytes.
  Everything else calls the runtime.

The helpers are internal module functions that LLVM inlines; they are
emitted whenever the runtime entry is declared.

The comparator sort kernel also holds one undefined-`this` window for the
whole sort (each emitted call restores the receiver stack it pushes) and
resolves the element retain function once.
2026-10-08 15:31:04 -05:00
Malte Ubl c5bd41b3c5 Update lifetime tests for closure-body locals and borrowing union equality
The cleanup fixture runs as a closure body; its literal binding is now a
stable immortal value without cleanup, so the test uses an owned value.
Whole-union string equality is a borrowing use, so its caller passes a
stack box instead of a heap union.
2026-10-08 15:31:04 -05:00
Malte Ubl bf10875927 Sort comparator snapshots in the runtime without per-move reference counting
After the union fix, records-sort spent most of its time in the IR merge
sort's element moves: every src[i] read retained the swapped `src` array
and the element (scr_arr_get_ref), every dst[k] write released the old
slot, and `dst` grew from an empty array.

scr_arr_sort_values runs the IR helper's exact algorithm (same comparator
call order) over borrowed slot copies of the private, compacted snapshot:

- comparator calls use the callValue ABI (owned element arguments, an
  undefined `this` window, pending-exception check);
- the array keeps owning every element and is untouched while user code
  runs, so the cycle collector and a throwing comparator see the original
  snapshot; the sorted permutation is published only after success;
- f64 and reference-counted element types qualify (isSortValuesElement);
  unions with an undefined arm and checked/island values keep the IR.

The new internal arrIntrinsic `sortValues` is validated, may throw, and
is not reference-preserving.
2026-10-08 15:31:04 -05:00
Malte Ubl 9d9dd52652 Keep closure-body union locals and literal equality operands on the stack
The records-sort comparator spent ~70% of its samples allocating and
freeing union boxes: `for (const key of keys)` binds `key` as
`string | undefined` (array holes), and every `key === "department"`
heap-wrapped the literal as well.

- call-lifetimes: strict union equality borrows a local operand, like
  unionIsTag/unionNarrow; closure bodies get local facts (their captured
  cells are boxed and already rejected), parameter facts stay limited to
  environment-free synchronous bodies.
- unionEq emission: a freshly wrapped operand gets a stack box.
2026-10-08 15:31:04 -05:00
Malte Ubl 7c7d80dae5 Emit serialized IR with the compile pipeline's object audit option
The serialized-IR emission test compared a non-sanitized compile, which
now passes objectAudit: false, against direct emission with the default
objectAudit: true. The extra scr_obj_alloc_note/scr_obj_free_note calls
were an option mismatch, not an IR round-trip loss. Emit from the artifact
with the option compile derives from --sanitize, and check both lanes.
2026-10-08 15:29:36 -05:00