Iter 16d: chain-terminator via __unreachable__ builtin

Eliminates the Unit-typed chain default that 16c left in place.
Adds __unreachable__ : forall a. a — codegen lowers to LLVM
unreachable. The 16a/16c chain machinery now uses it as the
deepest fall-through, so exhaustive matches with non-Unit arms
no longer need a (case _ ...) workaround.

- check/builtins.rs: register __unreachable__ as Forall(a, a).
- codegen: Term::Var "__unreachable__" emits LLVM unreachable
  and sets block_terminated; If/Match/fn-body gate downstream
  work on that flag.
- desugar: chain default switched from Lit{Unit} to Var.
- examples/lit_pat: categorize_first's trailing _ arm removed.
  Hash changes from c4faec3abc2ed388 to 644de0c0ec15fc17.
- examples/unreachable_demo: positive fixture using safe_div
  with __unreachable__ in the impossible branch.
- e2e + desugar tests: 122 → 124 (+2).

Path-a from 16b.2 planning: real primitive over a desugar-time
exhaustiveness pre-check (which would need cross-module type
registry access from desugar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 23:00:23 +02:00
parent e5f9828a04
commit af35612c1d
10 changed files with 353 additions and 14 deletions
+87 -7
View File
@@ -28,10 +28,15 @@
//! 2. Let-bind the scrutinee to a fresh name `$mp_N` (so that we don't
//! re-evaluate effectful scrutinees per arm).
//! 3. Build a chain of single-level matches via
//! `build_chain(s_var, arms, default)` where `default` is a unit
//! literal — a placeholder for the unreachable fallthrough. Valid
//! `build_chain(s_var, arms, default)` where `default` is the
//! polymorphic bottom builtin `__unreachable__` (`forall a. a`,
//! Iter 16d) — codegen lowers it to LLVM `unreachable`. Valid
//! programs never reach it because the checker requires
//! exhaustiveness (the catch-all arm dominates the chain).
//! Pre-16d the default was a `Unit` literal, which forced any
//! match whose arms returned a non-Unit type to carry a
//! synthetic `_` arm dominating the terminator. The polymorphic
//! `__unreachable__` removes that workaround.
//! 4. Each arm is lowered via `desugar_one_arm`:
//! - `Pattern::Wild` → arm body (catch-all; later arms are dropped).
//! - `Pattern::Var { name }` → `Let { name = scrutinee_var; body }`.
@@ -844,11 +849,14 @@ impl Desugarer {
}
let s = self.fresh();
let s_var = Term::Var { name: s.clone() };
// Document: `default` is unreachable for valid programs because
// the typechecker requires a catch-all arm (`pat-wild` or
// exhaustive ctor coverage). It only exists to give
// `build_chain` a well-typed terminator.
let default = Term::Lit { lit: Literal::Unit };
// Iter 16d: `default` is unreachable for valid programs (the
// typechecker requires either a catch-all arm or exhaustive
// ctor coverage). Use the polymorphic bottom builtin
// `__unreachable__` (`forall a. a`) so the terminator unifies
// against any arm result type without forcing a synthetic
// `Unit`-typed `_` arm to dominate it. Codegen lowers the
// var to LLVM `unreachable`.
let default = Term::Var { name: "__unreachable__".into() };
let chain = self.build_chain(&s_var, &arms, &default);
Term::Let {
name: s,
@@ -2652,4 +2660,76 @@ mod tests {
"Pattern::Lit must be classified as non-flat after 16c"
);
}
/// Iter 16d: the chain machinery's deepest fall-through is the
/// polymorphic `__unreachable__` builtin (`forall a. a`), not a
/// `Unit` literal. A match with two lit arms and a wildcard
/// catches all paths via the wild-arm body, so the deepest
/// `else_` of the chain is the `__unreachable__` var.
#[test]
fn chain_default_is_unreachable_builtin() {
// (match n (case (pat-lit 0) 100) (case (pat-lit 1) 200))
// Note: no wildcard on purpose — the chain reaches the
// synthetic terminator after both lit arms fail.
let body_match = Term::Match {
scrutinee: Box::new(Term::Var { name: "n".into() }),
arms: vec![
Arm {
pat: Pattern::Lit {
lit: Literal::Int { value: 0 },
},
body: Term::Lit {
lit: Literal::Int { value: 100 },
},
},
Arm {
pat: Pattern::Lit {
lit: Literal::Int { value: 1 },
},
body: Term::Lit {
lit: Literal::Int { value: 200 },
},
},
],
};
let m = Module {
schema: crate::SCHEMA.to_string(),
name: "t".into(),
imports: vec![],
defs: vec![Def::Fn(FnDef {
name: "f".into(),
ty: Type::Fn {
params: vec![Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
},
params: vec!["n".into()],
body: body_match,
doc: None,
})],
};
let out = desugar_module(&m);
let body = match &out.defs[0] {
Def::Fn(f) => &f.body,
_ => unreachable!(),
};
// Outer is `let $mp_N = scrutinee in <chain>`. The chain is
// `if (== sv 0) then 100 else if (== sv 1) then 200 else __unreachable__`.
let chain = match body {
Term::Let { body, .. } => body.as_ref(),
other => panic!("expected outer Let from chain machinery, got {other:?}"),
};
let inner_else = match chain {
Term::If { else_, .. } => else_.as_ref(),
other => panic!("expected outer If, got {other:?}"),
};
let deepest = match inner_else {
Term::If { else_, .. } => else_.as_ref(),
other => panic!("expected nested If, got {other:?}"),
};
assert!(
matches!(deepest, Term::Var { name } if name == "__unreachable__"),
"deepest chain fall-through must be `__unreachable__`, got {deepest:?}"
);
}
}