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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user