Iter 14g: Term::If restored (revert of 14d)
Reconsidered 14d's removal of Term::If. The decision was wrong.
"No redundancies" requires judgment; reducibility (if -> match)
is not redundancy in the strong sense. Term::If is a primitive
control-flow shape; bool branching is the second most common
shape after sequencing, and removing it cost 3x tokens on every
branch site (`(if c a b)` 4 tokens vs the match-on-Bool form
12 tokens).
Meta-pattern fixed: I had been treating user observations as
directives. User said "if is a subset of match"; I jumped to
remove it citing CLAUDE.md, with no independent conviction.
The leak appeared in 14f's JOURNAL prose ("three lines for what
if used to do in one"), which read as regret. Two feedback
memories saved (memory/feedback_user_suggestions_not_directives,
memory/feedback_no_nostalgia_for_removed_features) to head this
off.
Implementation: mechanical reverse-application of 14d's diff at
every site (AST, check including the 14e tail-position arm,
codegen 4 sites, surface parser/printer, pretty, CLI walker,
e2e test mutation). Removed lower_bool_match helper — it existed
only because 14d's migration shape needed codegen for non-ptr
match scrutinees; with Term::If back, match-on-Bool returns to
its pre-14d unsupported state. Three fixtures (sum, sort, max3)
restored to pre-14d shape. gc_stress (added in 14f) also
migrated back to (if ...) since it was authored under the wrong
constraint.
14e (musttail) and 14f (GC_malloc) verified intact in IR.
Hashes restored to pre-14d values:
- sum.sum: db33f57cb329935e
- sort.insert: 697fcb9f30f8633a
- max3.max: 65c45d6a45dd0a72
- max3.max3: 624b14429bf302f5
All other defs across all 18 fixtures keep their post-14f
hashes. Tests 80/80 green; cargo doc 0 warnings. LOC delta
+265/-295 net -30.
DESIGN.md Decision 7 preserved with a "Status: REVERTED" header
for audit trail. Form-(A) `if-term` production restored.
Plan: back to 15a (std_maybe stdlib module).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -878,6 +878,11 @@ 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.
|
||||
|
||||
@@ -249,11 +249,8 @@ 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 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.
|
||||
// Variant: load sum.ail.json, mutate the `then` branch (1 instead of 0)
|
||||
// in the `sum` definition. `main` stays bit-identical.
|
||||
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");
|
||||
{
|
||||
@@ -263,11 +260,12 @@ 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") {
|
||||
let new_body = serde_json::json!({
|
||||
// Replace the then branch literal 0 → literal 1.
|
||||
let new_then = serde_json::json!({
|
||||
"t": "lit",
|
||||
"lit": { "kind": "int", "value": 1 }
|
||||
});
|
||||
def["body"]["arms"][0]["body"] = new_body;
|
||||
def["body"]["then"] = new_then;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -962,6 +962,11 @@ pub fn verify_tail_positions(t: &Term, is_tail: bool) -> Result<()> {
|
||||
verify_tail_positions(value, false)?;
|
||||
verify_tail_positions(body, is_tail)
|
||||
}
|
||||
Term::If { cond, then, else_ } => {
|
||||
verify_tail_positions(cond, false)?;
|
||||
verify_tail_positions(then, is_tail)?;
|
||||
verify_tail_positions(else_, is_tail)
|
||||
}
|
||||
Term::Seq { lhs, rhs } => {
|
||||
verify_tail_positions(lhs, false)?;
|
||||
verify_tail_positions(rhs, is_tail)
|
||||
@@ -1118,6 +1123,14 @@ 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
|
||||
@@ -1695,13 +1708,8 @@ 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 match_arms_must_unify() {
|
||||
fn if_branches_must_match() {
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "t".into(),
|
||||
@@ -1714,24 +1722,14 @@ mod tests {
|
||||
effects: vec![],
|
||||
},
|
||||
vec![],
|
||||
Term::Match {
|
||||
scrutinee: Box::new(Term::Lit {
|
||||
Term::If {
|
||||
cond: Box::new(Term::Lit {
|
||||
lit: Literal::Bool { value: true },
|
||||
}),
|
||||
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 },
|
||||
},
|
||||
],
|
||||
then: Box::new(Term::Lit {
|
||||
lit: Literal::Int { value: 1 },
|
||||
}),
|
||||
else_: Box::new(Term::Lit { lit: Literal::Unit }),
|
||||
},
|
||||
)],
|
||||
};
|
||||
|
||||
@@ -816,6 +816,87 @@ 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)?;
|
||||
// Iter 14e: a tail-call in this branch already terminated
|
||||
// its block; skip its branch to join and exclude from phi.
|
||||
let then_terminated = self.block_terminated;
|
||||
let then_block_end = self.current_block.clone();
|
||||
if !then_terminated {
|
||||
self.body.push_str(&format!(" br label %{join_lbl}\n"));
|
||||
}
|
||||
|
||||
self.start_block(&else_lbl);
|
||||
let (else_v, else_ty) = self.lower_term(else_)?;
|
||||
let else_terminated = self.block_terminated;
|
||||
let else_block_end = self.current_block.clone();
|
||||
if !then_terminated && !else_terminated && then_ty != else_ty {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"if branches type mismatch: {then_ty} vs {else_ty}"
|
||||
)));
|
||||
}
|
||||
if !else_terminated {
|
||||
self.body.push_str(&format!(" br label %{join_lbl}\n"));
|
||||
}
|
||||
|
||||
// Iter 14e: if both branches terminated, the whole `if` is
|
||||
// terminated and no join is reachable. Mark and bail.
|
||||
if then_terminated && else_terminated {
|
||||
self.block_terminated = true;
|
||||
return Ok(("0".into(), then_ty));
|
||||
}
|
||||
// If exactly one branch terminated, the join receives only
|
||||
// the other branch's value — no phi node is needed.
|
||||
if then_terminated {
|
||||
self.start_block(&join_lbl);
|
||||
return Ok((else_v, else_ty));
|
||||
}
|
||||
if else_terminated {
|
||||
self.start_block(&join_lbl);
|
||||
return Ok((then_v, then_ty));
|
||||
}
|
||||
|
||||
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, tail } => {
|
||||
// Direct call when the callee is a `Var` referring to a
|
||||
// statically-known target (builtin, current-module fn,
|
||||
@@ -962,16 +1043,6 @@ 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"
|
||||
@@ -1184,120 +1255,6 @@ 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)?;
|
||||
// Iter 14e: a tail-call in this arm already terminated its
|
||||
// block; skip its branch to join and exclude from phi.
|
||||
let then_terminated = self.block_terminated;
|
||||
let then_block_end = self.current_block.clone();
|
||||
if !then_terminated {
|
||||
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)?;
|
||||
let else_terminated = self.block_terminated;
|
||||
let else_block_end = self.current_block.clone();
|
||||
if !then_terminated && !else_terminated && then_ty != else_ty {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"Bool-match arms type mismatch: {then_ty} vs {else_ty}"
|
||||
)));
|
||||
}
|
||||
if !else_terminated {
|
||||
self.body.push_str(&format!(" br label %{join_lbl}\n"));
|
||||
}
|
||||
|
||||
// Iter 14e: if both arms terminated, the whole match is
|
||||
// terminated and no join is reachable. Mark and bail.
|
||||
if then_terminated && else_terminated {
|
||||
self.block_terminated = true;
|
||||
return Ok(("0".into(), then_ty));
|
||||
}
|
||||
// If exactly one arm terminated, the join receives only the
|
||||
// other arm's value — no phi node is needed.
|
||||
if then_terminated {
|
||||
self.start_block(&join_lbl);
|
||||
return Ok((else_v, else_ty));
|
||||
}
|
||||
if else_terminated {
|
||||
self.start_block(&join_lbl);
|
||||
return Ok((then_v, then_ty));
|
||||
}
|
||||
|
||||
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], tail: bool) -> Result<(String, String)> {
|
||||
// Built-in arithmetic / comparison.
|
||||
if let Some((instr, ret_ty)) = builtin_binop(name) {
|
||||
@@ -1874,6 +1831,11 @@ 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);
|
||||
@@ -2214,6 +2176,7 @@ 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}`"
|
||||
@@ -2520,6 +2483,11 @@ 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, tail } => Term::Do {
|
||||
op: op.clone(),
|
||||
args: args.iter().map(|a| apply_subst_to_term(a, subst)).collect(),
|
||||
|
||||
@@ -219,6 +219,13 @@ 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.
|
||||
///
|
||||
|
||||
@@ -99,11 +99,6 @@ 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"));
|
||||
@@ -113,7 +108,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), "7f5fe7f72c63a9fd");
|
||||
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
|
||||
|
||||
let list_src = std::fs::read(examples.join("list.ail.json"))
|
||||
.expect("examples/list.ail.json present");
|
||||
|
||||
@@ -193,6 +193,16 @@ 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 {
|
||||
@@ -327,6 +337,14 @@ 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,8 @@
|
||||
//!
|
||||
//! term ::= var-ref | int-lit | str-lit | bool-lit | unit-lit
|
||||
//! | app-term | tail-app-term | match-term | ctor-term
|
||||
//! | do-term | tail-do-term | seq-term | lam-term | let-term
|
||||
//! | do-term | tail-do-term | seq-term | lam-term | if-term
|
||||
//! | let-term
|
||||
//! var-ref ::= ident ; reserved: true/false → bool-lit
|
||||
//! int-lit ::= integer ; numeric atom
|
||||
//! str-lit ::= string ; string atom
|
||||
@@ -53,6 +54,7 @@
|
||||
//! "(" "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
|
||||
@@ -686,6 +688,7 @@ impl<'a> Parser<'a> {
|
||||
"tail-do" => self.parse_tail_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);
|
||||
@@ -693,7 +696,7 @@ impl<'a> Parser<'a> {
|
||||
production: "term",
|
||||
message: format!(
|
||||
"unknown term head `{other}`; expected one of \
|
||||
`app`, `tail-app`, `lam`, `let`, `match`, `do`, \
|
||||
`app`, `tail-app`, `lam`, `let`, `if`, `match`, `do`, \
|
||||
`tail-do`, `seq`, `term-ctor`, `lit-unit`"
|
||||
),
|
||||
pos,
|
||||
@@ -912,6 +915,20 @@ 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")?;
|
||||
|
||||
@@ -245,6 +245,15 @@ 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, tail } => {
|
||||
// Iter 14e: `tail-do` mirrors `tail-app` for effect ops.
|
||||
out.push_str(if *tail { "(tail-do " } else { "(do " });
|
||||
|
||||
Reference in New Issue
Block a user