Iter 17a: per-fn arena via alloca for non-escaping allocations
Adds a conservative escape analysis that flags Term::Ctor and
Term::Lam allocations as non-escaping when they are bound by a
let, never flow to a fn arg / ctor field / tail-return / lambda
capture, and remain inside the allocating fn's frame. Codegen
lowers flagged sites to LLVM `alloca` instead of `@GC_malloc`;
escaping sites continue to use Boehm.
- escape.rs (new): name-based taint propagation; let-only
candidates; tail-position / app-arg / ctor-field / lam-capture
all taint.
- codegen: three sites switched (lower_ctor, lam env, closure
pair). Save/restore non_escape across lambda thunk emit.
- examples/escape_local_demo.{ailx,ail.json}: focused fixture
exercising both branches (peek and recursive count).
- e2e + escape unit tests: 133 → 141 (+8).
Stop-gate: 17a closes the queue. Memory-management discussion
(GC scope) is the next user-driven step. Per-fn arena observations
appended to JOURNAL — including the headline finding that 0/270
shipped allocations are flagged non-escaping under this rule
(real AILang threads ctors directly between fns; build-locally /
consume-locally is not idiomatic).
All existing fixture stdouts and content hashes bit-identical;
IR snapshots unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+73
-5
@@ -599,6 +599,67 @@ escape analysis, the corresponding AST/IR plumbing, and is its
|
||||
own design pass. Boehm-everything is the floor; arena is an
|
||||
optimisation above it.
|
||||
|
||||
### Per-fn arena via stack `alloca` (Iter 17a)
|
||||
|
||||
Iter 17a layers exactly that optimisation, in its simplest form.
|
||||
`ailang-codegen` runs an escape-analysis pre-pass over every fn
|
||||
body (and every lifted lambda thunk body); allocations the pass
|
||||
proves do not outlive the fn frame are lowered to LLVM `alloca`
|
||||
instead of `@GC_malloc`. Allocations that may escape continue to
|
||||
use `@GC_malloc`. The Boehm collector is still linked and
|
||||
unchanged; this is purely an optimisation above the floor.
|
||||
|
||||
**Allocation mechanism: LLVM `alloca`** (not a heap arena). Stack
|
||||
allocation matches the "freed at fn return" lifetime exactly,
|
||||
needs no malloc/free pair, and integrates with LLVM's existing
|
||||
optimiser (mem2reg / SROA may further promote the alloca'd box
|
||||
to registers if the box is small and its uses are simple). No
|
||||
new runtime is introduced; no language-level change; no AST or
|
||||
schema change.
|
||||
|
||||
**Escape rule (conservative).** A `Term::Ctor` or `Term::Lam`
|
||||
allocation is non-escaping iff (1) it is the value of a
|
||||
`Term::Let { name = X, value = ALLOC, body = B }`, and (2) the
|
||||
body `B` does not let any value derived from `X` flow past the
|
||||
fn frame. "Derived from" follows two propagation rules:
|
||||
- A `Term::Match` whose scrutinee is a `Var` referring to a
|
||||
tainted name propagates taint to every pattern-bound name in
|
||||
every arm. (Pattern bindings hold field projections of the
|
||||
scrutinee, which live inside the same allocation.)
|
||||
- A `Term::Let { name = Y, value = Var(t), ... }` where `t` is
|
||||
tainted makes `Y` tainted in the let's body.
|
||||
|
||||
A tainted name "escapes" if it appears in any of: the tail
|
||||
position of `B`, the arg list of any `Term::App` / `Term::Do`,
|
||||
the field list of a `Term::Ctor`, or the free-var capture set of
|
||||
a `Term::Lam`. The closure-pair-callee position of `Term::App`
|
||||
where the callee is a bare `Var` to the tainted name is NOT an
|
||||
escape (calling locally is fine).
|
||||
|
||||
**What this is not.** Not a region-inference system. Not
|
||||
flow-sensitive within an arm. Not field-sensitive (pattern
|
||||
bindings are tainted wholesale). Precision can be improved later;
|
||||
correctness is the priority for this iter. A pessimistic answer
|
||||
(claiming an allocation escapes when it does not) only loses
|
||||
optimisation, never correctness.
|
||||
|
||||
**Codegen integration.** Three sites in
|
||||
`ailang-codegen/src/lib.rs`:
|
||||
- `lower_ctor` — ADT box.
|
||||
- `lower_lambda` env block (when there are captures).
|
||||
- `lower_lambda` closure pair (always 16 bytes).
|
||||
|
||||
Each site queries the per-fn `non_escape: BTreeSet<usize>` (raw
|
||||
pointer addresses of `Term::Ctor` / `Term::Lam` AST nodes flagged
|
||||
as non-escaping). On a hit the emitter writes
|
||||
`alloca i8, i64 <size>, align 8`; on a miss it writes
|
||||
`call ptr @GC_malloc(i64 <size>)`. The rest of the lowering (tag
|
||||
store, field stores, closure-pair packing) is identical.
|
||||
|
||||
The closure-pair and its env share an escape verdict — they have
|
||||
parallel lifetimes. If the closure pair is non-escaping, the env
|
||||
is too.
|
||||
|
||||
## Mangling scheme (Iter 5c)
|
||||
|
||||
All AILang functions are mangled to `@ail_<module>_<def>` — even in the
|
||||
@@ -861,11 +922,18 @@ What **is** supported (and used as the smoke test for the pipeline):
|
||||
sole text projection; `ail parse` is the inverse direction. Round-trip
|
||||
identity (text → AST → JSON → AST → text) is gated by
|
||||
`ailang-surface/tests/round_trip.rs` over every shipped fixture.
|
||||
- **Memory management via Boehm conservative GC** (Decision 9 / Iter 14f).
|
||||
Every ADT box, lambda env, and closure pair is allocated by `@GC_malloc`,
|
||||
declared in the LLVM module preamble and linked from `libgc` at build
|
||||
time. Soak-tested by `examples/gc_stress.ail.json` and the
|
||||
`examples/std_list_stress.ail.json` fixture.
|
||||
- **Memory management via Boehm conservative GC** (Decision 9 / Iter 14f),
|
||||
with **per-fn arena via stack `alloca` for non-escaping allocations**
|
||||
layered on top (Iter 17a). Every ADT box, lambda env, and closure
|
||||
pair allocates either via `@GC_malloc` (escaping; Boehm-managed) or
|
||||
via LLVM `alloca` (non-escaping; freed at fn return). The decision
|
||||
is made by an escape-analysis pre-pass over the fn body — see
|
||||
Decision 9's "Per-fn arena via stack `alloca`" subsection.
|
||||
Boehm-only soak tests are unchanged: `examples/gc_stress.ail.json`
|
||||
and `examples/std_list_stress.ail.json` still allocate via
|
||||
`@GC_malloc` because their boxes flow into other fns and escape.
|
||||
The per-fn-arena path is exercised end-to-end by
|
||||
`examples/escape_local_demo.ail.json` (Iter 17a fixture).
|
||||
- **First-class function references** (Iter 7). A top-level fn name (or
|
||||
qualified `prefix.def`) used as a `Term::Var` is a fn-value.
|
||||
- **Anonymous lambdas with capture** (Iter 8). `Term::Lam` constructs a
|
||||
|
||||
Reference in New Issue
Block a user