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:
@@ -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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user