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
+101 -69
View File
@@ -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<String, Type>) -> 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(),