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:
@@ -34,18 +34,20 @@
|
||||
//! effects-clause::= "(" "effects" ident+ ")"
|
||||
//!
|
||||
//! term ::= var-ref | int-lit | str-lit | bool-lit | unit-lit
|
||||
//! | app-term | match-term | ctor-term | do-term | seq-term
|
||||
//! | lam-term | let-term
|
||||
//! | app-term | tail-app-term | match-term | ctor-term
|
||||
//! | do-term | tail-do-term | seq-term | lam-term | let-term
|
||||
//! var-ref ::= ident ; reserved: true/false → bool-lit
|
||||
//! int-lit ::= integer ; numeric atom
|
||||
//! str-lit ::= string ; string atom
|
||||
//! bool-lit ::= "true" | "false"
|
||||
//! unit-lit ::= "(" "lit-unit" ")"
|
||||
//! app-term ::= "(" "app" term term+ ")"
|
||||
//! tail-app-term ::= "(" "tail-app" term term+ ")" ; Iter 14e
|
||||
//! ctor-term ::= "(" "term-ctor" ident ident term* ")"
|
||||
//! match-term ::= "(" "match" term case-arm+ ")"
|
||||
//! case-arm ::= "(" "case" pattern term ")"
|
||||
//! do-term ::= "(" "do" ident term* ")"
|
||||
//! tail-do-term ::= "(" "tail-do" ident term* ")" ; Iter 14e
|
||||
//! seq-term ::= "(" "seq" term term ")"
|
||||
//! lam-term ::= "(" "lam" "(" "params" typed-param* ")"
|
||||
//! "(" "ret" type ")"
|
||||
@@ -677,9 +679,11 @@ impl<'a> Parser<'a> {
|
||||
match head {
|
||||
"lit-unit" => self.parse_lit_unit(),
|
||||
"app" => self.parse_app(),
|
||||
"tail-app" => self.parse_tail_app(),
|
||||
"term-ctor" => self.parse_term_ctor(),
|
||||
"match" => self.parse_match(),
|
||||
"do" => self.parse_do(),
|
||||
"tail-do" => self.parse_tail_do(),
|
||||
"seq" => self.parse_seq(),
|
||||
"lam" => self.parse_lam(),
|
||||
"let" => self.parse_let(),
|
||||
@@ -689,8 +693,8 @@ impl<'a> Parser<'a> {
|
||||
production: "term",
|
||||
message: format!(
|
||||
"unknown term head `{other}`; expected one of \
|
||||
`app`, `lam`, `let`, `match`, `do`, `seq`, \
|
||||
`term-ctor`, `lit-unit`"
|
||||
`app`, `tail-app`, `lam`, `let`, `match`, `do`, \
|
||||
`tail-do`, `seq`, `term-ctor`, `lit-unit`"
|
||||
),
|
||||
pos,
|
||||
})
|
||||
@@ -736,12 +740,32 @@ impl<'a> Parser<'a> {
|
||||
fn parse_app(&mut self) -> Result<Term, ParseError> {
|
||||
self.expect_lparen("app-term")?;
|
||||
self.expect_keyword("app")?;
|
||||
self.parse_app_body(false, "app-term")
|
||||
}
|
||||
|
||||
/// Iter 14e: `(tail-app callee arg+)` — same shape as `app` but
|
||||
/// constructs `Term::App { tail: true, .. }`. The typechecker's
|
||||
/// tail-position pass verifies that the call really is in tail
|
||||
/// position; an unmarked call in tail position is also legal.
|
||||
fn parse_tail_app(&mut self) -> Result<Term, ParseError> {
|
||||
self.expect_lparen("tail-app-term")?;
|
||||
self.expect_keyword("tail-app")?;
|
||||
self.parse_app_body(true, "tail-app-term")
|
||||
}
|
||||
|
||||
/// Body shared by [`Self::parse_app`] and [`Self::parse_tail_app`]:
|
||||
/// callee + 1+ args + closing `)`.
|
||||
fn parse_app_body(
|
||||
&mut self,
|
||||
tail: bool,
|
||||
production: &'static str,
|
||||
) -> Result<Term, ParseError> {
|
||||
let callee = self.parse_term()?;
|
||||
// 1+ args
|
||||
if matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
|
||||
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
||||
return Err(ParseError::Production {
|
||||
production: "app-term",
|
||||
production,
|
||||
message: "expected at least one argument".into(),
|
||||
pos,
|
||||
});
|
||||
@@ -750,10 +774,11 @@ impl<'a> Parser<'a> {
|
||||
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
|
||||
args.push(self.parse_term()?);
|
||||
}
|
||||
self.expect_rparen("app-term")?;
|
||||
self.expect_rparen(production)?;
|
||||
Ok(Term::App {
|
||||
callee: Box::new(callee),
|
||||
args,
|
||||
tail,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -809,13 +834,30 @@ impl<'a> Parser<'a> {
|
||||
fn parse_do(&mut self) -> Result<Term, ParseError> {
|
||||
self.expect_lparen("do-term")?;
|
||||
self.expect_keyword("do")?;
|
||||
self.parse_do_body(false, "do-term")
|
||||
}
|
||||
|
||||
/// Iter 14e: `(tail-do op arg*)` — same shape as `do` but
|
||||
/// constructs `Term::Do { tail: true, .. }`.
|
||||
fn parse_tail_do(&mut self) -> Result<Term, ParseError> {
|
||||
self.expect_lparen("tail-do-term")?;
|
||||
self.expect_keyword("tail-do")?;
|
||||
self.parse_do_body(true, "tail-do-term")
|
||||
}
|
||||
|
||||
/// Body shared by [`Self::parse_do`] and [`Self::parse_tail_do`].
|
||||
fn parse_do_body(
|
||||
&mut self,
|
||||
tail: bool,
|
||||
production: &'static str,
|
||||
) -> Result<Term, ParseError> {
|
||||
let op = self.expect_ident("effect op (e.g. `io/print_int`)")?;
|
||||
let mut args = Vec::new();
|
||||
while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) {
|
||||
args.push(self.parse_term()?);
|
||||
}
|
||||
self.expect_rparen("do-term")?;
|
||||
Ok(Term::Do { op, args })
|
||||
self.expect_rparen(production)?;
|
||||
Ok(Term::Do { op, args, tail })
|
||||
}
|
||||
|
||||
fn parse_seq(&mut self) -> Result<Term, ParseError> {
|
||||
|
||||
@@ -223,8 +223,12 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
|
||||
match t {
|
||||
Term::Lit { lit } => write_lit(out, lit),
|
||||
Term::Var { name } => out.push_str(name),
|
||||
Term::App { callee, args } => {
|
||||
out.push_str("(app ");
|
||||
Term::App { callee, args, tail } => {
|
||||
// Iter 14e: `tail-app` is the form for `App { tail: true }`;
|
||||
// otherwise the regular `app` head is used. Both productions
|
||||
// are positional analogues of each other — only the head
|
||||
// keyword differs.
|
||||
out.push_str(if *tail { "(tail-app " } else { "(app " });
|
||||
write_term(out, callee, level);
|
||||
for a in args {
|
||||
out.push(' ');
|
||||
@@ -241,8 +245,9 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
|
||||
write_term(out, body, level);
|
||||
out.push(')');
|
||||
}
|
||||
Term::Do { op, args } => {
|
||||
out.push_str("(do ");
|
||||
Term::Do { op, args, tail } => {
|
||||
// Iter 14e: `tail-do` mirrors `tail-app` for effect ops.
|
||||
out.push_str(if *tail { "(tail-do " } else { "(do " });
|
||||
out.push_str(op);
|
||||
for a in args {
|
||||
out.push(' ');
|
||||
|
||||
Reference in New Issue
Block a user