Iter 10: Term::Seq sequencing operator

Adds `Term::Seq { lhs, rhs }` (serde tag "seq") as a first-class
AST node for sequencing effectful expressions. Equivalent in
behaviour to `let _ = lhs in rhs`, but the dedicated node gives the
pretty-printer and diagnostics a cleaner shape and surfaces the
intent ("run for effect, then yield rhs") to future tooling.

Typecheck: lhs must be Unit; rhs's type is the result; effects
accumulate.
Codegen: lower lhs (drop SSA), lower rhs (return).
Capture / deps walkers: recurse into both sides.

Refactored examples/list_map.ail.json's print_list to use seq
instead of `let _ = ...`. Output unchanged (2/4/6).

Hash stability: existing examples without Term::Seq serialise
bit-identical; only list_map.ail.json's hashes changed (deliberate
refactor).

Tests: 51 green (was 50). New unit test
`ailang_check::tests::seq_lhs_must_be_unit` covers the type-error
path; existing list_map e2e covers the happy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 13:14:28 +02:00
parent 4852df51fc
commit c75517ac79
8 changed files with 131 additions and 5 deletions
+4
View File
@@ -844,6 +844,10 @@ fn walk_term(
scope.remove(&p);
}
}
Term::Seq { lhs, rhs } => {
walk_term(lhs, out, builtins, scope);
walk_term(rhs, out, builtins, scope);
}
}
}
+40
View File
@@ -797,6 +797,14 @@ fn synth(
Ok(result_ty.expect("checked arms is non-empty"))
}
Term::Seq { lhs, rhs } => {
// Iter 10: `lhs ; rhs`. lhs must be Unit (its value is
// discarded). Effects from both sides accumulate. The
// expression's type is rhs's type.
let lty = synth(lhs, env, locals, effects, in_def)?;
expect_eq(&Type::unit(), &lty)?;
synth(rhs, env, locals, effects, in_def)
}
Term::Lam { params, param_tys, ret_ty, effects: lam_effects, body } => {
// Iter 8b: a lambda's type is the declared `Type::Fn`. Push
// params as locals, check the body's type matches `ret_ty`,
@@ -1203,4 +1211,36 @@ mod tests {
let err = check(&m).unwrap_err();
assert!(format!("{err}").contains("type mismatch"));
}
/// Iter 10: `seq` requires lhs to be Unit. A non-Unit lhs is a
/// type error — the value of lhs gets discarded so a useful (non-
/// Unit) value would silently vanish.
#[test]
fn seq_lhs_must_be_unit() {
let m = Module {
schema: SCHEMA.into(),
name: "t".into(),
imports: vec![],
defs: vec![fn_def(
"f",
Type::Fn {
params: vec![],
ret: Box::new(Type::int()),
effects: vec![],
},
vec![],
Term::Seq {
lhs: Box::new(Term::Lit {
lit: Literal::Int { value: 7 },
}),
rhs: Box::new(Term::Lit {
lit: Literal::Int { value: 1 },
}),
},
)],
};
let err = check(&m).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("type mismatch"), "got: {msg}");
}
}
+10
View File
@@ -634,6 +634,12 @@ impl<'a> Emitter<'a> {
Term::Lam { params, param_tys, ret_ty, effects: _, body } => {
self.lower_lambda(params, param_tys, ret_ty, body)
}
Term::Seq { lhs, rhs } => {
// Iter 10: lower lhs for its effects, discard the SSA;
// lower rhs and return its value as the whole expression.
let _ = self.lower_term(lhs)?;
self.lower_term(rhs)
}
}
}
@@ -1326,6 +1332,10 @@ impl<'a> Emitter<'a> {
bound.remove(&n);
}
}
Term::Seq { lhs, rhs } => {
Self::collect_captures(lhs, bound, captures, captures_set, builtins, top_level, import_map);
Self::collect_captures(rhs, bound, captures, captures_set, builtins, top_level, import_map);
}
}
}
+8
View File
@@ -142,6 +142,14 @@ pub enum Term {
effects: Vec<String>,
body: Box<Term>,
},
/// Sequencing (Iter 10). `lhs` is evaluated for its effects and its
/// result discarded; `rhs` is the value of the whole expression.
/// Equivalent to `let _ = lhs in rhs`, but with a dedicated node so
/// pretty-print and diagnostics read cleanly.
Seq {
lhs: Box<Term>,
rhs: Box<Term>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
+11
View File
@@ -220,6 +220,14 @@ fn term_block(t: &Term, indent: usize) -> String {
s.push(')');
s
}
Term::Seq { lhs, rhs } => {
let mut s = format!("{pad}(seq\n");
s.push_str(&term_block(lhs, indent + 2));
s.push('\n');
s.push_str(&term_block(rhs, indent + 2));
s.push(')');
s
}
}
}
@@ -298,6 +306,9 @@ fn term_inline(t: &Term) -> String {
Term::Lam { params, .. } => {
format!("(\\ {} ...)", params.join(" "))
}
Term::Seq { lhs, rhs } => {
format!("(seq {} {})", term_inline(lhs), term_inline(rhs))
}
}
}