Skip to main content

Module escape

Module escape 

Source
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_lowering rewrites non-escaping MakeRecord to AllocStackRecord.
  • Tuples (MakeTuple): analysis only, this slice. Sites are classified and reported with SiteKind::Tuple, but no codegen consumes them yet — apply_escape_lowering only rewrites MakeRecord, 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 at pc, still local) or Slot::Other (anything else — int, string, captured value, aggregate we’ve stopped tracking)
  • per-local: same Slot lattice, indexed by locals[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§

EscapeReport
Per-function escape report — the artifact codegen consumes to decide where to emit a stack-alloc opcode vs the heap constructor.
EscapeSite

Enums§

Policy
Scope policy for the escape analysis. Only Op::Return consults it — every other escape rule is identical across policies.
SiteKind
Which aggregate constructor an escape site is — determines which stack-alloc opcode a future codegen slice would emit for it (AllocStackRecord for records; tuple stack-alloc is not yet implemented, so Tuple sites 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. See analyze_function_with_policy for 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 one EscapeReport per function that contains at least one MakeRecord site (functions with no record allocations are omitted — the consumer doesn’t care about them).
build_escape_index
Convenience wrapper over analyze_program returning a map keyed by (fn_name, pc) for direct lookup during codegen.