0%

Section 08: Escape Analysis & Physical Placement

Context:

  • Keep liveness, borrow inference, and Locality in the AIMS calculus.
  • Add lifetime evidence for block/function extent, argument-boundary crossing, and external reachability.
  • Treat “heap” as a later physical-plan choice, never an AIMS verdict.

That evidence is computed once in ori_arc and published as a typed, site-keyed carrier. VmLayoutPlan and CompiledLayoutPlan select physical storage from it; ReprPlan never feeds escape or ownership policy backward into AIMS.

Reference implementations:

  • Go cmd/compile/internal/escape/: Connection graph-based escape analysis — tracks dataflow from allocations to escape points
  • Swift lib/SILOptimizer/Transforms/StackPromotion.cpp: Walks SIL to check if alloc_ref escapes
  • Java HotSpot macro.cpp: Scalar replacement — replaces heap object with individual fields on stack
  • Lean4 Borrow.lean: Parameter ownership inference that implicitly identifies non-escaping borrows

Depends on: §02 (triviality classification helps escape analysis — trivial values don’t need escape tracking).

Risk warning (VERY HIGH COMPLEXITY): This is the largest (~1,500 lines) and most dangerous section. Key risks:

  1. Connection graph escape analysis is interprocedural — requires whole-module fixed-point iteration that interacts with ori_arc’s existing borrow inference.
  2. Stack promotion (§08.3) changes allocation semantics — a bug means use-after-free. Requires the most thorough Valgrind testing of any section.
  3. Bump allocation (§08.4) adds a new runtime allocation scheme to ori_rt that must integrate with existing COW, slice, and RC infrastructure.
  4. §08.5 touches the closed realize_closed_program() / executable-artifact seam — the most sensitive semantic boundary in the compiler. Internal AIMS pipeline functions remain crate-private implementation details.

Recommended approach: Implement §08.1 (intraprocedural) first as a standalone pass. Ship it, measure, and verify with Valgrind before attempting §08.2 (interprocedural) or §08.4 (bump allocation).


08.1 Intraprocedural Escape Analysis

File(s): compiler/ori_arc/src/aims/ locality/escape analysis plus the shared executable fact carrier

Start with per-function analysis (no cross-function information). This catches the most common patterns: temporary collections, intermediate strings, local structs.

  • Extend the existing AIMS Locality evidence; do not define a parallel representation-owned escape lattice:

    pub struct AllocationLocalityFact {
        pub site: AllocationSiteId,
        pub locality: Locality,
    }
    
    pub enum ExtentClass {
        StaticShape(TypeId),
        RuntimeSized(StorageSiteId),
    }

    AIMS owns the first record. Neutral representation analysis owns the second; neither contains byte size, alignment, stack, arena, region, or heap policy.

  • Implement connection graph:

    pub struct ConnectionGraph {
        /// Node per allocation site + parameter + return
        nodes: Vec<CgNode>,
        /// Edges: PointsTo (field → object), Deferred (alias)
        edges: Vec<CgEdge>,
    }
    
    pub enum CgNode {
        /// Logical owned-value birth site; physical storage is not selected here.
        Alloc { id: AllocId, locality: Locality },
        /// Function parameter (locality depends on callers)
        Param { index: usize, locality: Locality },
        /// Function return (outlives this function; physical placement unknown)
        Return,
        /// Phantom node for unknown destinations
        Unknown,
    }
    
    pub enum CgEdge {
        /// a.field points to b
        PointsTo { from: NodeId, field: u32, to: NodeId },
        /// a defers to b (alias — same object)
        Deferred { from: NodeId, to: NodeId },
    }
  • Implement escape propagation:

    pub fn analyze_allocation_locality(
        func: &ArcFunction,
        pool: &Pool,
    ) -> AllocationLocalityFacts {
        let mut graph = build_connection_graph(func, pool);
    
        // Fixed-point: propagate AIMS locality through edges
        let mut changed = true;
        while changed {
            changed = false;
            for edge in &graph.edges {
                let (from_locality, to_locality) = match edge {
                    PointsTo { from, to, .. } => (graph.locality(*from), graph.locality(*to)),
                    Deferred { from, to } => (graph.locality(*from), graph.locality(*to)),
                };
                // If destination escapes farther, source does too.
                let merged = from_locality.join(to_locality);
                if merged != graph.locality(edge.source()) {
                    graph.set_locality(edge.source(), merged);
                    changed = true;
                }
            }
        }
    
        AllocationLocalityFacts::from_graph(graph)
    }
  • Escape sources (what forces HeapEscaping):

    • Value is returned from function
    • Value is stored in a mutable reference parameter
    • Value is captured by a closure that escapes
    • Value is stored in a global variable
    • Value is passed as an owned parameter to an unknown function
    • Value is stored in an ownership-bearing field of a value that escapes
  • Non-escape sinks (what preserves BlockLocal or FunctionLocal):

    • Value is only read (not stored)
    • Value is passed as a borrowed parameter to a known function
    • Value is consumed (last use) within the function
    • Value is used in pattern matching then discarded
  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (08.1) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-08.1 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 08.1: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


08.2 Interprocedural Escape Analysis

File(s): compiler/ori_arc/src/aims/interprocedural/ locality summaries

Cross-function escape analysis uses function summaries to track which parameters escape.

  • Define function escape summaries:

    pub struct FunctionEscapeSummary {
        /// Which parameters escape?
        pub param_locality: Vec<Locality>,
        /// Does the return value contain any input parameters?
        pub return_aliases: Vec<usize>, // param indices
    }
  • Compute summaries bottom-up through the call graph:

    • Leaf functions (no callees): direct analysis
    • Non-leaf functions: use callee summaries to refine escape states
    • Recursive functions: conservative (assume all params escape) then refine
  • Apply summaries at call sites:

    // At call site: f(x, y, z)
    // If f's summary says param 0 doesn't escape:
    //   → x does NOT escape through this call
    // If f's summary says param 1 escapes:
    //   → y DOES escape through this call
  • Integrate with the same interprocedural AIMS contract solve:

    • borrow/access and locality remain distinct lattice dimensions with one SCC schedule
    • a borrowed parameter does not by itself prove block/function locality
    • an owned parameter does not by itself prove heap escape
    • publish one contract result; never infer locality later from ownership alone
  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (08.2) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-08.2 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 08.2: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


08.3 Stack-Promotion Physical Projections

File(s): shared VmLayoutPlan/CompiledLayoutPlan construction plus the VM and compiled allocation adapters

When AIMS proves BlockLocal or FunctionLocal lifetime and neutral representation evidence proves a compatible static extent, each physical plan may choose stack/frame/region storage. LLVM alloca is one compiled projection; VM frame/arena storage and direct native/WASM stack slots are siblings.

  • Replace ori_rc_alloc with alloca:

    ; Before (heap):
    %ptr = call ptr @ori_rc_alloc(i64 24, i64 8)
    ; ... use ptr ...
    call void @ori_rc_dec(ptr %ptr, ptr @_ori_drop$42)
    
    ; After (stack), only for a plan whose exact facts discharge count storage:
    %ptr = alloca [24 x i8], align 8
    ; ... use ptr ...
    ; storage lifetime ends automatically; emit the bound logical child/user-drop
    ; plan and any still-required sharing observation before lifetime.end
  • Elide count updates only when the bound AIMS artifact has OwnerBound::Bounded(0), no additional-credit or sharing-observation events, closed external visibility, and an exact satisfaction mapping for every release event. Stack placement alone never licenses ownership-event removal.

  • Discharge storage release mechanically at the validated lifetime end, while preserving every logical child/user-drop and unwind obligation.

  • Preserve the AIMS event contract while changing mechanism:

    • additional-credit/release identities project to verified ownership or lifetime operations, or individually licensed no-op count updates
    • field/user-drop order remains the shared ExecutableDropPlan order
    • a backend cannot infer locality or suppress a drop from its own layout
  • Handle non-trivial fields in stack-promoted values:

    • If the struct has managed fields (e.g., struct { name: str, age: int }):
      • The struct itself may use stack storage with whatever ownership-observation protocol its facts require
      • The str field retains an independent logical ownership/drop-plan identity; a physical plan may or may not realize it as a separate heap allocation with a counter
      • At the validated lifetime end, execute the exact shared drop plan for the string field through the selected physical mechanism
  • Lifetime extension for stack-promoted values:

    • If the value is live across a function call, the alloca must dominate the call
    • LLVM’s alloca in the entry block is lifetime-safe
    • Use llvm.lifetime.start / llvm.lifetime.end intrinsics for precise scoping
  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (08.3) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-08.3 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 08.3: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


08.4 Bump Allocation for Non-Escaping Dynamic Values

File(s): shared region-allocation plan plus VM and compiled allocation adapters

Stack/frame placement (§08.3) works for statically shaped values, but not for runtime-sized collections. Those still need dynamic storage, but a physical planner need not choose general-purpose heap allocation when their lifetime is bounded.

Candidate compiled strategy:

  • Emit a function-local bump allocator for runtime-sized values admitted by AIMS lifetime/cleanup facts.

  • Release the region on every normal and unwind exit.

  • Permit the VM to satisfy the same facts with a traced arena or another measured mechanism.

  • Contrast Zig’s manually passed arena allocators with Ori’s validated automatic physical-plan selection.

  • Define backend-owned allocation mechanisms; these enums are physical-plan vocabulary and never AIMS output:

    pub enum CompiledAllocationMechanism {
        RuntimeHeap,
        StackSlot,
        Region(CompiledRegionId),
    }
    
    pub enum VmAllocationMechanism {
        ManagedHeap,
        FrameSlot,
        Region(VmRegionId),
    }
  • Select strategy from AIMS lifetime/cleanup facts, neutral extent evidence, and backend layout constraints:

    • block/function-local + static shape → stack/frame candidate (§08.3)
    • block/function-local + runtime-sized + region-complete cleanup → region/bump candidate
    • wider lifetime or unsupported physical constraint → a conservative longer-lived plan
  • Emit bump allocator prologue/epilogue in LLVM IR:

    ; Function prologue — allocate bump region
    %bump.base = call ptr @ori_bump_alloc(i64 4096)  ; initial 4KB region
    %bump.ptr = alloca ptr                            ; current bump pointer
    store ptr %bump.base, ptr %bump.ptr
    
    ; Bump allocation (instead of ori_rc_alloc):
    %current = load ptr, ptr %bump.ptr
    %next = getelementptr i8, ptr %current, i64 %size
    store ptr %next, ptr %bump.ptr
    ; %current is the allocation base selected by CompiledLayoutPlan.
    ; Header/count metadata is present or absent only as that validated plan says.
    
    ; Function epilogue — free entire region
    call void @ori_bump_free(ptr %bump.base)
  • Handle growth: if bump region is exhausted, allocate a new linked region. The ori_bump_alloc / ori_bump_free functions in ori_rt manage a linked list of regions.

  • Bump/arena placement does not imply headerlessness or RC elision. Elide count mechanics only when OwnerBound, OwnershipObservationFacts, and visibility permit it; preserve logical drop and unwind events at region release.

  • Interaction with COW: Use AIMS uniqueness and sharing-observation facts. A local region may contain multiple aliases, so physical placement alone cannot select StaticUnique or remove IsShared.

  • Unit tests:

    • A runtime-sized list with function-bounded lifetime, region-complete cleanup, and compatible compiled constraints selects the region candidate and avoids the general allocator.
    • A returned list is rejected from any region whose lifetime ends before the caller’s bound; the planner may select managed storage, caller-provided storage, or another proved longer-lived mechanism rather than hardcoding “heap.”
    • A temporary list with an unknown/externally visible lifetime does not select the region merely because it is syntactically local.
    • Region growth beyond 4 KiB uses linked regions and one validated cleanup on normal and unwind exits.
  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (08.4) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-08.4 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 08.4: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


08.4a Per-Iteration Nonescaping Allocation Elision

Transferred from: plans/completed/aims-burden-tracking/ section s-332042fe. This work is physical placement and escape optimization driven by neutral AIMS facts; it is not part of provenance-ledger retirement.

File(s): AIMS lifetime/locality production, ExecutableProgram allocation facts, VM and compiled physical planners, optimization diagnostics, and pinned allocation benchmarks.

Canonical stress shape: construct a 100-element list inside each loop iteration, observe only its constant length, and discard it before the back edge. The old burden section measured the compiled path at roughly 374 ms versus roughly 1 ms for clang++ -O3, which eliminates the allocation. The exact baseline must be remeasured after bytecode-vm; the semantic requirement is unchanged.

  • Pin the allocation-per-iteration workload across [bool], [int], and str, including a constant-foldable read-only case, a nonconstant read case, a value that crosses the back edge, a returned value, an escaping closure capture, and an unwind path. Record VM and compiled allocation traffic, peak/live bytes, and wall time separately.
  • Extend the one AIMS locality/lifetime calculus with a generalized per-iteration fresh-allocation theorem. A candidate must have a stable allocation identity, remain within the iteration lifetime, cross no loop-body ownership boundary, satisfy exact child/user-drop and unwind cleanup, and have no external visibility. Prove the rule in compiled Lean before implementing it and synchronize all four AIMS surfaces.
  • Publish the result as neutral LifetimeBound/locality/cleanup facts keyed by the shared executable allocation identity. Do not introduce a representation-owned escape lattice, a per-shape ownership scan, or an AIMS-selected stack/arena/heap mechanism.
  • Let each physical planner choose a satisfying mechanism: dead-allocation elimination when every observation is constant-foldable and effects are absent; frame/stack placement for compatible static extent; bounded VM/compiled regions for runtime-sized values; otherwise a conservative longer-lived mechanism. Every choice carries an exhaustive fact-satisfaction mapping.
  • Preserve exact ownership-observation, drop, and unwind obligations even when storage allocation disappears. A planner may license a no-op only with an individually identified proof; “local” alone never licenses cleanup deletion.
  • Add must-fire and must-not-fire IR/plan pins. The constant-length fixture must show zero general-heap allocations in every supporting physical projection; escaping and observable fixtures must retain a valid allocation. Evaluator behavior remains the independent oracle.
  • Require evaluator/VM/LLVM debug/release/AOT and affected native/WASM/JIT parity, zero final live allocations, sanitizer/leak cleanliness, and a release benchmark ratio at or below 1.5x the pinned C++ baseline unless a stricter plan-wide target supersedes it.

Banned approaches: a 29th leaf scan; inferring lifetime from ownership mode; treating stack placement as automatic RC/drop elision; LLVM-only escape authority; or deleting a logical cleanup event because a benchmark still prints the right value.

  • /tpr-review passed and all findings resolved.
  • /impl-hygiene-review passed after TPR.
  • Subsection close-out (08.4a) — run the required /improve-tooling, /sync-claude, and repo-hygiene checks; record the transferred burden section as delivered here.

08.5 Compose Neutral Allocation Facts for Physical Planning

File(s): AIMS result carrier, ExecutableProgram, VmLayoutPlan, and CompiledLayoutPlan

  • Publish site-keyed locality, lifetime, owner bounds, ownership-observation events, thread-reachability, visibility, and cleanup after AIMS.

  • Publish neutral ExtentClass evidence from representation analysis.

  • Compose both sources into one immutable, identity-bound allocation-fact table after AIMS.

  • Forbid ReprPlan, target layout, and selected allocation mechanisms from feeding back into the calculus.

  • Add an exhaustive allocation-fact table to the shared executable artifact:

    • stable allocation-site identity
    • AIMS locality, lifetime, owner bounds, exact OwnershipObservationFacts, cleanup, thread-reachability, and visibility facts
    • representation-owned static-shape or runtime-sized ExtentClass, without target byte offsets
    • exact value/drop-plan identity and required field/user-drop order
  • Validate one-to-one coverage and exact fact identity between realized allocation operations, the AIMS result, representation evidence, and the composed table before backend selection.

  • Derive VmLayoutPlan and CompiledLayoutPlan independently from the same table.

  • Map logical owner-credit creation/release/drop events to the chosen mechanism without deleting, inventing, or reordering a calculus-owned obligation.

  • Let a physical planner choose a conservative longer-lived mechanism when needed, but fail closed when the selected plan has insufficient lifetime, count capacity, cleanup, synchronization, or external-layout compatibility.

  • Prohibit ReprPlan -> AIMS dependency edges and tests that make ownership correctness depend on an LLVM layout.

  • /tpr-review passed — independent review found no critical or major issues (or all findings triaged)

  • /impl-hygiene-review passed — hygiene review clean. MUST run AFTER /tpr-review is clean.

  • Subsection close-out (08.5) — MANDATORY before starting the next subsection. Run /improve-tooling retrospectively on THIS subsection’s debugging journey (per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”): which diagnostics/ scripts you ran, where you added dbg!/tracing calls, where output was hard to interpret, where test failures gave unhelpful messages, where you ran the same command sequence repeatedly. Forward-look: what tool/log/diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-08.5 retrospectivebuild/test/chore/ci/docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 08.5: no tooling gaps”. Update this subsection’s status in section frontmatter to complete.

  • /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.

  • Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.


08.6 Completion Checklist

Test matrix for §08 (write failing tests FIRST, verify they fail, then implement):

Allocation patternExpected neutral factsPhysical-plan requirementSemantic pin
let x = Point { x: 1, y: 2 }; x.x + x.yblock-local lifetime + static shapevalidated stack/frame candidateYes — selected plan avoids general heap
let list = [1, 2, 3]; len(list)function-local lifetime + runtime-sized extentvalidated bounded-region candidateYes — selected plan avoids general heap
Per-iteration [bool; 100] observed only through constant lengthiteration-local lifetime + no observable allocation identitydead-allocation elimination or equivalent zero-general-heap planYes — must-fire zero allocation
let s = "hello"; print(s) where print borrowscall-extent lifetime independent of borrow modeany satisfying planYes — borrow does not imply heap escape
fn make_point() -> Point { Point { x: 1, y: 2 } }caller-extent or escaping lifetimeno storage shorter than the returned valueYes — caller owns result
let closure = |x| x + 1 (no captures)function-local lifetime + closed callable identityany satisfying planTest captures correctly
chan.send(value)potentially shared + externally reachablelifetime-compatible, thread-safe planYes — thread boundary
Returned recursive Node { value: int, next: Option<Node> }escaping lifetime + recursive extent evidenceconservative satisfying planYes — recursion does not itself define locality
let pair = (1, 2); pair.0block-local lifetime + static shapevalidated stack/frame candidateYes — tuple remains local
  • Selected static-shape local fixtures use validated stack/frame storage where the physical planner supports it
  • Borrowed calls preserve independently computed lifetime facts; borrow mode never selects placement
  • Selected dynamic-size local fixtures use a validated bounded region where the physical planner supports it
  • Per-iteration constant-observation fixtures eliminate the general-heap allocation; back-edge, return, capture, and unwind negative controls remain conservatively allocated
  • Region placement preserves all AIMS additional-credit/release/sharing-observation, COW, child-drop, user-drop, and unwind obligations
  • Returned values never use storage shorter than their AIMS lifetime; caller storage, a longer-lived region, or managed heap may satisfy it
  • Closures that capture values correctly mark those values as escaping
  • ./test-all.sh green
  • ./clippy-all.sh green
  • ./diagnostics/valgrind-aot.sh clean (no use-after-free from premature stack deallocation or bump region reuse)
  • Evaluator, VM, LLVM debug/release/AOT, and affected native/WASM/JIT projections preserve observable behavior and exact cleanup; dual-exec-verify.sh supplies only the current evaluator/LLVM leg
  • Compiled fixtures whose validated plan selects only stack/region mechanisms emit zero ori_rc_alloc calls
  • /tpr-review passed — independent Codex review found no critical or major issues (or all findings triaged)
  • /impl-hygiene-review passed — implementation hygiene review clean (phase boundaries, SSOT, algorithmic DRY, naming). MUST run AFTER /tpr-review is clean.
  • /improve-tooling retrospective completed — MANDATORY at section close, after both reviews are clean. Reflect on the section’s debugging journey (which diagnostics/ scripts you ran, which command sequences you repeated, where you added ad-hoc dbg!/tracing calls, where output was hard to interpret) and identify any tool/log/diagnostic improvement that would have made this section materially easier OR that would help the next section touching this area. Implement every accepted improvement NOW (zero deferral) and commit each via SEPARATE /commit-push. The retrospective is mandatory even when nothing felt painful — that is exactly when blind spots accumulate. See .claude/skills/improve-tooling/SKILL.md “Retrospective Mode” for the full protocol.

Exit Criteria: One identity-bound executable allocation-fact table composes AIMS locality/lifetime/ownership/cleanup/thread/visibility facts with representation-owned extent evidence and drives both VM and compiled physical plans without a reverse dependency. A temporary fixed-size value may use frame/stack storage, a dynamic local may use a bounded region, and a longer-lived value uses any validated mechanism that satisfies its lifetime.

LLVM IR contains no unnecessary heap-RC calls for promoted values, the VM reports the corresponding storage class, and all executors preserve behavior, drop order, and leak freedom.


08.R Third Party Review Findings

  • [TPR-08-001][major] section-08-escape-analysis.md:223-277Bump allocation (§08.4) is a separate runtime subsystem (~500+ LOC across 3 crates) embedded as a 56-line subsection. §08.4 proposes new runtime functions (ori_bump_alloc, ori_bump_free), linked-list region growth, LLVM prologue/epilogue emission, COW interaction (“always StaticUnique”), and integration with existing RC infrastructure. This requires coordinated changes to ori_rt, ori_repr, and ori_llvm. The plan rates §08 as “VERY HIGH COMPLEXITY” and recommends shipping §08.1 first — but §08.4 is not separated into its own section or formally deferred. Action: Extract §08.4 into a standalone section (§08b) with its own completion checklist, exit criteria, and line estimates, OR explicitly defer bump allocation to a future plan and remove it from §08’s exit criteria. The phased recommendation at line 54 is correct but should be formalized as a section boundary.