diff --git a/LANGUAGE.md b/LANGUAGE.md index 13e1ca8..7242a5d 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -233,6 +233,24 @@ factorial = n -> Num => n == 0 ? 1 : n * factorial(n - 1) ``` (See `examples/factorial.ql`, `examples/fibonacci.ql`.) +#### Tail self-recursion is optimized to a loop (guaranteed) + +When a function returns a call **to itself in tail position** — i.e. the self-call is +the function's whole result, with nothing left to do to it — the compiler **guarantees** +it is lowered to a loop (the parameters become loop-carried slots and the call becomes a +back-edge jump) instead of a stack-pushing call. So a tail-recursive function runs in +**constant stack** and will not overflow, however deep the recursion: +```quilon +count = (n :: Num, acc :: Num) -> Num => + n == 0 ? acc : count(n - 1, acc + n) ~ the self-call IS the `:` branch → tail position +``` +Tail position flows through the constructs that yield a value directly: `?`/`|` match +arms, `if`/ternary branches, the tail of a `< >` block, and a `|>` pipeline. A self-call +**not** in tail position (e.g. `n * fact(n - 1)`, whose result is multiplied first) stays +ordinary recursion, as does a tail call to a *different* function (general/mutual tail +calls are a later follow-up). This is codegen-only — there is no surface syntax for it. +(See `examples/tail_recursion.ql`, which recurses 1,000,000 deep.) + --- ## Overloading @@ -495,6 +513,7 @@ message instead. Any compile error exits with status 1. | Named record types + methods (`it`) | ✅ | | In-place mutation of `:=` records: field writes (`obj.f := v`) + setter methods | ✅ | | Functions, recursion, blocks, type inference | ✅ | +| Guaranteed self-tail-call optimization (tail self-recursion → loop, constant stack) | ✅ | | Pipe `\|>` (first-arg injection) | ✅ | | `for n <- collection => body` loops | ✅ | | Ranges: infix `lo <- hi` → inclusive `[]Num` (descends when `lo > hi`) | ✅ | diff --git a/examples/tail_recursion.ql b/examples/tail_recursion.ql new file mode 100644 index 0000000..2123717 --- /dev/null +++ b/examples/tail_recursion.ql @@ -0,0 +1,12 @@ +~ Guaranteed self-tail-call optimization: a self-call in tail position is lowered to +~ a loop, so this recurses 1,000,000 deep in constant stack. Without the optimization +~ the same program would overflow the stack and crash. +~ +~ `count` tail-recurses with an accumulator (the recursive call is the whole value of +~ the `:` branch — i.e. it is in tail position), counting `n` down to 0. `acc` cycles +~ through 0..250 so the result stays a small, deterministic exit code without needing +~ `%` on the result: it equals (number of steps) mod 251 = 1_000_000 mod 251 = 16. +count = (n :: Num, acc :: Num) -> Num => + n == 0 ? acc : count(n - 1, acc == 250 ? 0 : acc + 1) + +^ = () -> Num => count(1000000, 0) ~ 1_000_000 mod 251 = 16 diff --git a/src/codegen/generator.rs b/src/codegen/generator.rs index 6e67f4c..1a88a6e 100644 --- a/src/codegen/generator.rs +++ b/src/codegen/generator.rs @@ -147,6 +147,29 @@ pub struct CodeGenerator<'ctx> { // an argument to an overloaded call/operator — keeping codegen dispatch in sync // with the type checker. (Overloaded callees' returns come from `overloads`.) fn_return_types: HashMap, + // Active self-tail-call optimization context for the function currently being + // emitted, set up by `generate_function_decl` only when the body has a self-call in + // tail position. A tail self-call then overwrites the param slots and branches back + // to `loop_header` instead of emitting a stack-growing `call` + `ret` — guaranteeing + // self-tail-recursion runs in constant stack (see `Tco` / `generate_tail_expr`). + tco: Option>, +} + +/// The loop-lowering context for self-tail-call optimization of one function. Present +/// (in `CodeGenerator::tco`) only while emitting a function whose body has at least one +/// self-call in tail position. Classic TCO transform: the body's parameter `=`-bindings +/// become mutable slots (`param_slots`), and a tail self-call stores its argument values +/// into those slots and `br`s back to `header` — turning the recursion into a loop. +struct Tco<'ctx> { + /// The LLVM symbol of the function being optimized (mangled if overloaded). A `Call` + /// is a self-tail-call only if it resolves to exactly this symbol with matching arity. + self_symbol: String, + /// The function's parameter names, in declaration order, paired with the alloca slot + /// each is stored in. A tail self-call recomputes the args and rewrites these slots. + param_slots: Vec<(String, PointerValue<'ctx>)>, + /// The loop header — the block a tail self-call branches back to. Positioned right + /// after the parameter slots are (re)loaded into the `variables` map for the body. + header: inkwell::basic_block::BasicBlock<'ctx>, } /// Codegen-side view of the type checker's [`TypeTable`] — the "type oracle". @@ -215,6 +238,7 @@ impl<'ctx> CodeGenerator<'ctx> { overloads: HashMap::new(), var_types: HashMap::new(), fn_return_types: HashMap::new(), + tco: None, }; codegen.register_builtin_sum_types(); codegen @@ -759,8 +783,43 @@ impl<'ctx> CodeGenerator<'ctx> { self.var_types.insert(param.name.clone(), qty); } - // Generate function body - let body_value = self.generate_expr(&decl.body)?; + // Guaranteed self-tail-call optimization: if the body returns a call to THIS + // function in tail position, lower the recursion to a loop instead of a + // stack-growing `call` + `ret`. Set up a loop header (branched to from the entry + // block, after the param slots are populated) and a TCO context; a tail self-call + // then rewrites the param slots and `br`s back here. The param allocas created + // above are reused as the loop's mutable slots — there is no separate IR shape for + // recursive vs. non-recursive functions beyond this header + the back-edge. + let body_value = if self.body_has_self_tail_call(decl) { + let param_slots: Vec<(String, PointerValue<'ctx>)> = decl + .params + .iter() + .map(|p| (p.name.clone(), self.variables[&p.name].0)) + .collect(); + let header = self.context.append_basic_block(function, "tco_loop"); + self.builder + .build_unconditional_branch(header) + .map_err(|e| format!("Failed to build branch to loop header: {:?}", e))?; + self.builder.position_at_end(header); + self.tco = Some(Tco { + self_symbol: symbol.clone(), + param_slots, + header, + }); + // Emit the body in tail-aware mode. A `None` result means every tail exit was a + // self-call (e.g. an unconditional `f(...)` body, or a match all of whose arms + // tail-recurse): the function never falls through to a normal return, and + // `generate_tail_expr` has already terminated the current block (with the + // back-edge `br`, or an `unreachable`). In that case there is no `ret` to emit. + let result = self.generate_tail_expr(&decl.body)?; + self.tco = None; + match result { + Some(v) => v, + None => return Ok(()), + } + } else { + self.generate_expr(&decl.body)? + }; // Entry point `^`: if the body's value isn't a Num (f64) — e.g. a side-effecting // main ending in a Text/Bool/record expression — discard it and implicitly @@ -779,6 +838,350 @@ impl<'ctx> CodeGenerator<'ctx> { Ok(()) } + // ---- Self-tail-call optimization (loop lowering) -------------------------------- + // + // A call is in **tail position** when it is the value the enclosing function returns + // directly — i.e. nothing happens to its result before the `ret`. Tail position flows + // through exactly the constructs that yield a value as their tail without further + // computation: a block's last expression, both arms of an `if`/ternary, every arm of a + // `?`/`|` match, a parenthesizing pipeline's desugaring, and the function body itself. + // It does NOT flow into the operand of a `+`/`*`/comparison, a call argument, an array + // element, etc. — those consume the value, so a call there is not in tail position. + // + // `body_has_self_tail_call` is the pure analysis (no IR), used once to decide whether + // to set up the loop. `generate_tail_expr` is the codegen counterpart: it walks the + // SAME tail-position structure and, at a tail self-call, rewrites the param slots and + // branches to the loop header; everything else (and every non-tail subexpression) goes + // through the ordinary `generate_expr`. The two must agree on what "tail position" is. + + /// Does `decl`'s body contain a self-call in tail position? Pure (emits no IR). + fn body_has_self_tail_call(&self, decl: &FunctionDecl) -> bool { + // Determine the symbol this function is/will be emitted under, so a tail call can + // be recognized as a SELF-call (matching name + arity, and — when overloaded — the + // exact mangled member). This mirrors the symbol chosen in `generate_function_decl`. + let self_symbol = if self.overloads.contains_key(&decl.name) { + let params: Vec = decl + .params + .iter() + .map(|p| p.type_annotation.clone().unwrap_or(Type::Num)) + .collect(); + mangle_overload(&decl.name, ¶ms) + } else { + decl.name.clone() + }; + self.expr_has_self_tail_call(&decl.body, &self_symbol, decl.params.len()) + } + + /// Whether `expr`, evaluated in tail position, contains a self-call (to `self_symbol` + /// with `arity` args). Recurses only through tail-position sub-expressions. + fn expr_has_self_tail_call(&self, expr: &Expr, self_symbol: &str, arity: usize) -> bool { + match expr { + Expr::Call { .. } => self.is_self_tail_call(expr, self_symbol, arity), + Expr::Block { stmts, .. } => match stmts.last() { + Some(crate::ast::Statement::Expr(tail)) => { + self.expr_has_self_tail_call(tail, self_symbol, arity) + } + _ => false, + }, + Expr::If { then, else_, .. } => { + self.expr_has_self_tail_call(then, self_symbol, arity) + || self.expr_has_self_tail_call(else_, self_symbol, arity) + } + Expr::Match { arms, .. } => arms + .iter() + .any(|arm| self.expr_has_self_tail_call(&arm.body, self_symbol, arity)), + // A pipeline desugars to a call; check the call it becomes. + Expr::Pipeline { left, right, span } => { + let call = Expr::desugar_pipeline(left, right, span); + self.is_self_tail_call(&call, self_symbol, arity) + } + _ => false, + } + } + + /// Whether `expr` is a direct call that resolves to `self_symbol` with `arity` args — + /// i.e. the function calling itself. Resolution mirrors `generate_call`'s: a plain + /// name maps to itself, an overloaded name to its exact mangled member by argument + /// types. A constructor/method/intrinsic call (which `generate_call` routes elsewhere) + /// is never a self-call. NB only the *callee identity* matters here; the arguments are + /// generated normally by `generate_tail_expr`. + fn is_self_tail_call(&self, expr: &Expr, self_symbol: &str, arity: usize) -> bool { + let Expr::Call { func, args, .. } = expr else { + return false; + }; + let Expr::Ident { name, .. } = func.as_ref() else { + return false; + }; + if args.len() != arity { + return false; + } + // A name shadowed by a sum-type constructor or an intrinsic is not a self-call. + if self.sum_variants.contains_key(name.as_str()) + || matches!(name.as_str(), "print" | "eprint" | "write") + { + return false; + } + let symbol = if self.overloads.contains_key(name.as_str()) { + let arg_types: Vec = args.iter().map(|a| self.infer_type(a)).collect(); + match self.resolve_overload_symbol(name, &arg_types) { + Some(s) => s, + None => return false, + } + } else { + name.clone() + }; + symbol == self_symbol + } + + /// Emit `expr` in tail position under an active [`Tco`] context. Returns `Some(value)` + /// for an ordinary tail (the caller `ret`s it) or `None` when this path does not fall + /// through to a normal return — every tail exit was a self-call. **Invariant:** on + /// `None`, the current insert block is already TERMINATED (by the back-edge `br` of a + /// tail self-call, or an `unreachable` for an if/match all of whose arms recurse), so + /// the caller must not emit anything more into it. Walks the same tail-position + /// structure as `expr_has_self_tail_call`; any non-tail node falls through to + /// `generate_expr` (always `Some`). + fn generate_tail_expr(&mut self, expr: &Expr) -> Result>, String> { + let tco = self + .tco + .as_ref() + .expect("generate_tail_expr without a TCO context"); + let self_symbol = tco.self_symbol.clone(); + let arity = tco.param_slots.len(); + + match expr { + // A pipeline in tail position is its desugared call; lower that. + Expr::Pipeline { left, right, span } => { + let call = Expr::desugar_pipeline(left, right, span); + self.generate_tail_expr(&call) + } + + Expr::Call { args, .. } if self.is_self_tail_call(expr, &self_symbol, arity) => { + self.emit_tail_self_call(args)?; + Ok(None) + } + + Expr::Block { stmts, .. } => { + // Emit every statement normally except the tail expression, which stays in + // tail position. A non-`Expr`-tail block (ends in an item) has no tail call + // (the analysis returned false), so generating it whole is correct. + match stmts.split_last() { + Some((crate::ast::Statement::Expr(tail), init)) => { + for stmt in init { + match stmt { + crate::ast::Statement::Item(item) => self.generate_item(item)?, + crate::ast::Statement::Expr(e) => { + self.generate_expr(e)?; + } + } + } + self.generate_tail_expr(tail) + } + _ => Ok(Some(self.generate_block(stmts)?)), + } + } + + Expr::If { + cond, then, else_, .. + } => self.generate_tail_if(cond, then, else_), + + Expr::Match { + expr: scrutinee, + arms, + .. + } => self.generate_tail_match(expr, scrutinee, arms), + + // Anything else in tail position is an ordinary value. + other => Ok(Some(self.generate_expr(other)?)), + } + } + + /// Lower a tail self-call: evaluate the argument expressions, write them into the + /// parameter slots, then `br` back to the loop header. All args are evaluated into + /// temporaries BEFORE any slot is overwritten, so an argument that reads a parameter + /// (e.g. `f(n - 1, acc + n)` reading `n` for `acc`) sees the current iteration's + /// values, not a half-updated set. + fn emit_tail_self_call(&mut self, args: &[Expr]) -> Result<(), String> { + let new_vals: Vec> = args + .iter() + .map(|a| self.generate_expr(a)) + .collect::, _>>()?; + // Snapshot slots + header before the mutable stores (releases the `self.tco` + // borrow so the `&mut self` builder calls below are allowed). + let tco = self + .tco + .as_ref() + .expect("emit_tail_self_call without a TCO context"); + let slots: Vec> = tco.param_slots.iter().map(|(_, ptr)| *ptr).collect(); + let header = tco.header; + for (slot, val) in slots.iter().zip(new_vals) { + self.builder + .build_store(*slot, val) + .map_err(|e| format!("Failed to store tail-call arg: {:?}", e))?; + } + self.builder + .build_unconditional_branch(header) + .map_err(|e| format!("Failed to branch to loop header: {:?}", e))?; + Ok(()) + } + + /// Tail-position `if`/ternary: emit each arm in tail position. An arm that tail-recurses + /// branches to the loop header (yields no value); an arm that produces a value branches + /// to a merge block. We `phi` only over the value-producing arms — if both arms tail + /// self-call, there is no merge value and we return `None`. + fn generate_tail_if( + &mut self, + cond: &Expr, + then_expr: &Expr, + else_expr: &Expr, + ) -> Result>, String> { + let cond_val = self.generate_expr(cond)?; + let BasicValueEnum::IntValue(cond_bool) = cond_val else { + return Err("Condition must be a boolean".to_string()); + }; + let function = self + .current_function + .ok_or_else(|| "If expression outside of function".to_string())?; + + let then_bb = self.context.append_basic_block(function, "then"); + let else_bb = self.context.append_basic_block(function, "else"); + let merge_bb = self.context.append_basic_block(function, "ifcont"); + + self.builder + .build_conditional_branch(cond_bool, then_bb, else_bb) + .map_err(|e| format!("Failed to build conditional branch: {:?}", e))?; + + // Collect each non-tail-recursing arm's (value, originating block) for the phi. + let mut incoming: Vec<(BasicValueEnum<'ctx>, inkwell::basic_block::BasicBlock<'ctx>)> = + Vec::new(); + + self.builder.position_at_end(then_bb); + if let Some(v) = self.generate_tail_expr(then_expr)? { + let bb = self.builder.get_insert_block().unwrap(); + self.builder + .build_unconditional_branch(merge_bb) + .map_err(|e| format!("Failed to build branch: {:?}", e))?; + incoming.push((v, bb)); + } + + self.builder.position_at_end(else_bb); + if let Some(v) = self.generate_tail_expr(else_expr)? { + let bb = self.builder.get_insert_block().unwrap(); + self.builder + .build_unconditional_branch(merge_bb) + .map_err(|e| format!("Failed to build branch: {:?}", e))?; + incoming.push((v, bb)); + } + + self.builder.position_at_end(merge_bb); + match incoming.as_slice() { + // Both arms tail-recursed: control never reaches the merge block. Terminate it + // as `unreachable` (it has no value-producing predecessors) and report `None` + // — every `None` from a tail node leaves the current block already terminated. + [] => { + self.builder + .build_unreachable() + .map_err(|e| format!("Failed to build unreachable: {:?}", e))?; + Ok(None) + } + _ => { + let phi = self + .builder + .build_phi(incoming[0].0.get_type(), "iftmp") + .map_err(|e| format!("Failed to build phi: {:?}", e))?; + for (v, bb) in &incoming { + phi.add_incoming(&[(v as &dyn BasicValue, *bb)]); + } + Ok(Some(phi.as_basic_value())) + } + } + } + + /// Tail-position `?`/`|` match: same shape as `generate_match`, but each arm body is + /// emitted in tail position. An arm that tail-recurses branches to the loop header and + /// stores nothing; an arm that yields a value stores it into the shared result slot and + /// falls through to the continuation. If EVERY arm tail-recurses, the continuation is + /// unreachable and we return `None` (no result to load). + fn generate_tail_match( + &mut self, + match_expr: &Expr, + scrutinee: &Expr, + arms: &[MatchArm], + ) -> Result>, String> { + let match_val = self.generate_expr(scrutinee)?; + let function = self + .current_function + .ok_or_else(|| "Match expression must be in a function".to_string())?; + + let mut arm_blocks = vec![]; + let mut check_blocks = vec![]; + for i in 0..arms.len() { + check_blocks.push( + self.context + .append_basic_block(function, &format!("check_{}", i)), + ); + arm_blocks.push( + self.context + .append_basic_block(function, &format!("arm_{}", i)), + ); + } + let cont_block = self.context.append_basic_block(function, "match_cont"); + + // Result slot for the value-producing (non-tail-recursing) arms, sized from the + // oracle exactly as `generate_match` does. Only written by arms that yield a value. + let result_llvm = self.oracle_value_type(match_expr)?; + let result_alloca = self.create_entry_block_alloca("match_result", result_llvm)?; + + self.builder + .build_unconditional_branch(check_blocks[0]) + .map_err(|e| format!("Failed to build branch: {:?}", e))?; + + let mut any_value_arm = false; + for (i, arm) in arms.iter().enumerate() { + self.builder.position_at_end(check_blocks[i]); + let matches = self.check_pattern(&arm.pattern, match_val)?; + let next_block = if i + 1 < check_blocks.len() { + check_blocks[i + 1] + } else { + cont_block + }; + self.builder + .build_conditional_branch(matches, arm_blocks[i], next_block) + .map_err(|e| format!("Failed to build conditional branch: {:?}", e))?; + + self.builder.position_at_end(arm_blocks[i]); + self.bind_pattern(&arm.pattern, match_val)?; + if let Some(arm_val) = self.generate_tail_expr(&arm.body)? { + any_value_arm = true; + self.builder + .build_store(result_alloca, arm_val) + .map_err(|e| format!("Failed to store result: {:?}", e))?; + self.builder + .build_unconditional_branch(cont_block) + .map_err(|e| format!("Failed to build branch: {:?}", e))?; + } + // Else: the arm tail-recursed and already branched to the loop header. + } + + self.builder.position_at_end(cont_block); + if any_value_arm { + Ok(Some( + self.builder + .build_load(result_llvm, result_alloca, "match_result") + .map_err(|e| format!("Failed to load result: {:?}", e))?, + )) + } else { + // Every arm tail-recursed: control never produces a value here (the only edge + // into `cont_block` is the last check's no-match fallthrough, which an + // exhaustive match never takes). Terminate it as `unreachable` and report + // `None` — keeping the "a `None` leaves the block terminated" invariant. + self.builder + .build_unreachable() + .map_err(|e| format!("Failed to build unreachable: {:?}", e))?; + Ok(None) + } + } + fn create_entry_block_alloca( &self, name: &str, diff --git a/tests/examples_test.rs b/tests/examples_test.rs index 52b351e..8819c94 100644 --- a/tests/examples_test.rs +++ b/tests/examples_test.rs @@ -58,6 +58,9 @@ const EXPECTED_EXIT: &[(&str, i32)] = &[ ("use_module.ql", 5), ("unit.ql", 0), ("overloading.ql", 161), + // Recurses 1_000_000 deep; only terminates because self-tail-recursion is lowered + // to a loop (guaranteed TCO). 1_000_000 mod 251 = 16. + ("tail_recursion.ql", 16), ]; fn ql_files() -> Vec { diff --git a/tests/tail_call_test.rs b/tests/tail_call_test.rs new file mode 100644 index 0000000..ae42305 --- /dev/null +++ b/tests/tail_call_test.rs @@ -0,0 +1,193 @@ +//! Guaranteed self-tail-call optimization (M3): a function that returns a call to +//! ITSELF in tail position is lowered to a loop, so deep self-recursion runs in +//! constant stack instead of overflowing it. +//! +//! These tests drive the full pipeline (lex -> parse -> typecheck -> codegen -> JIT) +//! and assert the program's real exit code. The depth-1_000_000 cases are the +//! load-bearing ones: WITHOUT the optimization they recurse a million frames deep and +//! crash with a stack overflow; the fact that they return a deterministic value at all +//! is the guarantee. The remaining cases verify the transform preserves semantics for +//! tail calls reached through `?`/`|` match arms and nested `< >` blocks (the subtle +//! part), and that non-tail self-calls are left as ordinary recursion. + +use quilon::jit; +use quilon::lexer::Lexer; +use quilon::parser; +use quilon::typechecker::TypeChecker; +use std::sync::Mutex; + +// LLVM's JIT / native-target init isn't thread-safe; cargo runs tests in parallel. +static JIT_LOCK: Mutex<()> = Mutex::new(()); + +/// Compile and run `src`, asserting the entry point yields `expected`. +fn assert_exit(src: &str, expected: i32) { + let _guard = JIT_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let tokens = Lexer::tokenize(src).expect("lexing failed"); + let program = parser::parse(&tokens).expect("parsing failed"); + TypeChecker::new() + .check_program(&program) + .expect("type checking failed"); + let code = jit::run_program(&program).expect("execution failed"); + assert_eq!(code, expected, "unexpected exit code for source:\n{}", src); +} + +/// Assert the generated IR for `src` contains no recursive `call` to `callee` — i.e. +/// the self-recursion was lowered to a loop (a back-edge branch), not a stack call. +/// (The IR still names the function in its `define` line and at the initial call from +/// `^`; we check there is no `call @callee(` inside `callee` itself by counting: +/// the only legitimate `call ... @callee(` is the one in `^`.) +fn assert_no_self_call(src: &str, callee: &str) { + let context = inkwell::context::Context::create(); + let tokens = Lexer::tokenize(src).expect("lexing failed"); + let program = parser::parse(&tokens).expect("parsing failed"); + let mut codegen = quilon::codegen::CodeGenerator::with_oracle(&context, "test", &program) + .expect("oracle setup failed"); + let ir = codegen.generate(&program).expect("codegen failed"); + let self_calls = ir.matches(&format!("@{}(", callee)).count(); + // Exactly one mention with `(` — the initial call from `^`; the `define` line uses + // `@callee(` too, so allow up to 2 (define + initial call), but NONE may be a + // back-edge recursive call. We assert the loop header is present as the positive + // signal that the transform fired. + assert!( + ir.contains("tco_loop"), + "expected a TCO loop header for {callee}; IR:\n{ir}" + ); + // The recursive body must contain a back-edge branch to the loop header rather than + // a self `call`. The `define` + single `^` call are the only `@callee(` occurrences. + assert!( + self_calls <= 2, + "expected no recursive self-call to {callee} (found {self_calls} occurrences); IR:\n{ir}" + ); +} + +/// Assert `src` compiles AND the generated module passes LLVM verification (codegen runs +/// `verify` internally and surfaces a failure as an `Err`). Used for programs that would +/// loop forever if run, but whose IR shape we still want to guard. +fn assert_compiles_clean(src: &str) { + let context = inkwell::context::Context::create(); + let tokens = Lexer::tokenize(src).expect("lexing failed"); + let program = parser::parse(&tokens).expect("parsing failed"); + let mut codegen = quilon::codegen::CodeGenerator::with_oracle(&context, "test", &program) + .expect("oracle setup failed"); + codegen + .generate(&program) + .expect("codegen / module verification failed"); +} + +/// The headline guarantee: a ternary tail self-call recursing 1_000_000 deep returns +/// (does not overflow the stack) and computes the right value. `acc` cycles 0..250 so +/// the result is (steps) mod 251 = 1_000_000 mod 251 = 16. +#[test] +fn deep_ternary_tail_recursion_does_not_overflow() { + assert_exit( + "count = (n :: Num, acc :: Num) -> Num => \ + n == 0 ? acc : count(n - 1, acc == 250 ? 0 : acc + 1)\n\ + ^ = () -> Num => count(1000000, 0)", + 16, + ); +} + +/// The same depth, but the tail self-call is reached through a `?`/`|` match arm — the +/// subtle path (the recursion lives in an arm body, not a ternary). +#[test] +fn deep_match_arm_tail_recursion_does_not_overflow() { + assert_exit( + "count = (n :: Num, acc :: Num) -> Num => n ? \ + | 0 => acc \ + | _ => count(n - 1, acc == 250 ? 0 : acc + 1)\n\ + ^ = () -> Num => count(1000000, 0)", + 16, + ); +} + +/// The tail self-call is the tail expression of the function body `< >` block (so tail +/// position flows through the block to the ternary and into the call). Still must run in +/// constant stack. (A `< >` block can't start mid-expression, so the block is the whole +/// body and its tail is the recursing ternary — exercising the block-tail path.) +#[test] +fn deep_block_tail_recursion_does_not_overflow() { + assert_exit( + "count = (n :: Num, acc :: Num) -> Num => <\n\ + next = acc == 250 ? 0 : acc + 1\n\ + n == 0 ? acc : count(n - 1, next)\n\ + >\n\ + ^ = () -> Num => count(1000000, 0)", + 16, + ); +} + +/// A simple tail countdown returning the accumulator, checked for a small deterministic +/// value (10 steps, +3 each) AND that codegen actually emitted the loop (no self-call). +#[test] +fn tail_recursion_computes_correct_value_and_loops() { + let src = "sum = (n :: Num, acc :: Num) -> Num => n == 0 ? acc : sum(n - 1, acc + 3)\n\ + ^ = () -> Num => sum(10, 0)"; + assert_exit(src, 30); + assert_no_self_call(src, "sum"); +} + +/// A NON-tail self-call (the result is multiplied before returning) must NOT be loop- +/// lowered — it stays ordinary recursion and still computes correctly. factorial(5)=120. +#[test] +fn non_tail_recursion_still_works() { + assert_exit( + "fact = (n :: Num) -> Num => n <= 1 ? 1 : n * fact(n - 1)\n\ + ^ = () -> Num => fact(5)", + 120, + ); +} + +/// Mutual / cross-function tail calls are explicitly OUT of scope for this milestone +/// (only SELF-tail-calls are optimized). A call to ANOTHER function in tail position +/// must remain a normal call and still compute the right value. +#[test] +fn tail_call_to_other_function_is_normal_call() { + assert_exit( + "twice = (n :: Num) -> Num => n + n\n\ + go = (n :: Num) -> Num => twice(n)\n\ + ^ = () -> Num => go(21)", + 42, + ); +} + +/// Codegen-only: a body that is an UNCONDITIONAL tail self-call (`f(...)` with no base +/// case) must still produce a verifiable module — the back-edge `br` terminates the loop +/// block and no spurious `ret` is appended after it. (Running it would loop forever, so +/// we only check it compiles + verifies.) Guards a real double-terminator bug. +#[test] +fn unconditional_tail_self_call_verifies() { + assert_compiles_clean("loop = (n :: Num) -> Num => loop(n)\n^ = () -> Num => loop(5)"); +} + +/// Codegen-only: a match all of whose arms tail-recurse leaves no value-producing path, +/// so the continuation block must be terminated as `unreachable` (not left dangling). +#[test] +fn all_match_arms_recurse_verifies() { + assert_compiles_clean( + "spin = (n :: Num) -> Num => n ? | 0 => spin(0) | _ => spin(n - 1)\n\ + ^ = () -> Num => spin(3)", + ); +} + +/// Codegen-only: an `if`/ternary both of whose arms tail-recurse — the merge block has no +/// value-producing predecessor and must be terminated as `unreachable`. +#[test] +fn all_if_arms_recurse_verifies() { + assert_compiles_clean( + "spin = (n :: Num) -> Num => n == 0 ? spin(1) : spin(n - 1)\n\ + ^ = () -> Num => spin(3)", + ); +} + +/// A tail self-call evaluates ALL its arguments against the CURRENT iteration's params +/// before overwriting any slot. Here the second arg reads `n`, which the first arg also +/// rebinds — a naive in-order overwrite would corrupt it. Sum 1..5 with a swap-style +/// dependency: count(5, 0) -> ... -> 15. +#[test] +fn tail_call_args_use_pre_update_param_values() { + assert_exit( + "count = (n :: Num, acc :: Num) -> Num => n == 0 ? acc : count(n - 1, acc + n)\n\ + ^ = () -> Num => count(5, 0)", + 15, + ); +}