Repository navigation
Drop guards on private constructor stores and borrow construction-only fields - #838
Merged
Merged
Conversation
…y fields Two uses of the construction-only facts outside the collector (backend/immutability.ts): - Publication guards: a store into the constructor's own `this` that lies before the constructor's escape point, in a constructor no ancestor of which lets `this` escape and that is never invoked directly on an existing object, cannot target a published object. It loses its rc == SIZE_MAX guard and no longer seeds may-throw, so constructors that could only throw through guards lose their pending checks at every `new`. Storing `this` into one of its own fields does not expose it. - Borrowed reads: `x.f` holding an object, record, array or string is borrowed as a call argument, receiver or stable alias when `f` is construction-only and `x` is itself borrowable (unchanged local, borrowed parameter, or such a projection): nothing can replace and release it while `x` lives. Not inside constructors, which may still write their own fields. Module constants (immutable, not TDZ-checked, assigned at most once) are borrowable too. No object is frozen or marked at run time; publication errors, store order and reflection are unchanged. tsc-ts (e469e33), static: published-field guards 6,528 -> 4,462, sc_release calls 146,056 -> 129,756, sc_retain calls 26,215 -> 22,123, scr_arr_release 29,865 -> 25,617, binary 23.68 -> 22.96 MB (both commits). Self-check single-threaded, executed: guards 96.1M -> 60.8M, retains 273.5M -> 247.2M, releases 340.3M -> 314.0M. Linux sandbox, interleaved medians, both commits against perf/alloc-hugepages: single-threaded (6 runs) base new change Excalidraw 8.62 s 8.34 s -3.2% Playwright 7.83 s 7.71 s -1.5% tsc-ts self-check 4.36 s 4.22 s -3.0% tRPC 1.63 s 1.58 s -3.1% TypeORM 6.68 s 6.50 s -2.5% --checkers 8 (10 runs): -2.3% .. +0.7% (neutral) Runtime suite (--layouts=4 --runs=16): geomean -1.9%; ast-interp -4.4%, validate-errors -3.8%, regex-logs -1.7% faster, template-render +1.4% slower, the rest neutral; sizes unchanged.
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
cramforce
force-pushed
the
perf/trace-cycle-candidates
branch
from
October 11, 2026 03:40
44f4d0b to
095fdca
Compare
cramforce
force-pushed
the
perf/static-immutability
branch
from
October 11, 2026 03:41
6ddaa2a to
2ae8459
Compare
cramforce
force-pushed
the
perf/trace-cycle-candidates
branch
from
October 11, 2026 03:45
095fdca to
44f4d0b
Compare
cramforce
force-pushed
the
perf/static-immutability
branch
from
October 11, 2026 03:45
2ae8459 to
6ddaa2a
Compare
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.
Brings in cc7c6cb (inline truncation decided for the host when no triple is given), which this branch predates; without it the native bootstrap's LLVM comparison with the Node seed fails on x86-64 Linux.
…ases Since #837 a type is traced only if it lies in a cycle-capable component or reaches an intrinsic reference; a type that merely holds a cycle cannot be reached back from it. In both backend-analysis cases `record:outer` only holds `record:cycle`, so the Node oracle and the native compiler agree on ["record:cycle"] and the expectations still listed `record:outer`.
# Conflicts: # packages/compiler/test/ts7/baselines/order-parity.json
# Conflicts: # packages/compiler/test/ts7/baselines/order-parity.json
# Conflicts: # packages/compiler/src/backend/llvm/emitter.ts # packages/compiler/test/ts7/baselines/order-parity.json
Borrow and move references whose owners provably outlive them
# Conflicts: # packages/compiler/src/backend/llvm/emitter.ts # packages/compiler/test/ts7/baselines/order-parity.json
(cherry picked from commit 9b80f3d)
(cherry picked from commit f3cfa80)
f3cfa80 asserted that the canonical byte loop contains no `fptosi double` at all, meaning no exact-index round trip. The fixture also coerces its running sum with ToUint32, whose inline fast path legitimately uses `fptosi double ... to i64`, so the test fails on main (aaccbb3) as well; #865 merged before the shards that run it. Assert instead that no `bytes.index` check block is emitted, which keeps the original intent (no unproven-index path for a proven in-bounds induction index).
# Conflicts: # packages/compiler/test/ts7/baselines/order-parity.json
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two uses of the construction-only facts outside the collector, in
backend/immutability.ts:thisthat is not yet observable cannot target a published object. The conditions: the store comes before the constructor's escape point, no ancestor constructor escapes, and the constructor is never invoked directly on an existing object. Such a store loses itsrc == SIZE_MAXguard. It also stops seeding may-throw, so constructors that could only throw through guards lose their pending checks at everynew.x.f(an object, record, array or string) is borrowed as a call argument, receiver or stable alias whenfis construction-only andxis itself borrowable. Nothing can replace and release it whilexlives. This does not apply inside constructors. Module constants (immutable, not TDZ-checked, assigned at most once) are borrowable too.Nothing is frozen or marked at run time.
Object.isFrozen, publication errors and store order are unchanged.Measured on tsc-ts, both PRs against #830, Linux sandbox, interleaved medians:
Static counts: published-field guards 6,528 → 4,462,
sc_releasecalls −11%,sc_retaincalls −16%. Binary 23.68 → 22.96 MB.Executed on the self-check: guards 96.1M → 60.8M, retains −9.6%, releases −7.7%.
Removing every publication guard (unsound, measurement only) stays within noise, so the time comes from borrowing and tracing, not from the guards.
Runtime suite (
--layouts=4 --runs=16): geomean −1.9%. ast-interp −4.4%, validate-errors −3.8% and regex-logs −1.7% are faster. template-render is +1.4% slower. The rest are neutral, and sizes are unchanged.Tests: new
immutability.test.tsunit tests, and an adjustedstring-lifetimestest (its record field was never written, so the borrow is sound; the test now adds a writer to keep covering the snapshot path). New corpus programs:4598-publish-constructor-stores: escaping constructors must still throw Node's frozen-object errors.4599-construction-only-field-borrows: callees drop every other reference before using a borrowed field; a field written by a method; a constructor reading its own field while a later operand replaces it.4600-construction-only-fields-dynamic-writes: writes throughanyand structural casts while the field is being read.All pass the plain and sanitized lanes. Preflight/order baselines are recorded for the five new fixtures.
Known risk: borrowed field reads rely on every field write being visible in the IR or in the untyped-store audit; a runtime helper writing class fields directly would become a use-after-free. Stacked on the cycle-tracing PR.