Skip to content

Borrow and move references whose owners provably outlive them - #848

Merged
ctate merged 9 commits into
perf/static-immutabilityfrom
perf/rc-elision-si
Oct 11, 2026
Merged

ctate merged 9 commits into
perf/static-immutabilityfrom
perf/rc-elision-si

Conversation

@cramforce

Copy link
Copy Markdown
Contributor

Borrow references whose locations nothing in their extent can write

Commits: 3be18844, 318038e4 (self-hosted subset fix), caa7096d (TS baselines).

Borrowing decisions so far asked ReferenceEffects one question per function: does anything in it (or its callees) remove any heap edge? In tsc-ts almost every interesting callee does somewhere (a cache store, a constructor initializing its own fresh object, a closure call), so field-read arguments, rebound locals, walk locals, borrowed returns and loop elements were retained and released around most calls.

backend/llvm/reference-writes.ts computes, per function and transitively, which reference locations can be overwritten or removed: class fields (keyed by hierarchy root), record fields, array elements, map/set entries, boxes, globals and the current function's own locals, plus one "unknown heap write" location that never covers a local (no callee can rebind a caller's unboxed local). Constructor stores into their own this are excluded from new/super summaries (verified: constructors run only through new and super). Indirect calls resolve their closure targets through function-typed parameters, captures and adapter helpers. Fields that ImmutableFields (#838) proves construction-only add no location outside constructors.

A borrowed read is valid while nothing in its extent writes a location it depends on. Uses:

  • arguments of borrowed parameters (direct calls, new, virtual calls) and projection-only array-read arguments: the callee and the later arguments;
  • stable locals initialized by a frame-unowned projection (const t = node.type), and optional element reads into locals: the rest of their block;
  • loop element borrows: the iteration (body, condition, update) writes no array element nor the array's binding;
  • walk locals and borrowed returns in bodies that write the heap: the whole body writes none of the walked locations.

Measurements (tsc-ts e469e33, Linux 8 vCPU, interleaved medians; "st" = single-threaded, "c8" = --checkers 8), whole stack on top of #838:

app st c8
Excalidraw -5.6% -8.1%
tsc-ts self-check -1.2% -4.3%
TypeORM -1.5% -4.4%

Executed RC operations in emitted code (self-check, non-null and non-immortal): 452.8M -> 252.9M (-44%). Binary 22.96 -> 22.37 MB. Build time unchanged. Runtime suite (--layouts=4 --runs=16) vs #838: geomean -1.8%, no workload slower, sizes unchanged or smaller.

Corpus: 4630 (call arguments), 4631 (aliases), 4632 (loops and walks), 4633 (callbacks), each with cases where the overwritten field or element is the value's last owner.

Move owned locals into return values (95ecb6d9)

return x of an owned local loaded, retained and then released the local at scope exit. Outside finally regions the return now takes the local's reference. -3.9% executed RC operations on the self-check. Corpus 4633.

Lend optional element reads as borrowed returns (a225df63)

The lifted helpers for arr[i] reads of T | undefined elements (%arr.idxOr.N) returned an owned element. Walks now admit required element reads, so the helpers become borrowed returns (their body borrows the optional read directly) and receivers consume them without a retain/release, like any borrowed-return call. Removes the page-array retain in paged lookups such as this.pages[id >> 8]?.[id & 255] (checkerhot's handoff; e469e33 has no paged stores, so the self-check counts barely move). Corpus 4634.

Stacked on #838 (static immutability): builds on its construction-only facts.

@vercel

vercel Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 11, 2026 5:16pm UTC

ReferenceEffects answers one bit per function: does anything remove any
heap edge? Any reference store in the transitive callees (including a
constructor's stores into its own fresh `this` behind every `new`, a
closure call, or atomics in symbol-id allocation) disabled borrowing of
field-read and rebound-local arguments, walk locals, borrowed returns and
loop element borrows.

backend/llvm/reference-writes.ts keeps the location instead: per-function
transitive write sets over class fields (keyed by hierarchy root), record
fields, array elements, map entries, boxes, globals and the current
function's own locals, with key 0 as an unknown heap write. Locals stay
exact under unknown writes: no callee can rebind a caller's unboxed local.
Constructor stores into their own `this` drop out of `new`/`super`
summaries (checked: constructors run only through `new` and `super`).
Indirect calls resolve closure targets through function-typed parameters,
captures and adapters, otherwise the union of all closure bodies; runtime
library families that run no user code count as inert.

Uses:
- arguments of borrowed parameters (direct, `new`, virtual) and of
  projection-only array-read parameters borrow when neither the callee
  nor a later argument writes a location they depend on;
- stable locals initialized by a frame-unowned projection borrow when the
  rest of their block writes none of its locations; optional element
  reads into projection-only locals likewise;
- loop element borrows need only an iteration that writes no array
  element (nor the array's binding);
- walk locals and borrowed returns are admitted in bodies whose whole
  write set misses the walked locations.

tsc-ts e469e33 self-check (single-threaded, executed, non-null and
non-immortal RC operations in emitted code, measurement build):
505.3M -> 307.6M (-39%) before return moves (next commit).
Output identical. Build time unchanged (98.5 s vs 99.9 s, Linux).
`return x` of a local whose slot is an owned scope entry of the return
type loaded the value, retained it for the caller and released the local
at scope exit. Outside finally regions the return now takes over the
local's reference: no retain, and the local's entry is skipped by the
return's releases.

tsc-ts e469e33 self-check, executed non-null non-immortal RC operations
in emitted code: 320.2M -> 307.6M (-3.9%) on top of the previous commit.
Safe index reads of possibly-missing elements lower to lifted helpers
(%arr.idxOr.N) that returned an owned element. Walks now admit required
element reads of walked arrays (an index whose computation removes no
edge), so these helpers qualify as borrowed returns: their body lends the
element its array keeps alive (borrowing the optional read directly), and
receivers consume the call's result without a retain/release pair, as for
any borrowed-return call. Paged lookups such as
`this.pages[id >> 8]?.[id & 255]` no longer retain the page array.

Corpus 4630-4633 renumbered from 4620-4623 (checkerhot uses 4620/4621);
4634 pins paged lookups and a read followed by a removal.
…ubset

The natively compiled compiler rejected compound assignments to typed-array
elements, Uint32Array.some, a chained Set.add and a structural Pick of
ImmutableFields; bit operations now go through small helpers. The dense
reference-read test truncates its array first: a bare element return is
now lent as a borrowed return.
Two expectations predate the reference-write analysis and return moves on this branch. `keep(item, items) { items.push(item); return item; }` now qualifies as a borrowed-return function: its write set (array elements) misses the walked parameter, so the body retains only the appended element, returns the parameter +0 while the caller still owns it, and the owned adapter retains the result before releasing its parameters. In `snapshot`, `return saved` moves the saved alias's owned scope entry into the result rather than retaining it again and releasing the local at scope exit, leaving one retain.
… element

The reference-write analysis borrows an argument when neither the callee nor a later argument writes a location it depends on. A later argument that only throws a fresh Error writes no array element, so `read(items[0], fail())` now borrows the element from the borrowed parameter's array and has nothing to release on any edge; the test pins that. The exception-edge release is now exercised by a later argument that pops the array before throwing, which still retains the snapshot and releases it before the exceptional return.
# Conflicts:
#	packages/compiler/test/ts7/baselines/order-parity.json
@ctate
ctate merged commit 58b64fa into perf/static-immutability Oct 11, 2026
75 of 83 checks passed
@ctate
ctate deleted the perf/rc-elision-si branch October 11, 2026 17:56

This branch was successfully deployed

1 active deployment
Preview — 9931ea30 Deployed Oct 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants