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:
@@ -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"]);
|
||||
}
|
||||
|
||||
@@ -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]"),
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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