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:
@@ -200,10 +200,18 @@ pub enum Term {
|
||||
Var { name: String },
|
||||
/// Function application. `callee` is evaluated to a function value;
|
||||
/// `args` are evaluated left-to-right.
|
||||
///
|
||||
/// Iter 14e: `tail` marks this call as occurring in tail position
|
||||
/// (per Decision 8). When set, codegen lowers the call as
|
||||
/// `musttail call`. The flag defaults to `false` and is omitted
|
||||
/// during canonical-JSON serialisation when unset, so pre-14e
|
||||
/// fixtures keep bit-identical hashes.
|
||||
App {
|
||||
#[serde(rename = "fn")]
|
||||
callee: Box<Term>,
|
||||
args: Vec<Term>,
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
tail: bool,
|
||||
},
|
||||
/// Let-binding: `value` is evaluated and bound to `name` in `body`.
|
||||
Let {
|
||||
@@ -213,9 +221,13 @@ pub enum Term {
|
||||
},
|
||||
/// Effect operation invocation (e.g. `do print "hi"`). The `op` is
|
||||
/// resolved against the effect-handler table at link time.
|
||||
///
|
||||
/// Iter 14e: see [`Term::App`] for the `tail` field semantics.
|
||||
Do {
|
||||
op: String,
|
||||
args: Vec<Term>,
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
tail: bool,
|
||||
},
|
||||
/// Constructor application. `type_name` binds the ADT, `ctor` the
|
||||
/// variant. Example: `Some(42)` ->
|
||||
@@ -402,3 +414,13 @@ impl PartialEq for Type {
|
||||
}
|
||||
}
|
||||
impl Eq for Type {}
|
||||
|
||||
/// Serde helper for `#[serde(skip_serializing_if = "is_false")]`.
|
||||
///
|
||||
/// Used by [`Term::App::tail`] and [`Term::Do::tail`] (Iter 14e) so the
|
||||
/// `tail` flag is omitted from the canonical JSON whenever it is false,
|
||||
/// preserving bit-identical hashes for every pre-14e definition.
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_false(b: &bool) -> bool {
|
||||
!*b
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ mod tests {
|
||||
Term::Var { name: "a".into() },
|
||||
Term::Var { name: "b".into() },
|
||||
],
|
||||
tail: false,
|
||||
},
|
||||
doc: None,
|
||||
})
|
||||
|
||||
@@ -175,7 +175,7 @@ fn term_block(t: &Term, indent: usize) -> String {
|
||||
match t {
|
||||
Term::Lit { lit } => format!("{pad}{}", lit_to_string(lit)),
|
||||
Term::Var { name } => format!("{pad}{name}"),
|
||||
Term::App { callee, args } => {
|
||||
Term::App { callee, args, .. } => {
|
||||
let mut s = format!("{pad}(");
|
||||
s.push_str(&term_inline(callee));
|
||||
for a in args {
|
||||
@@ -193,7 +193,7 @@ fn term_block(t: &Term, indent: usize) -> String {
|
||||
s.push(')');
|
||||
s
|
||||
}
|
||||
Term::Do { op, args } => {
|
||||
Term::Do { op, args, .. } => {
|
||||
let mut s = format!("{pad}(do {op}");
|
||||
for a in args {
|
||||
s.push(' ');
|
||||
@@ -287,7 +287,7 @@ fn term_inline(t: &Term) -> String {
|
||||
match t {
|
||||
Term::Lit { lit } => lit_to_string(lit),
|
||||
Term::Var { name } => name.clone(),
|
||||
Term::App { callee, args } => {
|
||||
Term::App { callee, args, .. } => {
|
||||
let mut s = String::from("(");
|
||||
s.push_str(&term_inline(callee));
|
||||
for a in args {
|
||||
@@ -297,7 +297,7 @@ fn term_inline(t: &Term) -> String {
|
||||
s.push(')');
|
||||
s
|
||||
}
|
||||
Term::Do { op, args } => {
|
||||
Term::Do { op, args, .. } => {
|
||||
let mut s = format!("(do {op}");
|
||||
for a in args {
|
||||
s.push(' ');
|
||||
@@ -414,6 +414,7 @@ mod tests {
|
||||
Term::Var { name: "a".into() },
|
||||
Term::Var { name: "b".into() },
|
||||
],
|
||||
tail: false,
|
||||
},
|
||||
doc: None,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user