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:
@@ -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
|
||||
|
||||
@@ -2002,6 +2002,102 @@ Anything else (records as a primitive, nested patterns, local
|
||||
recursive let, type classes) can layer on later without forcing
|
||||
stdlib rewrites.
|
||||
|
||||
## 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, ~30 LOC net.**
|
||||
|
||||
- `crates/ailang-codegen/src/lib.rs`: 4× `@malloc` → `@GC_malloc`
|
||||
(declare line + 3 call sites: `lower_ctor`, `lower_lambda`'s
|
||||
env, `lower_lambda`'s closure pair).
|
||||
- `crates/ail/src/main.rs`: `.arg("-lgc")` added to the clang
|
||||
invocation in `build_to`.
|
||||
- `crates/ail/tests/snapshots/{hello,sum,list,max3,ws_main}.ll`:
|
||||
mechanical s/@malloc/@GC_malloc/, 9 occurrences across 5
|
||||
files. The IR is bit-identical to pre-14f modulo this
|
||||
substitution — exactly Decision 9's promise.
|
||||
- `crates/ail/tests/e2e.rs`: new test
|
||||
`gc_handles_recursive_list_construction` (+19 LOC).
|
||||
- `examples/gc_stress.{ailx,ail.json}`: new fixture.
|
||||
|
||||
**Hash invariance verified.** Every existing fixture's def
|
||||
hashes are unchanged. The codegen and link line are downstream
|
||||
of canonical bytes; the AST schema didn't move; nothing on
|
||||
disk in `examples/*.ail.json` was touched. The new
|
||||
`gc_stress` module adds 4 new hashes, all unrelated.
|
||||
|
||||
**Tests: 80/80 (was 79).** Existing 79 produce byte-identical
|
||||
stdout — only the allocator changed, semantics unchanged. New:
|
||||
`gc_handles_recursive_list_construction` builds a List<Int> of
|
||||
length 50 via recursive `Cons`, sums it (`1275`). Manual smoke:
|
||||
|
||||
- `gc_stress.ail.json` → `1275`.
|
||||
- `list_map_poly` → `2 3 4` (unchanged).
|
||||
- `sort` → sorted list (unchanged).
|
||||
|
||||
`cargo doc --no-deps`: 0 warnings (DESIGN.md item 6 invariant
|
||||
preserved through nine iters of feature work).
|
||||
|
||||
**Pattern shape used in `gc_stress`** (caught a small typechecker
|
||||
constraint). The first proposed shape `(case (lit-int 0) Nil)`
|
||||
doesn't parse — `pat-lit` takes the bare literal token, not a
|
||||
keyword-prefixed form, and `case` requires a pattern. Worked
|
||||
shape: comparison-and-bool-match, mirroring `sort.ail.json`'s
|
||||
`<=` arm:
|
||||
|
||||
```
|
||||
(match (app == n 0)
|
||||
(case (pat-lit true) (term-ctor List Nil))
|
||||
(case (pat-wild) (term-ctor List Cons n (app build (app - n 1)))))
|
||||
```
|
||||
|
||||
This is the canonical "if-then-else" pattern post-14d. Worth
|
||||
flagging for the stdlib brief: predicates that need to branch
|
||||
go through the `==` / `<` / `<=` builtin returning Bool, then
|
||||
match on that Bool with a wildcard fallback. Three lines for
|
||||
what `if` used to do in one — but uniform with the rest of the
|
||||
language, no special case.
|
||||
|
||||
**GC integration notes.**
|
||||
- `GC_INIT()` is **not needed** on this build host (Arch
|
||||
with `libgc 1.5.6`). libgc auto-inits via
|
||||
`__attribute__((constructor))`.
|
||||
- No conservative-scan over-retention symptom observed: every
|
||||
existing test's stdout byte-identical; behaviour preserved.
|
||||
- `-lgc` alone is sufficient for the link; pthread/dl come in
|
||||
transitively from libgc.so's NEEDED entries.
|
||||
|
||||
**Language is feature-complete enough for stdlib.** Iters 14d
|
||||
(redundancy removal), 14e (explicit tail calls), 14f (GC) are
|
||||
the three blockers identified at the 14b boundary. They are
|
||||
all done. Anything else (records as primitive, nested patterns,
|
||||
local rec let, type classes) layers on later without forcing
|
||||
stdlib rewrite.
|
||||
|
||||
**Plan 15a.** First stdlib module: `examples/std/std_list.ailx`.
|
||||
Combinators: `length`, `append`, `reverse`, `map`, `filter`,
|
||||
`fold_left`, `fold_right`, `head`, `tail`, `is_empty`. Each
|
||||
combinator a fresh test vector for the parameterised-ADT +
|
||||
GC + tail-call combination. Authored in form (A) from day one;
|
||||
`.ail.json` produced via `ail parse`. Each combinator gets a
|
||||
dedicated e2e test.
|
||||
|
||||
If 15a surfaces compiler bugs (likely — every prior dogfood
|
||||
iter has, see 14a's monomorphisation bug), debugger handles
|
||||
them inline. If a compiler limitation surfaces that genuinely
|
||||
blocks the stdlib (e.g. nested patterns turn out to be needed),
|
||||
that becomes its own iter before 15a continues.
|
||||
|
||||
The architectural pin from Decision 6 governs: stdlib lives
|
||||
under `examples/std/` as `.ailx` source; tests load the
|
||||
generated `.ail.json`. `ailang-check` and `ailang-codegen`
|
||||
remain projection-agnostic.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user