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:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
],
|
||||
},
|
||||
)],
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(" "))
|
||||
}
|
||||
|
||||
@@ -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<Term, ParseError> {
|
||||
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<Term, ParseError> {
|
||||
self.expect_lparen("let-term")?;
|
||||
self.expect_keyword("let")?;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user