Repository navigation
Stack promotion: short-lived objects on the stack under every memory model - #605
Merged
Merged
Conversation
Escape analysis picks stack or heap for every class, array, boxed object literal and capture box allocation, under all memory models; Class() is the same as new Class(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Calling a class without new went through a stack-allocation path that rejected every class (all have a virtual table now). It is now the same C..new call plus constructor call as new C(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--no-stack-promotion, --stack-promotion-max-object/-max-frame and --stack-promotion-report; the pass runs under every model after own's inference. test-runner takes -no-stack-promotion with its own script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A boxed object literal or a new C() whose object is only read and written through its fields, and owns no heap memory, goes on the stack: an entry-block block with the heap layout and an immortal header. A C..new call is replaced by its body with the ts.New on the stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A per-parameter 'may escape' fact for every function body in the module, to a fixpoint; virtual calls take every vtable slot of the same method, and only for a class whose vtable is local to the module. Objects followed through local slots; a site in a loop whose object may reach the next iteration stays on the heap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ts.ReleaseContents gives back what a stack object's fields own without freeing it. A promoted ts.New moves to the entry block, so the object dominates every return; where the allocation was, ReleaseContents reset releases the previous iteration's contents and zeroes the object, and each return gives back the last one. Functions with a try region, and under rc/own objects owning memory in a module that uses Shared, stay on the heap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An object over --stack-promotion-max-object, or past the function's --stack-promotion-max-frame (sites in loops first), stays on the heap; so do sites in recursive functions, async functions and generator bodies, and thrown objects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An array literal that never changes length and never leaves the function gets its header and elements in the frame; under rc and own only when its elements own nothing. A closure's capture box goes in the frame when its closures stay in the function, under gc and none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TSLANG_TEST_NO_STACK_PROMOTION passes -no-stack-promotion to every test-runner test, so the suite can be compared both ways. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/memory-models.md describes what is promoted, what stays on the heap, the limits and flags, and measured times under every model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A virtual call's candidates are trusted only when every vtable holding that slot is local to the module: an exported subclass may be extended elsewhere. A call through a value the module cannot resolve may reach any function whose address is taken, so recursion through a closure or a function variable is seen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 18, 2026
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.
Summary
An allocation whose object provably never outlives its function now goes in the function's stack frame instead of on the heap, under every memory model (gc, rc, own, none). It is automatic and silent: a site the analysis cannot prove safe stays on the heap exactly as before.
new C(), andC(), which now means exactly the same), object literals with methods, array literals that never change length, and closure capture boxes (gc/none).StackPromotionPassruns at the affine level after own's inference.StackEscapeFactscomputes, for every function in the module, whether each parameter may escape (to a fixpoint, with virtual calls resolved through the module's vtables). A promoted object is an entry-block block laid out like a heap block with an immortal header, so every retain, release and destroy leaves it alone. Under rc and own, the newts.ReleaseContentsop gives back what its fields own: at the allocation site withreset(for loop reuse) and before every return.try,async, generators and recursion (including recursion through function values); loop-carried objects; objects over 256 bytes or past a 4096-byte frame budget; under rc/own, objects owning memory in a module that usesShared(becauseShared.countwould see the later release).--no-stack-promotion,--stack-promotion-max-object=,--stack-promotion-max-frame=,--stack-promotion-report(per-site remark:stack, orheap: <reason>).Design:
tslang/docs/superpowers/specs/2026-10-18-stack-promotion-design.md. Plan, with the deviations listed:tslang/docs/superpowers/plans/2026-10-18-stack-promotion.md. User docs:docs/memory-models.md.Measured
Milliseconds, best of three,
--opt --opt_level=3,--no-stack-promotion→ default:The ray tracer under rc is about 8% slower and this is not understood. Only two one-time setup objects are promoted, the hot functions' IR is identical, and a build that runs the analysis but promotes nothing matches
--no-stack-promotion.Tests
tslang/test/tester/stack-promotion/: a run test per program under all four models, plus a decision test per escape rule asserting the remark.TSLANG_TEST_NO_STACK_PROMOTION=ONoption (every runner test built with--no-stack-promotion).pass_to_keeperandctor_keeps_thisread dangling stack memory and fail under every model.Follow-ups (not in this PR)
anyholding a class instance throws "uncaught exception".🤖 Generated with Claude Code