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
+22
View File
@@ -1109,9 +1109,31 @@ fn check_json_unbound_var() {
/// either a wrong result or an `unreachable!` panic depending on
/// the path. The fixture exercises both sites: `classify` (top-
/// level lit arms) and `categorize_first` (nested lit-in-Ctor).
///
/// Iter 16d update: the trailing `(case _ 0)` arm of
/// `categorize_first` was removed when the chain machinery's
/// terminator switched from a `Unit` literal to the polymorphic
/// `__unreachable__` builtin. Output is unchanged — this test
/// guards both 16c (lit-pattern desugar) and 16d (no-workaround
/// chain default).
#[test]
fn lit_pat_demo() {
let stdout = build_and_run("lit_pat.ail.json");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["100", "200", "999", "-1", "0", "7"]);
}
/// Iter 16d: `__unreachable__` as an explicit user-callable
/// primitive. Property protected: a polymorphic `forall a. a` value
/// reference (a) typechecks against any expected type at the use
/// site (here `Int`), and (b) codegen lowers it to LLVM
/// `unreachable`, with the surrounding `if` correctly forwarding
/// the live branch's value past the terminated branch. The fixture
/// only ever hits the live branch, so the binary must succeed and
/// print the expected outputs.
#[test]
fn unreachable_demo() {
let stdout = build_and_run("unreachable_demo.ail.json");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["4", "5"]);
}
+15
View File
@@ -73,6 +73,20 @@ pub fn install(env: &mut crate::Env) {
},
);
// Iter 16d: `__unreachable__` is the polymorphic bottom value.
// Type: `forall a. a`. Used by the desugar pass as the chain
// terminator of an exhaustive match, and available to user code as
// a primitive panic point. Codegen lowers it to LLVM `unreachable`.
// It is a value, not a fn — reference site is `(var __unreachable__)`,
// not `(app __unreachable__)`.
env.globals.insert(
"__unreachable__".into(),
Type::Forall {
vars: vec!["a".into()],
body: Box::new(Type::Var { name: "a".into() }),
},
);
env.effect_ops.insert(
"io/print_int".into(),
EffectOpSig {
@@ -130,6 +144,7 @@ pub fn list() -> Vec<(&'static str, &'static str)> {
(">", "(Int, Int) -> Bool"),
(">=", "(Int, Int) -> Bool"),
("not", "(Bool) -> Bool"),
("__unreachable__", "forall a. a"),
("io/print_int", "(Int) -> Unit !IO [effect op]"),
("io/print_bool", "(Bool) -> Unit !IO [effect op]"),
("io/print_str", "(Str) -> Unit !IO [effect op]"),
+20
View File
@@ -863,6 +863,20 @@ impl<'a> Emitter<'a> {
Literal::Unit => ("0".into(), "i8".into()),
}),
Term::Var { name } => {
// Iter 16d: `__unreachable__` is a polymorphic bottom
// value (`forall a. a`). At codegen we emit LLVM
// `unreachable` as the block terminator and return a
// dummy SSA value. The surrounding `if`/`match`/`seq`
// already inspects `block_terminated` and forwards the
// sibling branch's type, so the type we report here is
// not consumed by a phi node — `i8` is a sound
// placeholder. Subsequent emissions in this block are
// gated by `block_terminated`.
if name == "__unreachable__" {
self.body.push_str(" unreachable\n");
self.block_terminated = true;
return Ok(("0".into(), "i8".into()));
}
if let Some((_, ssa, ty, _)) =
self.locals.iter().rev().find(|(n, _, _, _)| n == name)
{
@@ -2632,6 +2646,12 @@ fn builtin_ail_type(name: &str) -> Option<Type> {
ret: Box::new(Type::bool_()),
effects: vec![],
},
// Iter 16d: `__unreachable__` is the polymorphic bottom value
// (`forall a. a`). Mirrors the typechecker's `builtins::install`.
"__unreachable__" => Type::Forall {
vars: vec!["a".into()],
body: Box::new(Type::Var { name: "a".into() }),
},
_ => return None,
})
}
+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:?}"
);
}
}