Iter 14d: remove Term::If as a redundancy

Term::If was 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 a duplicate codegen path. Removed.

Migration shape (applied to sum, sort, max3 fixtures):
  (if c a b) -> (match c (case (lit-bool true) a) (case _ b))

No schema version bump (per user direction): no third-party consumes
ailang/v0, so version ceremony is pure overhead. Edited AST and
fixtures in place; pinned hashes in hash.rs updated.

Implementer deviation, called out and justified: a tightly-scoped
lower_bool_match helper (~95 LOC) was needed in codegen because
the existing match path rejects i1 scrutinees and Pattern::Lit.
Helper accepts only the canonical two-arm migration shape, errors
on anything else, emits the same br/phi IR Term::If used to. No
generalisation of the ADT-match codegen.

Diff: 13 files, +286/-221 (net +65 LOC). AST got smaller
(one variant gone), form-(A) got smaller (one production gone),
typecheck got smaller (one branch gone). Codegen got slightly
larger by the bool-match helper.

Hash deltas: sum.sum, sort.insert, max3.max, max3.max3 changed.
All other defs (e.g. sum.main, sort.IntList, sort.sort,
sort.print_list, max3.main) kept bit-identical hashes — confirms
canonical-JSON byte format intact.

Verification: 76/76 tests green; sum->55, max3->17, sort->[1,1,2,
3,3,4,5,5,5,6,9] (identical to pre-migration). cargo doc 0 warnings.

Tail-call survey by implementer (informs 14e): print_list
recursions are already in tail position (rhs of seq inside match
arm); map/sort/insert recursions are NOT (constructor-blocked
inside Cons applications). 14e annotation will benefit terminal
recursions; ctor-blocked ones need accumulator-form rewrites in
source, not a compiler-side transform.

Decision 7 added to DESIGN.md. JOURNAL entry has the language-
completion sequence (14d done, 14e tail-calls, 14f GC, 15a stdlib).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 16:56:02 +02:00
parent 706f90bacd
commit 8d97a924de
14 changed files with 419 additions and 223 deletions
-7
View File
@@ -211,13 +211,6 @@ pub enum Term {
value: Box<Term>,
body: Box<Term>,
},
/// If-expression. Both branches must have the same type.
If {
cond: Box<Term>,
then: Box<Term>,
#[serde(rename = "else")]
else_: Box<Term>,
},
/// Effect operation invocation (e.g. `do print "hi"`). The `op` is
/// resolved against the effect-handler table at link time.
Do {
+6 -1
View File
@@ -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");
-18
View File
@@ -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(" "))
}