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
-5
View File
@@ -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.
+7 -5
View File
@@ -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;
}
}
}
+22 -15
View File
@@ -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 },
},
],
},
)],
};
+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(),
-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(" "))
}
+2 -18
View File
@@ -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")?;
-9
View File
@@ -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);
+28 -2
View File
@@ -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_<module>_<def>` — even in the
@@ -492,7 +517,6 @@ hashes stay bit-identical.
{ "t": "var", "name": "<id>" }
{ "t": "app", "fn": Term, "args": [Term...] }
{ "t": "let", "name": "<id>", "value": Term, "body": Term }
{ "t": "if", "cond": Term, "then": Term, "else": Term }
{ "t": "do", "op": "<eff>/<op>", "args": [Term...] }
{ "t": "ctor", "type": "<id>", "ctor": "<id>", "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
+131
View File
@@ -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.
+63 -31
View File
@@ -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" }
}
]
}
}
]
}
},
{
+36 -28
View File
@@ -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" }
]
}
]
}
}
]
}
}
]
+23 -15
View File
@@ -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 } }
]
}
]
}
]
}
]
}
}
]
}
},
{