diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index 77703d7..aca1bf0 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -878,6 +878,11 @@ 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 6505b39..5cf85ae 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -249,11 +249,8 @@ 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 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. + // Variant: load sum.ail.json, mutate the `then` branch (1 instead of 0) + // in the `sum` definition. `main` stays bit-identical. 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"); { @@ -263,11 +260,12 @@ 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") { - let new_body = serde_json::json!({ + // Replace the then branch literal 0 → literal 1. + let new_then = serde_json::json!({ "t": "lit", "lit": { "kind": "int", "value": 1 } }); - def["body"]["arms"][0]["body"] = new_body; + def["body"]["then"] = new_then; } } } diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 1a17a95..757f6da 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -962,6 +962,11 @@ pub fn verify_tail_positions(t: &Term, is_tail: bool) -> Result<()> { verify_tail_positions(value, false)?; verify_tail_positions(body, is_tail) } + Term::If { cond, then, else_ } => { + verify_tail_positions(cond, false)?; + verify_tail_positions(then, is_tail)?; + verify_tail_positions(else_, is_tail) + } Term::Seq { lhs, rhs } => { verify_tail_positions(lhs, false)?; verify_tail_positions(rhs, is_tail) @@ -1118,6 +1123,14 @@ 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 @@ -1695,13 +1708,8 @@ 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 match_arms_must_unify() { + fn if_branches_must_match() { let m = Module { schema: SCHEMA.into(), name: "t".into(), @@ -1714,24 +1722,14 @@ mod tests { effects: vec![], }, vec![], - Term::Match { - scrutinee: Box::new(Term::Lit { + Term::If { + cond: Box::new(Term::Lit { lit: Literal::Bool { value: true }, }), - 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 }, - }, - ], + then: Box::new(Term::Lit { + lit: Literal::Int { value: 1 }, + }), + else_: Box::new(Term::Lit { lit: Literal::Unit }), }, )], }; diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 54574fa..3adba84 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -816,6 +816,87 @@ 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)?; + // Iter 14e: a tail-call in this branch already terminated + // its block; skip its branch to join and exclude from phi. + let then_terminated = self.block_terminated; + let then_block_end = self.current_block.clone(); + if !then_terminated { + self.body.push_str(&format!(" br label %{join_lbl}\n")); + } + + self.start_block(&else_lbl); + let (else_v, else_ty) = self.lower_term(else_)?; + let else_terminated = self.block_terminated; + let else_block_end = self.current_block.clone(); + if !then_terminated && !else_terminated && then_ty != else_ty { + return Err(CodegenError::Internal(format!( + "if branches type mismatch: {then_ty} vs {else_ty}" + ))); + } + if !else_terminated { + self.body.push_str(&format!(" br label %{join_lbl}\n")); + } + + // Iter 14e: if both branches terminated, the whole `if` is + // terminated and no join is reachable. Mark and bail. + if then_terminated && else_terminated { + self.block_terminated = true; + return Ok(("0".into(), then_ty)); + } + // If exactly one branch terminated, the join receives only + // the other branch's value — no phi node is needed. + if then_terminated { + self.start_block(&join_lbl); + return Ok((else_v, else_ty)); + } + if else_terminated { + self.start_block(&join_lbl); + return Ok((then_v, then_ty)); + } + + 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, tail } => { // Direct call when the callee is a `Var` referring to a // statically-known target (builtin, current-module fn, @@ -962,16 +1043,6 @@ 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" @@ -1184,120 +1255,6 @@ 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)?; - // Iter 14e: a tail-call in this arm already terminated its - // block; skip its branch to join and exclude from phi. - let then_terminated = self.block_terminated; - let then_block_end = self.current_block.clone(); - if !then_terminated { - 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)?; - let else_terminated = self.block_terminated; - let else_block_end = self.current_block.clone(); - if !then_terminated && !else_terminated && then_ty != else_ty { - return Err(CodegenError::Internal(format!( - "Bool-match arms type mismatch: {then_ty} vs {else_ty}" - ))); - } - if !else_terminated { - self.body.push_str(&format!(" br label %{join_lbl}\n")); - } - - // Iter 14e: if both arms terminated, the whole match is - // terminated and no join is reachable. Mark and bail. - if then_terminated && else_terminated { - self.block_terminated = true; - return Ok(("0".into(), then_ty)); - } - // If exactly one arm terminated, the join receives only the - // other arm's value — no phi node is needed. - if then_terminated { - self.start_block(&join_lbl); - return Ok((else_v, else_ty)); - } - if else_terminated { - self.start_block(&join_lbl); - return Ok((then_v, then_ty)); - } - - 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], tail: bool) -> Result<(String, String)> { // Built-in arithmetic / comparison. if let Some((instr, ret_ty)) = builtin_binop(name) { @@ -1874,6 +1831,11 @@ 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); @@ -2214,6 +2176,7 @@ 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}`" @@ -2520,6 +2483,11 @@ 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, tail } => 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 734ce49..bcd3799 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -219,6 +219,13 @@ 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. /// diff --git a/crates/ailang-core/src/hash.rs b/crates/ailang-core/src/hash.rs index df946c0..d851a7c 100644 --- a/crates/ailang-core/src/hash.rs +++ b/crates/ailang-core/src/hash.rs @@ -99,11 +99,6 @@ 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")); @@ -113,7 +108,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), "7f5fe7f72c63a9fd"); + assert_eq!(def_hash(sum_def), "db33f57cb329935e"); 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 3a7875d..55bbc07 100644 --- a/crates/ailang-core/src/pretty.rs +++ b/crates/ailang-core/src/pretty.rs @@ -193,6 +193,16 @@ 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 { @@ -327,6 +337,14 @@ 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 fc310f7..1e1d808 100644 --- a/crates/ailang-surface/src/parse.rs +++ b/crates/ailang-surface/src/parse.rs @@ -35,7 +35,8 @@ //! //! term ::= var-ref | int-lit | str-lit | bool-lit | unit-lit //! | app-term | tail-app-term | match-term | ctor-term -//! | do-term | tail-do-term | seq-term | lam-term | let-term +//! | do-term | tail-do-term | seq-term | lam-term | if-term +//! | let-term //! var-ref ::= ident ; reserved: true/false → bool-lit //! int-lit ::= integer ; numeric atom //! str-lit ::= string ; string atom @@ -53,6 +54,7 @@ //! "(" "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 @@ -686,6 +688,7 @@ impl<'a> Parser<'a> { "tail-do" => self.parse_tail_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); @@ -693,7 +696,7 @@ impl<'a> Parser<'a> { production: "term", message: format!( "unknown term head `{other}`; expected one of \ - `app`, `tail-app`, `lam`, `let`, `match`, `do`, \ + `app`, `tail-app`, `lam`, `let`, `if`, `match`, `do`, \ `tail-do`, `seq`, `term-ctor`, `lit-unit`" ), pos, @@ -912,6 +915,20 @@ 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 ad45ae7..5d206be 100644 --- a/crates/ailang-surface/src/print.rs +++ b/crates/ailang-surface/src/print.rs @@ -245,6 +245,15 @@ 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, tail } => { // Iter 14e: `tail-do` mirrors `tail-app` for effect ops. out.push_str(if *tail { "(tail-do " } else { "(do " }); diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 040bcdc..846f913 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -446,6 +446,13 @@ JSON identical to their corresponding `.ail.json` files. ## Decision 7: redundancy removal — `Term::If` is not a primitive +**Status: REVERTED in Iter 14g.** This decision was made on shaky grounds — +applying CLAUDE.md's "no redundancies" rule to a case that turned out to +be primitive control flow, not redundancy. The post-removal match-on-Bool +form (3× the tokens, asymmetric `pat-wild` for the false case) was +worse for token economy and worse for the natural shape of the language. +`Term::If` is restored. The text below is preserved for the audit trail. + `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 @@ -645,6 +652,7 @@ 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...] } @@ -740,9 +748,7 @@ 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. -- `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. +- `if`, `let`, function calls, recursion. - 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 f48e9d0..e8118f1 100644 --- a/docs/JOURNAL.md +++ b/docs/JOURNAL.md @@ -2098,6 +2098,99 @@ under `examples/std/` as `.ailx` source; tests load the generated `.ail.json`. `ailang-check` and `ailang-codegen` remain projection-agnostic. +## Iter 14g — `Term::If` restored (revert of 14d) + +Reconsidered 14d's removal of `Term::If`. The decision was wrong; +restored. + +**Why 14d was wrong.** "No redundancies" from CLAUDE.md is a real +rule but it requires judgment to apply. `Term::If` reduces to +`Term::Match` on Bool, but reducibility is not redundancy in a +strong sense — `1 + 1` reduces to `2`, you don't remove `+` from +the language because of it. `Term::If` is a primitive control- +flow shape that every programming language has for good reason: +bool branching is the second most common control flow shape after +sequencing. + +**Quantitative.** `(if c a b)` is 4 tokens. The post-14d +replacement `(match c (case (pat-lit true) a) (case (pat-wild) b))` +is 12. That's a 3× token-economy hit on every Bool branch — in +exactly the language whose authoring constraint was supposed to +be token-efficient. The match-on-Bool form is also asymmetric +(false case via `pat-wild` because the typechecker rejects +`pat-lit false` as exhaustive) and structurally lopsided. + +**Meta-pattern that produced the wrong call.** I had been treating +user observations as directives. The user said "if is a subset of +match" — which is a factual observation; I jumped to remove it, +citing CLAUDE.md as cover. There was no independent conviction +behind the change, only doctrinal hooking-up of a user remark. +The leak showed up in 14f's JOURNAL prose ("three lines for what +`if` used to do in one"), which the user correctly read as me +regretting the decision. + +Two feedback memories saved to head this off in future iters +(`/home/brummel/.claude/projects/-home-brummel-dev-ailang/memory/`): +- `feedback_user_suggestions_not_directives.md` — observations + are input, not output. Form an opinion before acting. +- `feedback_no_nostalgia_for_removed_features.md` — describe + canonical form on its own merits, not as compensation for + what was deleted. + +**Implementation (revert).** Mechanically reverse-applied 14d's +diff at every site (`ast.rs`, `check/lib.rs` (incl. the new +14e `verify_tail_positions` arm), `codegen/lib.rs` (4 sites), +`surface/{parse,print}.rs`, `core/pretty.rs`, `ail/main.rs`, +`e2e.rs` test mutation). Removed the `lower_bool_match` helper +that 14d had introduced — it existed only because the 14d +migration shape needed codegen for non-`ptr` match scrutinees; +with `Term::If` back, match-on-Bool returns to its pre-14d +unsupported state and the helper is dead weight. Three fixtures +(`sum`, `sort`, `max3`) restored to their pre-14d shape. + +**One additional fixture migration.** `gc_stress` was authored +in 14f using the 14d match-on-Bool migration shape (because +14f sat between 14d and this revert). After removing +`lower_bool_match` it would have failed to compile. Migrated +`gc_stress.{ail.json,ailx}` to use `(if ...)` directly. Output +unchanged: `1275`. + +**14e and 14f are intact.** Verified by spot-emit of +`list_map_poly`'s IR: `musttail call i8 @ail_list_map_poly_print_list` +and `call ptr @GC_malloc(i64 8)` both present. The revert is +strictly local to `Term::If`-related code paths. + +**Hash check.** All four pre-14d hashes returned: + +| def | restored hash | +|---|---| +| `sum.sum` | `db33f57cb329935e` | +| `sort.insert` | `697fcb9f30f8633a` | +| `max3.max` | `65c45d6a45dd0a72` | +| `max3.max3` | `624b14429bf302f5` | + +Untouched defs across all 18 fixtures (incl. the 14e print_list +hash deltas) keep their post-14f hashes. The revert's hash +movement is exactly the four 14d-migrated defs reverting plus +the one accidental 14f-victim (`gc_stress` defs, never previously +shipped under any other hash). + +**DESIGN.md.** Decision 7 is preserved with a `Status: REVERTED` +header. Audit trail matters; future reads should see the +decision and its reversal both. Form-(A) productions in +Decision 6's appendix have `if-term` restored. + +**Tests: 80/80 green.** Identical stdout for every existing +fixture. `cargo doc --no-deps` 0 warnings. + +**LOC delta.** +265/-295 net −30. Net cleanup: the `lower_bool_match` +helper was bigger than the restored `Term::If` codegen. + +**Plan.** Back to 15a — first stdlib module `std_maybe`. The +brief I had drafted included an "authoring note: post-14d +if-then-else" section that's now obsolete. Re-issue without +that, using `if` naturally where appropriate. + diff --git a/examples/gc_stress.ail.json b/examples/gc_stress.ail.json index fbeac57..57dca34 100644 --- a/examples/gc_stress.ail.json +++ b/examples/gc_stress.ail.json @@ -1 +1 @@ -{"defs":[{"ctors":[{"fields":[],"name":"Nil"},{"fields":[{"k":"var","name":"a"},{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}],"name":"Cons"}],"doc":"Polymorphic singly-linked list (re-declared locally).","kind":"type","name":"List","vars":["a"]},{"body":{"arms":[{"body":{"args":[],"ctor":"Nil","t":"ctor","type":"List"},"pat":{"lit":{"kind":"bool","value":true},"p":"lit"}},{"body":{"args":[{"name":"n","t":"var"},{"args":[{"args":[{"name":"n","t":"var"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"-","t":"var"},"t":"app"}],"fn":{"name":"build","t":"var"},"t":"app"}],"ctor":"Cons","t":"ctor","type":"List"},"pat":{"p":"wild"}}],"scrutinee":{"args":[{"name":"n","t":"var"},{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"==","t":"var"},"t":"app"},"t":"match"},"doc":"Build [n, n-1, ..., 1] :: List Int via Cons recursion.","kind":"fn","name":"build","params":["n"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"args":[{"k":"con","name":"Int"}],"k":"con","name":"List"}}},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"h","t":"var"},{"args":[{"name":"t","t":"var"}],"fn":{"name":"sum_list","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Recursively sum a List Int.","kind":"fn","name":"sum_list","params":["xs"],"type":{"effects":[],"k":"fn","params":[{"args":[{"k":"con","name":"Int"}],"k":"con","name":"List"}],"ret":{"k":"con","name":"Int"}}},{"body":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":50},"t":"lit"}],"fn":{"name":"build","t":"var"},"t":"app"}],"fn":{"name":"sum_list","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"doc":"Build [50..1], sum, print 1275.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"gc_stress","schema":"ailang/v0"} \ No newline at end of file +{"defs":[{"ctors":[{"fields":[],"name":"Nil"},{"fields":[{"k":"var","name":"a"},{"args":[{"k":"var","name":"a"}],"k":"con","name":"List"}],"name":"Cons"}],"doc":"Polymorphic singly-linked list (re-declared locally).","kind":"type","name":"List","vars":["a"]},{"body":{"cond":{"args":[{"name":"n","t":"var"},{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"==","t":"var"},"t":"app"},"else":{"args":[{"name":"n","t":"var"},{"args":[{"args":[{"name":"n","t":"var"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"-","t":"var"},"t":"app"}],"fn":{"name":"build","t":"var"},"t":"app"}],"ctor":"Cons","t":"ctor","type":"List"},"t":"if","then":{"args":[],"ctor":"Nil","t":"ctor","type":"List"}},"doc":"Build [n, n-1, ..., 1] :: List Int via Cons recursion.","kind":"fn","name":"build","params":["n"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"args":[{"k":"con","name":"Int"}],"k":"con","name":"List"}}},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"h","t":"var"},{"args":[{"name":"t","t":"var"}],"fn":{"name":"sum_list","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Recursively sum a List Int.","kind":"fn","name":"sum_list","params":["xs"],"type":{"effects":[],"k":"fn","params":[{"args":[{"k":"con","name":"Int"}],"k":"con","name":"List"}],"ret":{"k":"con","name":"Int"}}},{"body":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":50},"t":"lit"}],"fn":{"name":"build","t":"var"},"t":"app"}],"fn":{"name":"sum_list","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"doc":"Build [50..1], sum, print 1275.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"gc_stress","schema":"ailang/v0"} \ No newline at end of file diff --git a/examples/gc_stress.ailx b/examples/gc_stress.ailx index 045c9af..517dede 100644 --- a/examples/gc_stress.ailx +++ b/examples/gc_stress.ailx @@ -2,9 +2,8 @@ ; Builds a List Int of length 50 by recursive Cons construction, ; sums it, prints the sum (1275 = 50*51/2). ; -; Match-on-Int isn't supported as a pattern shape; we drop into a -; match-on-Bool against (== n 0), mirroring the sort fixture's -; (<= y h) idiom. (pat-wild) catches the false case. +; Bool branching uses the canonical `if` form (Decision 7, restored +; in Iter 14g): `(if (== n 0) Nil (Cons n (build (- n 1))))`. (module gc_stress @@ -21,13 +20,11 @@ (ret (con List (con Int))))) (params n) (body - (match (app == n 0) - (case (pat-lit true) - (term-ctor List Nil)) - (case _ - (term-ctor List Cons - n - (app build (app - n 1))))))) + (if (app == n 0) + (term-ctor List Nil) + (term-ctor List Cons + n + (app build (app - n 1)))))) (fn sum_list (doc "Recursively sum a List Int.") diff --git a/examples/max3.ail.json b/examples/max3.ail.json index ca18561..b57964c 100644 --- a/examples/max3.ail.json +++ b/examples/max3.ail.json @@ -17,8 +17,8 @@ }, "params": ["a", "b"], "body": { - "t": "match", - "scrutinee": { + "t": "if", + "cond": { "t": "app", "fn": { "t": "var", "name": ">" }, "args": [ @@ -26,16 +26,8 @@ { "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" } - } - ] + "then": { "t": "var", "name": "a" }, + "else": { "t": "var", "name": "b" } } }, { @@ -54,8 +46,8 @@ "params": ["a", "b", "c"], "doc": "Demonstriert verschachteltes if (statt max-call) zum Test des Block-Trackings.", "body": { - "t": "match", - "scrutinee": { + "t": "if", + "cond": { "t": "app", "fn": { "t": "var", "name": ">" }, "args": [ @@ -63,56 +55,32 @@ { "t": "var", "name": "b" } ] }, - "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": "if", + "cond": { + "t": "app", + "fn": { "t": "var", "name": ">" }, + "args": [ + { "t": "var", "name": "a" }, + { "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" } - } - ] - } - } - ] + "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" } + } } }, { diff --git a/examples/sort.ail.json b/examples/sort.ail.json index 39a0c28..b5cff48 100644 --- a/examples/sort.ail.json +++ b/examples/sort.ail.json @@ -63,8 +63,8 @@ ] }, "body": { - "t": "match", - "scrutinee": { + "t": "if", + "cond": { "t": "app", "fn": { "t": "var", "name": "<=" }, "args": [ @@ -72,47 +72,39 @@ { "t": "var", "name": "h" } ] }, - "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": { + "then": { + "t": "ctor", + "type": "IntList", + "ctor": "Cons", + "args": [ + { "t": "var", "name": "y" }, + { "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" } - ] - } + { "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/std_maybe.ailx b/examples/std_maybe.ailx new file mode 100644 index 0000000..08879fb --- /dev/null +++ b/examples/std_maybe.ailx @@ -0,0 +1,64 @@ +; Iter 15a — first stdlib module: optional values. +; Polymorphic Maybe with four pure combinators. + +(module std_maybe + + (data Maybe (vars a) + (doc "Polymorphic optional value: either Just or Nothing.") + (ctor Nothing) + (ctor Just a)) + + (fn from_maybe + (doc "Project out of Maybe with a default for the Nothing case.") + (type + (forall (vars a) + (fn-type + (params a (con Maybe a)) + (ret a)))) + (params default m) + (body + (match m + (case (pat-ctor Just x) x) + (case (pat-ctor Nothing) default)))) + + (fn is_some + (doc "Returns true iff m is Just<_>.") + (type + (forall (vars a) + (fn-type + (params (con Maybe a)) + (ret (con Bool))))) + (params m) + (body + (match m + (case (pat-ctor Just _) true) + (case (pat-ctor Nothing) false)))) + + (fn is_none + (doc "Returns true iff m is Nothing.") + (type + (forall (vars a) + (fn-type + (params (con Maybe a)) + (ret (con Bool))))) + (params m) + (body + (match m + (case (pat-ctor Just _) false) + (case (pat-ctor Nothing) true)))) + + (fn map_maybe + (doc "Apply f to the wrapped value, or pass Nothing through.") + (type + (forall (vars a b) + (fn-type + (params (fn-type (params a) (ret b)) + (con Maybe a)) + (ret (con Maybe b))))) + (params f m) + (body + (match m + (case (pat-ctor Just x) + (term-ctor Maybe Just (app f x))) + (case (pat-ctor Nothing) + (term-ctor Maybe Nothing)))))) diff --git a/examples/sum.ail.json b/examples/sum.ail.json index 2e5308c..a480b8b 100644 --- a/examples/sum.ail.json +++ b/examples/sum.ail.json @@ -15,8 +15,8 @@ "params": ["n"], "doc": "rekursive Summe 0..=n", "body": { - "t": "match", - "scrutinee": { + "t": "if", + "cond": { "t": "app", "fn": { "t": "var", "name": "==" }, "args": [ @@ -24,36 +24,28 @@ { "t": "lit", "lit": { "kind": "int", "value": 0 } } ] }, - "arms": [ - { - "pat": { "p": "lit", "lit": { "kind": "bool", "value": true } }, - "body": { "t": "lit", "lit": { "kind": "int", "value": 0 } } - }, - { - "pat": { "p": "wild" }, - "body": { + "then": { "t": "lit", "lit": { "kind": "int", "value": 0 } }, + "else": { + "t": "app", + "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": "app", - "fn": { "t": "var", "name": "sum" }, + "fn": { "t": "var", "name": "-" }, "args": [ - { - "t": "app", - "fn": { "t": "var", "name": "-" }, - "args": [ - { "t": "var", "name": "n" }, - { "t": "lit", "lit": { "kind": "int", "value": 1 } } - ] - } + { "t": "var", "name": "n" }, + { "t": "lit", "lit": { "kind": "int", "value": 1 } } ] } ] } - } - ] + ] + } } }, {