Expand description
Escape analysis for stack-allocatable aggregate sites —
Op::MakeRecord and Op::MakeTuple (#464 + tuple widening).
Walks every function’s bytecode to classify each aggregate
allocation site as either stack-allocatable (the value never
leaves the function frame) or escapes (used as a closure
capture, returned, stored in another aggregate, passed to a call,
sent on a channel, etc.). The escape rules are identical for
records and tuples — only the eventual codegen opcode differs —
so a single Slot::Agg(pc) lattice value tracks both.
§Status
- Records (
MakeRecord): analysis + codegen complete (#464).compiler::apply_escape_loweringrewrites non-escapingMakeRecordtoAllocStackRecord. - Tuples (
MakeTuple): analysis only, this slice. Sites are classified and reported withSiteKind::Tuple, but no codegen consumes them yet —apply_escape_loweringonly rewritesMakeRecord, so reporting tuple sites is inert until a tuple stack-alloc opcode lands. Mirrors how #464 sequenced records (analysis → codegen → bench).
§Approach
Abstract interpretation over the bytecode CFG. Each abstract state tracks:
- per-stack-slot:
Slot::Agg(pc)(the aggregate allocated atpc, still local) orSlot::Other(anything else — int, string, captured value, aggregate we’ve stopped tracking) - per-local: same
Slotlattice, indexed bylocals[i]
At each op we update the abstract state and union any newly-
observed escapes into a HashSet<u32> keyed by allocation pc.
Worklist fixpoint iterates until no state changes — joins use a
pointwise merge that downgrades Agg(a) ⊔ Agg(b) (a ≠ b) and
Agg(a) ⊔ Other to Other, marking the involved sites as
escaped (we can no longer prove they stay local from this
merge point forward).
§Intra-procedural limit
Calls (Call, TailCall, CallClosure) escape all argument
aggregates — we can’t see the callee’s body from here. Inlining
could recover the cross-fn case but is deliberately out of
scope; the issue’s wording (“function frame”) matches
intra-procedural.
§Conservative defaults
Whenever the analysis can’t prove an aggregate stays local, it defaults to escaped. False positives (sites flagged as escaping when they actually don’t) cost a heap allocation per request — the existing baseline. False negatives (a flagged- local site that actually escapes) would corrupt memory under stack-alloc codegen, so the codegen step treats the analysis output as a necessary but not sufficient precondition and pairs it with an unconditional runtime fallback.
Structs§
- Escape
Report - Per-function escape report — the artifact codegen consumes to decide where to emit a stack-alloc opcode vs the heap constructor.
- Escape
Site
Enums§
- Policy
- Scope policy for the escape analysis. Only
Op::Returnconsults it — every other escape rule is identical across policies. - Site
Kind - Which aggregate constructor an escape site is — determines which
stack-alloc opcode a future codegen slice would emit for it
(
AllocStackRecordfor records; tuple stack-alloc is not yet implemented, soTuplesites are reported but not lowered). - Slot
- Abstract value at a stack or local slot during the analysis.
Functions§
- analyze_
function - Analyze one function under the default
Policy::FrameScope. Seeanalyze_function_with_policyfor the policy-parameterized form used by #463 arena routing. - analyze_
function_ with_ policy - Analyze one function under the given scope policy. Cheap to call
even when there are no aggregate sites (early-exits after the
first pass over
code). - analyze_
program - Analyze every function in
functions. Returns oneEscapeReportper function that contains at least oneMakeRecordsite (functions with no record allocations are omitted — the consumer doesn’t care about them). - build_
escape_ index - Convenience wrapper over
analyze_programreturning a map keyed by(fn_name, pc)for direct lookup during codegen.