Iter 14e: explicit, verified tail calls

Decision 8 ships. Term::App and Term::Do gain tail: bool with
serde-default false and skip-when-false serialisation. New
typecheck pass verify_tail_positions enforces tail-position rules
(Scheme-style propagation through match arms, seq.rhs, let body,
lam body). Codegen emits musttail call for marked App calls.

Hash invariance verified: only the two migrated print_list defs
(list_map_poly.print_list, sort.print_list) changed hashes; all
other defs across all 18 fixtures kept bit-identical hashes —
confirms the skip-when-false serialisation rule works.

Tests 76 -> 79: tail_call_in_non_tail_position_is_rejected,
tail_call_in_tail_position_is_accepted, plus an IR-grep e2e test
asserting that print_list's recursive call site emits musttail
in the lowered IR. Existing 25 e2e tests unchanged in behaviour
(map -> [2,3,4], sort -> sorted list).

IR evidence at the recursive site:
  %v7 = musttail call i8 @ail_list_map_poly_print_list(ptr %v6)
  ret i8 %v7

Two deviations called out in the implementer report and JOURNAL:
1. tail-do uses tail call, not musttail. Cross-type return
   (runtime helpers return i32, AILang Unit is i8) would have
   LLVM reject musttail. Path is implemented but not exercised
   by any current fixture; proper fix is runtime-helper signature
   change, punted.
2. block_terminated flag in codegen so tail-call emit
   (musttail call + ret) doesn't get a duplicate trailing ret
   from surrounding code (match-arm phi, fn-body, lambda thunk).
   Internal plumbing; required for IR well-formedness.

Form (A) productions now at ~30, exactly the constraint-1
budget. Future surface additions need to retire something or
explicit-budget-rebalance in DESIGN.md.

GC notes from implementer survey land in JOURNAL:
- Allocations cluster in lower_ctor; every term-ctor does
  malloc(8+8n).
- Tail recursion does not reduce alloc pressure, only stack.
  For map-style ctor-blocked recursions, allocation IS the
  bottleneck.
- Per-fn arena is sound only when fn return type contains no
  boxed ADT. Most current fixtures violate this.

Plan 14f: Boehm conservative GC (GC_malloc, -lgc) as a first
cut. Single-iter integration, no AST/schema change. Stress
test: build a 100k Cons list, observe RSS doesn't blow up.

After 14f the language is feature-complete enough for stdlib
work (15a).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 17:16:28 +02:00
parent 8d97a924de
commit d64031c234
16 changed files with 768 additions and 81 deletions
+56
View File
@@ -107,6 +107,62 @@ fn list_map_poly_inc_then_prints() {
assert_eq!(lines, vec!["2", "3", "4"]);
}
/// Iter 14e: `tail: true` annotation on `print_list`'s recursive
/// call must reach LLVM as a `musttail call`. Asserted by emitting IR
/// for list_map_poly and grepping for the exact instruction. This is
/// the only direct evidence that the type-system marker actually
/// influences codegen — the e2e test above only checks observed
/// stdout, which `musttail` does not change.
#[test]
fn iter14e_print_list_recursion_emits_musttail() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let src = workspace.join("examples").join("list_map_poly.ail.json");
let tmp = std::env::temp_dir().join(format!(
"ailang_iter14e_musttail_{}",
std::process::id()
));
std::fs::create_dir_all(&tmp).unwrap();
let out_ll = tmp.join("list_map_poly.ll");
let status = Command::new(ail_bin())
.args(["emit-ir", src.to_str().unwrap(), "-o"])
.arg(&out_ll)
.status()
.expect("ail emit-ir failed to run");
assert!(status.success(), "ail emit-ir failed");
let ir = std::fs::read_to_string(&out_ll).expect("read emitted IR");
// Find the musttail call to print_list inside the print_list body.
// The exact line shape:
// %vN = musttail call i8 @ail_list_map_poly_print_list(ptr %vM)
let has_musttail = ir.lines().any(|l| {
l.contains("musttail call")
&& l.contains("@ail_list_map_poly_print_list(")
});
assert!(
has_musttail,
"expected `musttail call ... @ail_list_map_poly_print_list(...)` \
in emitted IR; not found.\nIR:\n{ir}"
);
// Defence-in-depth: the musttail call must be immediately followed
// by a `ret i8 %v...`. We assert the next non-empty line is a ret.
let mut lines = ir.lines();
while let Some(line) = lines.next() {
if line.contains("musttail call")
&& line.contains("@ail_list_map_poly_print_list(")
{
let next = lines.next().unwrap_or("").trim();
assert!(
next.starts_with("ret i8 "),
"musttail call must be immediately followed by `ret i8 ...`; \
got: `{next}`"
);
return;
}
}
panic!("musttail call line not found (sanity check)");
}
/// Iter 11 dogfood: insertion sort over an 11-element IntList.
/// Exercises `<=`, `if`, mutual-leaf recursion (`insert` and `sort`),
/// nested ctor construction, and the Iter 10 seq operator inside