Iter 14f: Boehm conservative GC

Decision 9 ships. Through Iter 14e every ADT box, lambda env, and
closure pair was leaked. This iter substitutes GC_malloc for malloc
in all four IR allocation sites and links -lgc. No language change,
no AST change, no schema change.

Diff: 5 files modified, ~30 LOC net.
- codegen/lib.rs: 4 substitutions @malloc -> @GC_malloc.
- ail/main.rs: .arg("-lgc") in the clang invocation.
- 5 IR snapshot files: mechanical s/@malloc/@GC_malloc/, 9
  occurrences. IR is bit-identical to pre-14f modulo this
  substitution — exactly Decision 9's promise.
- e2e.rs: new test gc_handles_recursive_list_construction.
- examples/gc_stress.{ailx,ail.json}: new fixture, builds a 50-
  element list via recursive Cons, sums it (1275).

Hash invariance verified: every existing fixture def hash
unchanged (codegen and link line are downstream of canonical
bytes; AST didn't move).

Tests 79 -> 80, all green. Existing 79 byte-identical stdout.
gc_stress -> 1275. list_map_poly -> 2/3/4 unchanged. sort
sorted-list unchanged. cargo doc 0 warnings.

GC notes (pertinent to future work):
- GC_INIT() not needed on Arch libgc 1.5.6 (auto-init via
  __attribute__((constructor))).
- No conservative-scan over-retention observed.
- -lgc alone sufficient for link (pthread/dl transitive).

Pattern-shape note from gc_stress fixture writing: the post-14d
"if-then-else" replacement is `(match (app == n 0) (case
(pat-lit true) ...) (case (pat-wild) ...))`. Three lines for
what `if` used to do in one, but uniform with the language.
Worth flagging for the stdlib brief.

Language is feature-complete enough for stdlib. The three
blockers identified at the 14b boundary (redundancy 14d, tail
calls 14e, GC 14f) are all done. Plan 15a: first stdlib module
std_list.ailx with length/append/reverse/map/filter/fold_left/
fold_right/head/tail/is_empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 17:25:07 +02:00
parent d64031c234
commit ba516b8b39
12 changed files with 240 additions and 14 deletions
+56
View File
@@ -526,6 +526,62 @@ constructor-blocked recursions in `map`, `sort`, `insert`
remain unmarked — they cannot benefit from `musttail`
without a source-level rewrite.
## Decision 9: memory management — Boehm conservative GC
Through Iter 14e, every ADT box, lambda env, and closure pair was
allocated with bare `malloc` and never freed. That worked for the
17 test fixtures (all small, all short-lived) but is incompatible
with any real workload — a stdlib `fold` over a million-element
list would leak a million boxes. The "Goal" section's "no GC for
the MVP" framing predates the parameterised-ADT pipeline (Iter 13)
and the explicit-recursion expectation (Iter 14e); both make a
collector necessary.
**Choice: Boehm-Demers-Weiser conservative GC.** The simplest
working option:
- Replace `malloc(...)` with `GC_malloc(...)` in every IR site
(currently `lower_ctor`'s ADT box, `lower_lambda`'s env block
and closure pair).
- Replace the IR-level `declare ptr @malloc(i64)` with
`declare ptr @GC_malloc(i64)`.
- Add `-lgc` to the `clang` link command (in
`crates/ail/src/main.rs`'s `Build` / `Run` paths).
- No language-level change. No AST change. No schema change.
Rationale:
- **Mature.** Boehm has been the default conservative GC for
decades. Linux distros ship it as `libgc` / `libgc-dev` /
`gc` (Arch).
- **No language work.** Conservative scan of the C stack handles
AILang's stack frames without LLVM stack-map infrastructure
(which is its own multi-iter design).
- **Single-iter integration.** Lift-and-shift of the four
allocation sites; all existing tests must still pass with
identical output.
Trade-offs accepted:
- **Conservative over-retention.** A user-supplied `Int` field
whose value happens to coincide with a heap address will pin
that allocation. In practice, vanishingly rare for typical
values; survivable.
- **Pause time non-deterministic.** Boehm uses stop-the-world
mark-sweep. For LLM-author-written stdlib code at MVP scale,
pause times are not the bottleneck.
- **Build-time dependency.** `libgc` must be installed on the
build host. Users without it get a link-time error from
clang, not a silent failure.
A future iter may layer a per-fn-arena optimisation on top: when
a fn's return type contains no boxed ADT, ADT boxes allocated
inside that fn cannot escape, so an arena freed at fn return is
sound by construction (per the 14e GC notes). That requires
escape analysis, the corresponding AST/IR plumbing, and is its
own design pass. Boehm-everything is the floor; arena is an
optimisation above it.
## Mangling scheme (Iter 5c)
All AILang functions are mangled to `@ail_<module>_<def>` — even in the