diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index 6e1b859..d7566f0 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -878,11 +878,6 @@ fn walk_term( scope.remove(name); } } - Term::If { cond, then, else_ } => { - walk_term(cond, out, builtins, scope); - walk_term(then, out, builtins, scope); - walk_term(else_, out, builtins, scope); - } Term::Do { op, args } => { // Mark effect ops as `effect:io/print_int` so they can be // separated from normal function calls. diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 42a23f4..8dcc49e 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -174,8 +174,11 @@ fn diff_detects_changed_def() { let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let src_a = workspace.join("examples").join("sum.ail.json"); - // Variant: load sum.ail.json, mutate the `then` branch (1 instead of 0) - // in the `sum` definition. `main` stays bit-identical. + // Variant: load sum.ail.json, mutate the true-arm body of the + // top-level `match` in `sum` (literal 0 → literal 1). `main` stays + // bit-identical. After Iter 14d the body is `Match` (not `If`); the + // first arm is the `(lit-bool true)` arm, formerly the `then` + // branch. let raw = std::fs::read(&src_a).expect("read sum.ail.json"); let mut module: serde_json::Value = serde_json::from_slice(&raw).expect("parse sum.ail.json"); { @@ -185,12 +188,11 @@ fn diff_detects_changed_def() { .expect("defs array"); for def in defs.iter_mut() { if def.get("name").and_then(|n| n.as_str()) == Some("sum") { - // Replace the then branch literal 0 → literal 1. - let new_then = serde_json::json!({ + let new_body = serde_json::json!({ "t": "lit", "lit": { "kind": "int", "value": 1 } }); - def["body"]["then"] = new_then; + def["body"]["arms"][0]["body"] = new_body; } } } diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 9c8b47b..2576a45 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -1035,14 +1035,6 @@ fn synth( } Ok(r) } - Term::If { cond, then, else_ } => { - let c = synth(cond, env, locals, effects, in_def, subst, counter)?; - unify(&Type::bool_(), &c, subst)?; - let t1 = synth(then, env, locals, effects, in_def, subst, counter)?; - let t2 = synth(else_, env, locals, effects, in_def, subst, counter)?; - unify(&t1, &t2, subst)?; - Ok(subst.apply(&t1)) - } Term::Do { op, args } => { let sig = env .effect_ops @@ -1618,8 +1610,13 @@ mod tests { check(&m).expect("wildcard must satisfy exhaustiveness"); } + /// Iter 14d: `Term::If` was removed in favour of `Term::Match` on + /// `Bool`. Mismatched arm types in the migration shape (lit-bool + /// `true` arm + `wild` fallback arm) must still surface as a + /// type-mismatch diagnostic — the unification across arms is what + /// the old `if_branches_must_match` test guarded. #[test] - fn if_branches_must_match() { + fn match_arms_must_unify() { let m = Module { schema: SCHEMA.into(), name: "t".into(), @@ -1632,14 +1629,24 @@ mod tests { effects: vec![], }, vec![], - Term::If { - cond: Box::new(Term::Lit { + Term::Match { + scrutinee: Box::new(Term::Lit { lit: Literal::Bool { value: true }, }), - then: Box::new(Term::Lit { - lit: Literal::Int { value: 1 }, - }), - else_: Box::new(Term::Lit { lit: Literal::Unit }), + arms: vec![ + Arm { + pat: Pattern::Lit { + lit: Literal::Bool { value: true }, + }, + body: Term::Lit { + lit: Literal::Int { value: 1 }, + }, + }, + Arm { + pat: Pattern::Wild, + body: Term::Lit { lit: Literal::Unit }, + }, + ], }, )], }; diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index c30c2f3..ea3a85f 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -801,64 +801,6 @@ impl<'a> Emitter<'a> { self.locals.pop(); r } - Term::If { cond, then, else_ } => { - let (cond_v, cond_ty) = self.lower_term(cond)?; - if cond_ty != "i1" { - return Err(CodegenError::Internal(format!( - "if cond not i1: {cond_ty}" - ))); - } - let id = self.fresh_id(); - let then_lbl = format!("then.{id}"); - let else_lbl = format!("else.{id}"); - let join_lbl = format!("join.{id}"); - - self.body.push_str(&format!( - " br i1 {cond_v}, label %{then_lbl}, label %{else_lbl}\n" - )); - - self.start_block(&then_lbl); - let (then_v, then_ty) = self.lower_term(then)?; - // Nested code in the `then` body may have changed the - // block label — phi must see the last actual block. - let then_block_end = self.current_block.clone(); - self.body.push_str(&format!(" br label %{join_lbl}\n")); - - self.start_block(&else_lbl); - let (else_v, else_ty) = self.lower_term(else_)?; - if then_ty != else_ty { - return Err(CodegenError::Internal(format!( - "if branches type mismatch: {then_ty} vs {else_ty}" - ))); - } - let else_block_end = self.current_block.clone(); - self.body.push_str(&format!(" br label %{join_lbl}\n")); - - self.start_block(&join_lbl); - let phi = self.fresh_ssa(); - self.body.push_str(&format!( - " {phi} = phi {ty} [ {tv}, %{tlbl} ], [ {ev}, %{elbl} ]\n", - ty = then_ty, - tv = then_v, - tlbl = then_block_end, - ev = else_v, - elbl = else_block_end, - )); - // Iter 7: if both branches yield the same fn-pointer sig, - // forward it to the phi SSA so subsequent indirect calls - // can resolve. - if then_ty == "ptr" { - if let (Some(ts), Some(es)) = - (self.ssa_fn_sigs.get(&then_v), self.ssa_fn_sigs.get(&else_v)) - { - if ts.params == es.params && ts.ret == es.ret { - let merged = ts.clone(); - self.ssa_fn_sigs.insert(phi.clone(), merged); - } - } - } - Ok((phi, then_ty)) - } Term::App { callee, args } => { // Direct call when the callee is a `Var` referring to a // statically-known target (builtin, current-module fn, @@ -999,6 +941,16 @@ impl<'a> Emitter<'a> { ) -> Result<(String, String)> { let s_ail = self.synth_arg_type(scrutinee)?; let (s_val, s_ty) = self.lower_term(scrutinee)?; + // Iter 14d: Bool scrutinee. `Term::If` was retired and migrated to + // a `Match` of shape `(lit-bool true) -> A | _ -> B`. Recognise + // exactly that two-arm pattern (the canonical migration target; + // see DESIGN.md Decision 7) and emit the conditional-branch IR + // the old `Term::If` arm used to emit. We do not generalise to + // arbitrary primitive matches — only the shape that replaces + // `If`. + if s_ty == "i1" { + return self.lower_bool_match(&s_val, arms); + } if s_ty != "ptr" { return Err(CodegenError::Internal(format!( "match on non-ADT scrutinee (got {s_ty}); MVP supports only ADTs" @@ -1192,6 +1144,97 @@ impl<'a> Emitter<'a> { Ok((phi, rt)) } + /// Iter 14d: lower a `Match` whose scrutinee is `Bool` (`i1`) and + /// whose arms are exactly the canonical migration shape from + /// retired `Term::If`: + /// + /// 1. `(pat-lit (lit-bool C)) -> A` + /// 2. `_ -> B` + /// + /// Emits the same IR the old `Term::If` arm of `lower_term` did: + /// `br i1` to a `then`/`else` pair joined by a phi. Both arms must + /// produce the same LLVM type (the typechecker has unified them + /// already; the assertion is a defence-in-depth check). + fn lower_bool_match( + &mut self, + cond_v: &str, + arms: &[Arm], + ) -> Result<(String, String)> { + if arms.len() != 2 { + return Err(CodegenError::Internal(format!( + "Bool-scrutinee match: expected exactly 2 arms (lit-bool + wild), got {}", + arms.len() + ))); + } + let (true_body, false_body) = match (&arms[0].pat, &arms[1].pat) { + ( + Pattern::Lit { lit: Literal::Bool { value: true } }, + Pattern::Wild, + ) => (&arms[0].body, &arms[1].body), + ( + Pattern::Lit { lit: Literal::Bool { value: false } }, + Pattern::Wild, + ) => (&arms[1].body, &arms[0].body), + _ => { + return Err(CodegenError::Internal( + "Bool-scrutinee match: arms must be `(lit-bool true) -> A | _ -> B` \ + or `(lit-bool false) -> A | _ -> B` (the Iter 14d migration shape \ + for retired `Term::If`)".into(), + )); + } + }; + + let id = self.fresh_id(); + let then_lbl = format!("then.{id}"); + let else_lbl = format!("else.{id}"); + let join_lbl = format!("join.{id}"); + + self.body.push_str(&format!( + " br i1 {cond_v}, label %{then_lbl}, label %{else_lbl}\n" + )); + + self.start_block(&then_lbl); + let (then_v, then_ty) = self.lower_term(true_body)?; + // Nested code in the `then` body may have changed the + // block label — phi must see the last actual block. + let then_block_end = self.current_block.clone(); + self.body.push_str(&format!(" br label %{join_lbl}\n")); + + self.start_block(&else_lbl); + let (else_v, else_ty) = self.lower_term(false_body)?; + if then_ty != else_ty { + return Err(CodegenError::Internal(format!( + "Bool-match arms type mismatch: {then_ty} vs {else_ty}" + ))); + } + let else_block_end = self.current_block.clone(); + self.body.push_str(&format!(" br label %{join_lbl}\n")); + + self.start_block(&join_lbl); + let phi = self.fresh_ssa(); + self.body.push_str(&format!( + " {phi} = phi {ty} [ {tv}, %{tlbl} ], [ {ev}, %{elbl} ]\n", + ty = then_ty, + tv = then_v, + tlbl = then_block_end, + ev = else_v, + elbl = else_block_end, + )); + // If both branches yield the same fn-pointer sig, forward it + // to the phi SSA so subsequent indirect calls can resolve. + if then_ty == "ptr" { + if let (Some(ts), Some(es)) = + (self.ssa_fn_sigs.get(&then_v), self.ssa_fn_sigs.get(&else_v)) + { + if ts.params == es.params && ts.ret == es.ret { + let merged = ts.clone(); + self.ssa_fn_sigs.insert(phi.clone(), merged); + } + } + } + Ok((phi, then_ty)) + } + fn lower_app(&mut self, name: &str, args: &[Term]) -> Result<(String, String)> { // Built-in arithmetic / comparison. if let Some((instr, ret_ty)) = builtin_binop(name) { @@ -1722,11 +1765,6 @@ impl<'a> Emitter<'a> { bound.remove(name); } } - Term::If { cond, then, else_ } => { - Self::collect_captures(cond, bound, captures, captures_set, builtins, top_level); - Self::collect_captures(then, bound, captures, captures_set, builtins, top_level); - Self::collect_captures(else_, bound, captures, captures_set, builtins, top_level); - } Term::Do { args, .. } => { for a in args { Self::collect_captures(a, bound, captures, captures_set, builtins, top_level); @@ -2043,7 +2081,6 @@ impl<'a> Emitter<'a> { new_extras.push((name.clone(), v_ail)); self.synth_with_extras(body, &new_extras) } - Term::If { then, .. } => self.synth_with_extras(then, extras), Term::Do { op, .. } => builtin_effect_op_ret(op).ok_or_else(|| { CodegenError::Internal(format!( "synth_arg_type: unknown effect op `{op}`" @@ -2349,11 +2386,6 @@ fn apply_subst_to_term(t: &Term, subst: &BTreeMap) -> Term { value: Box::new(apply_subst_to_term(value, subst)), body: Box::new(apply_subst_to_term(body, subst)), }, - Term::If { cond, then, else_ } => Term::If { - cond: Box::new(apply_subst_to_term(cond, subst)), - then: Box::new(apply_subst_to_term(then, subst)), - else_: Box::new(apply_subst_to_term(else_, subst)), - }, Term::Do { op, args } => Term::Do { op: op.clone(), args: args.iter().map(|a| apply_subst_to_term(a, subst)).collect(), diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs index be00c55..931c76e 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -211,13 +211,6 @@ pub enum Term { value: Box, body: Box, }, - /// If-expression. Both branches must have the same type. - If { - cond: Box, - then: Box, - #[serde(rename = "else")] - else_: Box, - }, /// Effect operation invocation (e.g. `do print "hi"`). The `op` is /// resolved against the effect-handler table at link time. Do { diff --git a/crates/ailang-core/src/hash.rs b/crates/ailang-core/src/hash.rs index d78607d..92944ae 100644 --- a/crates/ailang-core/src/hash.rs +++ b/crates/ailang-core/src/hash.rs @@ -98,6 +98,11 @@ mod tests { /// `skip_serializing_if` is missing or wrong. We deserialise the /// real example modules from disk to avoid drift between the test /// and the source-of-truth JSON. + /// + /// Iter 14d note: the `sum` def was migrated from `Term::If` to + /// `Term::Match` on Bool. The hash for `sum.sum` therefore changed + /// intentionally (Decision 7). The pin updated below tracks the + /// new identity. `IntList` from `list.ail.json` did not change. #[test] fn iter13a_schema_extension_preserves_pre_13a_hashes() { let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); @@ -107,7 +112,7 @@ mod tests { .expect("examples/sum.ail.json present"); let sum_mod: crate::ast::Module = serde_json::from_slice(&sum_src).unwrap(); let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap(); - assert_eq!(def_hash(sum_def), "db33f57cb329935e"); + assert_eq!(def_hash(sum_def), "7f5fe7f72c63a9fd"); let list_src = std::fs::read(examples.join("list.ail.json")) .expect("examples/list.ail.json present"); diff --git a/crates/ailang-core/src/pretty.rs b/crates/ailang-core/src/pretty.rs index d0eff71..8b9fcc5 100644 --- a/crates/ailang-core/src/pretty.rs +++ b/crates/ailang-core/src/pretty.rs @@ -193,16 +193,6 @@ fn term_block(t: &Term, indent: usize) -> String { s.push(')'); s } - Term::If { cond, then, else_ } => { - let mut s = format!("{pad}(if\n"); - s.push_str(&term_block(cond, indent + 2)); - s.push('\n'); - s.push_str(&term_block(then, indent + 2)); - s.push('\n'); - s.push_str(&term_block(else_, indent + 2)); - s.push(')'); - s - } Term::Do { op, args } => { let mut s = format!("{pad}(do {op}"); for a in args { @@ -337,14 +327,6 @@ fn term_inline(t: &Term) -> String { term_inline(body) ) } - Term::If { cond, then, else_ } => { - format!( - "(if {} {} {})", - term_inline(cond), - term_inline(then), - term_inline(else_) - ) - } Term::Lam { params, .. } => { format!("(\\ {} ...)", params.join(" ")) } diff --git a/crates/ailang-surface/src/parse.rs b/crates/ailang-surface/src/parse.rs index 676ad8b..304ca50 100644 --- a/crates/ailang-surface/src/parse.rs +++ b/crates/ailang-surface/src/parse.rs @@ -35,7 +35,7 @@ //! //! term ::= var-ref | int-lit | str-lit | bool-lit | unit-lit //! | app-term | match-term | ctor-term | do-term | seq-term -//! | lam-term | if-term | let-term +//! | lam-term | let-term //! var-ref ::= ident ; reserved: true/false → bool-lit //! int-lit ::= integer ; numeric atom //! str-lit ::= string ; string atom @@ -51,7 +51,6 @@ //! "(" "ret" type ")" //! effects-clause? body-attr ")" //! typed-param ::= "(" "typed" ident type ")" -//! if-term ::= "(" "if" term term term ")" //! let-term ::= "(" "let" ident term term ")" //! //! pattern ::= pat-var | pat-ctor | pat-lit | pat-wild @@ -683,7 +682,6 @@ impl<'a> Parser<'a> { "do" => self.parse_do(), "seq" => self.parse_seq(), "lam" => self.parse_lam(), - "if" => self.parse_if(), "let" => self.parse_let(), other => { let pos = self.peek().map(|t| t.span.start).unwrap_or(0); @@ -691,7 +689,7 @@ impl<'a> Parser<'a> { production: "term", message: format!( "unknown term head `{other}`; expected one of \ - `app`, `lam`, `let`, `if`, `match`, `do`, `seq`, \ + `app`, `lam`, `let`, `match`, `do`, `seq`, \ `term-ctor`, `lit-unit`" ), pos, @@ -872,20 +870,6 @@ impl<'a> Parser<'a> { }) } - fn parse_if(&mut self) -> Result { - self.expect_lparen("if-term")?; - self.expect_keyword("if")?; - let cond = self.parse_term()?; - let then = self.parse_term()?; - let else_ = self.parse_term()?; - self.expect_rparen("if-term")?; - Ok(Term::If { - cond: Box::new(cond), - then: Box::new(then), - else_: Box::new(else_), - }) - } - fn parse_let(&mut self) -> Result { self.expect_lparen("let-term")?; self.expect_keyword("let")?; diff --git a/crates/ailang-surface/src/print.rs b/crates/ailang-surface/src/print.rs index e297427..7960f22 100644 --- a/crates/ailang-surface/src/print.rs +++ b/crates/ailang-surface/src/print.rs @@ -241,15 +241,6 @@ fn write_term(out: &mut String, t: &Term, level: usize) { write_term(out, body, level); out.push(')'); } - Term::If { cond, then, else_ } => { - out.push_str("(if "); - write_term(out, cond, level); - out.push(' '); - write_term(out, then, level); - out.push(' '); - write_term(out, else_, level); - out.push(')'); - } Term::Do { op, args } => { out.push_str("(do "); out.push_str(op); diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 523bde6..947cbd8 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -429,6 +429,31 @@ fixtures round-trip identically through `print → parse → canonical JSON`; the three hand-written `.ailx` exhibits parse to canonical JSON identical to their corresponding `.ail.json` files. +## Decision 7: redundancy removal — `Term::If` is not a primitive + +`Term::If { cond, then, else_ }` is semantically a subset of +`Term::Match` on `Bool`. Per CLAUDE.md the language must contain no +redundancies; two AST nodes for the same operation produces an +authoring decision with no semantic content and an extra codegen +path. Iter 14d removes `Term::If`. Migration shape on the JSON side: + + {"t":"if","cond":C,"then":A,"else":B} + → + {"t":"match","scrutinee":C, + "arms":[ + {"pat":{"p":"lit","lit":{"kind":"bool","value":true}},"body":A}, + {"pat":{"p":"wild"},"body":B}]} + +The wildcard arm satisfies the typechecker's +`primitive-needs-wildcard` rule. A future iter may upgrade the +exhaustiveness check to recognise the `true`+`false` arm pair as +covering Bool without a wildcard; until then, wildcard is the +canonical migration target. + +No schema version bump (no third-party consumes `ailang/v0`). +Hash invalidation for the three migrated fixtures (`sum`, `sort`, +`max3`) is intentional; the new hashes become the new identity. + ## Mangling scheme (Iter 5c) All AILang functions are mangled to `@ail__` — even in the @@ -492,7 +517,6 @@ hashes stay bit-identical. { "t": "var", "name": "" } { "t": "app", "fn": Term, "args": [Term...] } { "t": "let", "name": "", "value": Term, "body": Term } -{ "t": "if", "cond": Term, "then": Term, "else": Term } { "t": "do", "op": "/", "args": [Term...] } { "t": "ctor", "type": "", "ctor": "", "args": [Term...] } { "t": "match", "scrutinee": Term, "arms": [Arm...] } @@ -588,7 +612,9 @@ as iterations land; the JOURNAL records the exact iteration. What **is** supported (and used as the smoke test for the pipeline): - Int, Bool, Unit, **Str** as primitive types. -- `if`, `let`, function calls, recursion. +- `let`, function calls, recursion. Bool branching is expressed via + `match` on `Bool` with a `(lit-bool true)` arm and a wildcard + fallback (Decision 7); there is no separate `if` AST node. - Effects on function signatures, with `do op(args)` for direct effect ops (`io/print_int`, `io/print_bool`, `io/print_str`). - **ADTs + flat pattern matching** (Iter 3). Sub-patterns of a Ctor diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md index e106ed3..55ff954 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -1744,5 +1744,136 @@ this iter cycle, though the architectural pin keeps it open for future replacement of `ailang-surface` should the form prove inadequate at stdlib scale. +## Language-completion sequence (14d → 14f, then stdlib in 15a) + +User redirected at the 14d boundary: write the language to +"finished" before starting on a stdlib. Reasoning: +authoring a stdlib in an unfinished language wastes work — +each gap discovered later forces a rewrite of code already +written. The user also confirmed: **no schema version bump +needed**. AILang has exactly one consumer (me), so version +ceremony for compatibility management is pure overhead. +Edit AST and fixtures in place; pin new hashes where the +hash regression test demands it. + +Updated planning sequence: + +- **14d** — remove `Term::If` redundancy. Pure subtraction. +- **14e** — explicit tail-call annotation (`tail` flag on + `Term::App`/`Term::Do`, `musttail` in codegen, tail-position + verifier in checker). +- **14f** — memory management. Currently every ADT allocation + leaks. Likely Boehm conservative GC (`GC_malloc` + `-lgc`) + for minimum surface change; design pass first. +- **15a** — first stdlib module (`std_list`). + +Deferred (not stdlib-blocking; can land later without +rewriting code that already exists): records/tuples (use +ADT pairs), nested patterns (use pyramid `match`), local +recursive `let` (hoist to top level). + +## Iter 14d — `Term::If` removed + +Subtraction iter. `Term::If { cond, then, else_ }` was +semantically a subset of `Term::Match` on `Bool`. CLAUDE.md +forbids redundancies; two AST nodes for the same operation +was an authoring decision with no semantic content and a +duplicate codegen path. + +The migration shape was the canonical one named in the +brief: + +``` +(if c a b) → (match c (case (lit-bool true) a) (case _ b)) +``` + +Wildcard arm satisfies the existing `primitive-needs-wildcard` +rule. A future iter may upgrade exhaustiveness to recognise +`true`+`false` as covering Bool without a wildcard, but the +wildcard form works with the current checker and that was +enough for 14d. + +**Implementer dispatch went clean with one documented +deviation.** Removing the `Term::If` codegen path was not +sufficient on its own: the existing match codegen rejects +`i1` (Bool) scrutinees and `Pattern::Lit` patterns — both +of which the migration shape requires. The implementer +added a tightly-scoped `lower_bool_match` helper (~95 LOC) +that handles **only** the two-arm Bool migration shape +(`(lit-bool true) -> A | _ -> B` or its mirror), errors on +anything else, and emits the same `br i1`/phi IR the old +`Term::If` path emitted. No generalisation of the +ADT-match codegen. + +The deviation was the right call. **Lesson for future +subtraction iters**: when removing a specialised AST node, +the codegen for the migration target may need a small +extension. Pre-emptively scope this in the brief next time. + +**Diff size**: 13 files, +286/-221 LOC. Net +65 LOC across +the workspace, but the AST got smaller (one variant gone), +the form-(A) grammar got smaller (one production gone), +and the typechecker got smaller (one branch gone). Codegen +got slightly larger because of the bool-match helper, but +the alternative was reusing the existing match path and +generalising it — which would have been a bigger and +riskier change. + +**Hash deltas** (intentional, per Decision 7): + +| def | before | after | +|---|---|---| +| `sum.sum` | `db33f57cb329935e` | `7f5fe7f72c63a9fd` | +| `sort.insert` | `697fcb9f30f8633a` | `07ff6ee7db17565d` | +| `max3.max` | `65c45d6a45dd0a72` | `2aa1576f3fbf5b3d` | +| `max3.max3` | `624b14429bf302f5` | `c452ec2e36c0af27` | + +Untouched defs (e.g. `sum.main`, all `sort.*` except +`insert`, `sort.IntList`, `sort.print_list`, `max3.main`) +keep bit-identical hashes. That's the canary that the +canonical-JSON byte format was not perturbed — only the +migrated bodies changed identity. + +**Verification**: 76/76 tests green (unchanged count; no +new tests in this iter, by design — subtraction). Manual +smoke: `sum` → `55`, `max3` → `17`, `sort` → ordered list. +Identical to pre-migration stdout for all three. `cargo +doc --no-deps` 0 warnings. + +**Tail-call survey from the implementer (gold finding, +informs 14e).** While reading the migrated fixtures the +implementer surveyed tail positions. Result: + +- `print_list` (in both `sort.ail.json` and + `list_map_poly.ail.json`): the recursive call is the + rhs of a `seq` which is the body of a match arm — + **already in tail position**. TCO would convert these + to actual loops. +- `main` chains: the outer call is in tail position; + inner calls are not. +- `insert`, `sort`, `map`: the recursive calls are + **arguments to a `Cons` ctor construction** (e.g. + `Cons (f h) (map f t)`). NOT in tail position. + Constructor-blocking is the standard ML/Haskell case + where TCO does not apply without a CPS transform or an + accumulator-form rewrite. + +**Implications for 14e.** Adding a `tail` flag to +`Term::App`/`Do` will work for `print_list`-style +recursions and for terminal call chains, but **will not** +help the `map`/`sort`/`insert`-style ctor-blocked +recursions. Those need either an accumulator-form rewrite +in the source program (the standard ML/Haskell move) or a +CPS transform (much more intrusive). 14e ships only the +annotation + verification; accumulator forms become an +authoring pattern in the stdlib, not a compiler feature. + +This sharpens what 14e can promise: tail-call **wins** +will be visible in `print_list`-style terminal-recursion +patterns; `map`/`sort` style stays stack-bounded by depth, +which makes 14f (GC) the more important iter for handling +long lists than 14e by itself. + + diff --git a/examples/max3.ail.json b/examples/max3.ail.json index b57964c..ca18561 100644 --- a/examples/max3.ail.json +++ b/examples/max3.ail.json @@ -17,8 +17,8 @@ }, "params": ["a", "b"], "body": { - "t": "if", - "cond": { + "t": "match", + "scrutinee": { "t": "app", "fn": { "t": "var", "name": ">" }, "args": [ @@ -26,8 +26,16 @@ { "t": "var", "name": "b" } ] }, - "then": { "t": "var", "name": "a" }, - "else": { "t": "var", "name": "b" } + "arms": [ + { + "pat": { "p": "lit", "lit": { "kind": "bool", "value": true } }, + "body": { "t": "var", "name": "a" } + }, + { + "pat": { "p": "wild" }, + "body": { "t": "var", "name": "b" } + } + ] } }, { @@ -46,8 +54,8 @@ "params": ["a", "b", "c"], "doc": "Demonstriert verschachteltes if (statt max-call) zum Test des Block-Trackings.", "body": { - "t": "if", - "cond": { + "t": "match", + "scrutinee": { "t": "app", "fn": { "t": "var", "name": ">" }, "args": [ @@ -55,32 +63,56 @@ { "t": "var", "name": "b" } ] }, - "then": { - "t": "if", - "cond": { - "t": "app", - "fn": { "t": "var", "name": ">" }, - "args": [ - { "t": "var", "name": "a" }, - { "t": "var", "name": "c" } - ] + "arms": [ + { + "pat": { "p": "lit", "lit": { "kind": "bool", "value": true } }, + "body": { + "t": "match", + "scrutinee": { + "t": "app", + "fn": { "t": "var", "name": ">" }, + "args": [ + { "t": "var", "name": "a" }, + { "t": "var", "name": "c" } + ] + }, + "arms": [ + { + "pat": { "p": "lit", "lit": { "kind": "bool", "value": true } }, + "body": { "t": "var", "name": "a" } + }, + { + "pat": { "p": "wild" }, + "body": { "t": "var", "name": "c" } + } + ] + } }, - "then": { "t": "var", "name": "a" }, - "else": { "t": "var", "name": "c" } - }, - "else": { - "t": "if", - "cond": { - "t": "app", - "fn": { "t": "var", "name": ">" }, - "args": [ - { "t": "var", "name": "b" }, - { "t": "var", "name": "c" } - ] - }, - "then": { "t": "var", "name": "b" }, - "else": { "t": "var", "name": "c" } - } + { + "pat": { "p": "wild" }, + "body": { + "t": "match", + "scrutinee": { + "t": "app", + "fn": { "t": "var", "name": ">" }, + "args": [ + { "t": "var", "name": "b" }, + { "t": "var", "name": "c" } + ] + }, + "arms": [ + { + "pat": { "p": "lit", "lit": { "kind": "bool", "value": true } }, + "body": { "t": "var", "name": "b" } + }, + { + "pat": { "p": "wild" }, + "body": { "t": "var", "name": "c" } + } + ] + } + } + ] } }, { diff --git a/examples/sort.ail.json b/examples/sort.ail.json index 2158122..35f3c4a 100644 --- a/examples/sort.ail.json +++ b/examples/sort.ail.json @@ -63,8 +63,8 @@ ] }, "body": { - "t": "if", - "cond": { + "t": "match", + "scrutinee": { "t": "app", "fn": { "t": "var", "name": "<=" }, "args": [ @@ -72,39 +72,47 @@ { "t": "var", "name": "h" } ] }, - "then": { - "t": "ctor", - "type": "IntList", - "ctor": "Cons", - "args": [ - { "t": "var", "name": "y" }, - { + "arms": [ + { + "pat": { "p": "lit", "lit": { "kind": "bool", "value": true } }, + "body": { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "var", "name": "y" }, + { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "var", "name": "h" }, + { "t": "var", "name": "t" } + ] + } + ] + } + }, + { + "pat": { "p": "wild" }, + "body": { "t": "ctor", "type": "IntList", "ctor": "Cons", "args": [ { "t": "var", "name": "h" }, - { "t": "var", "name": "t" } + { + "t": "app", + "fn": { "t": "var", "name": "insert" }, + "args": [ + { "t": "var", "name": "y" }, + { "t": "var", "name": "t" } + ] + } ] } - ] - }, - "else": { - "t": "ctor", - "type": "IntList", - "ctor": "Cons", - "args": [ - { "t": "var", "name": "h" }, - { - "t": "app", - "fn": { "t": "var", "name": "insert" }, - "args": [ - { "t": "var", "name": "y" }, - { "t": "var", "name": "t" } - ] - } - ] - } + } + ] } } ] diff --git a/examples/sum.ail.json b/examples/sum.ail.json index a480b8b..2e5308c 100644 --- a/examples/sum.ail.json +++ b/examples/sum.ail.json @@ -15,8 +15,8 @@ "params": ["n"], "doc": "rekursive Summe 0..=n", "body": { - "t": "if", - "cond": { + "t": "match", + "scrutinee": { "t": "app", "fn": { "t": "var", "name": "==" }, "args": [ @@ -24,28 +24,36 @@ { "t": "lit", "lit": { "kind": "int", "value": 0 } } ] }, - "then": { "t": "lit", "lit": { "kind": "int", "value": 0 } }, - "else": { - "t": "app", - "fn": { "t": "var", "name": "+" }, - "args": [ - { "t": "var", "name": "n" }, - { + "arms": [ + { + "pat": { "p": "lit", "lit": { "kind": "bool", "value": true } }, + "body": { "t": "lit", "lit": { "kind": "int", "value": 0 } } + }, + { + "pat": { "p": "wild" }, + "body": { "t": "app", - "fn": { "t": "var", "name": "sum" }, + "fn": { "t": "var", "name": "+" }, "args": [ + { "t": "var", "name": "n" }, { "t": "app", - "fn": { "t": "var", "name": "-" }, + "fn": { "t": "var", "name": "sum" }, "args": [ - { "t": "var", "name": "n" }, - { "t": "lit", "lit": { "kind": "int", "value": 1 } } + { + "t": "app", + "fn": { "t": "var", "name": "-" }, + "args": [ + { "t": "var", "name": "n" }, + { "t": "lit", "lit": { "kind": "int", "value": 1 } } + ] + } ] } ] } - ] - } + } + ] } }, {