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:?}"
);
}
}
+13
View File
@@ -798,6 +798,19 @@ What **is** supported (and used as the smoke test for the pipeline):
- `if`, `let`, function calls, recursion.
- Effects on function signatures, with `do op(args)` for direct effect
ops (`io/print_int`, `io/print_bool`, `io/print_str`).
- **Builtins.** Arithmetic and comparison operators (`+`, `-`, `*`,
`/`, `%`, `==`, `!=`, `<`, `<=`, `>`, `>=`) of type
`(Int, Int) -> Int` / `Bool`; logical `not : (Bool) -> Bool`; the
IO effect ops listed above; and **`__unreachable__ : forall a. a`**
(Iter 16d). The latter is a polymorphic bottom value: a use of
`__unreachable__` typechecks against any expected type at the use
site and codegens to the LLVM `unreachable` instruction (UB if
ever executed). It is the chain machinery's deepest fall-through
for matches that the typechecker proved exhaustive, and it is
available to user code as an explicit panic primitive
(`(if cond __unreachable__ ...)` for assertions or impossible
branches). Reference site is `Term::Var { name = "__unreachable__" }`
/ form-A bare `__unreachable__`.
- **ADTs + pattern matching** (Iter 3, extended in Iter 16a/16c).
Sub-patterns of a Ctor pattern may be `Var`, `Wild`, another `Ctor`
(Iter 16a), or a literal (Iter 16c). The desugar pass flattens
+151
View File
@@ -4495,3 +4495,154 @@ polymorphic enclosing fn; needs polymorphic closure
pairs). 16d (chain-machinery exhaustiveness or
`__unreachable__`), 16e (`==` extension to
Bool/Str/Unit), 17a (per-fn arena, gated) unchanged.
## Iter 16d — chain-terminator via `__unreachable__` builtin
**Goal.** Eliminate the synthetic `Unit`-typed chain terminator
that 16a/16c emitted for matches whose arms have non-Unit return
types. Pre-16d, the desugar pass's `desugar_match` used
`Term::Lit { lit: Literal::Unit }` as the deepest fall-through of
the let-bind + chain rewrite. That terminator unifies against
`Unit` only, so any match returning (say) `Int` had to carry a
trailing `(case _ <int>)` arm whose sole purpose was to dominate
the terminator with a same-type value. Surfaced by 16c's
`categorize_first` fixture, where `IntList`'s two ctors (`Nil`,
`Cons`) are exhaustive on their own but the trailing `(case _ 0)`
was load-bearing for the chain machinery rather than for the
program's semantics.
**Architectural decision (path a, by the orchestrator).** Two
paths were on the table per the 16c entry's "Adjacent open items":
**(a)** introduce a polymorphic `__unreachable__` builtin used as
the chain default; **(b)** run an exhaustiveness pre-check in
desugar against the scrutinee's ADT and omit the terminator
entirely for exhaustive matches. Path (b) is purer (terminator
never appears for exhaustive matches) but needs ADT lookup at
desugar time, which today runs single-module and would require
threading workspace type-registry access through the pass — a
non-trivial pipeline change. Path (a) is broader: besides
unblocking 16c's fixture, it gives users a real bottom primitive
for asserts, impossible branches, and panics. Path (a) chosen.
**The new builtin.** `__unreachable__` is registered as a
**polymorphic value** (not a zero-arg fn) typed
`Type::Forall { vars: ["a"], body: Type::Var { name: "a" } }` —
i.e. `forall a. a`, the textbook bottom type. Reference site is
plain `(var __unreachable__)` (form-A bare ident
`__unreachable__`). The value-form was preferred over the
zero-arg-fn form because the obvious authoring shape (a name in
expression position) is then the right shape — `(app
__unreachable__)` would have rejected the natural use as a value
and added paren noise. Chosen the same way as how the
typechecker already treats every `Forall`-typed global: the
`Term::Var` resolver (`maybe_instantiate`) substitutes the
forall var with a fresh metavar at every use site, and
unification with the surrounding context's expected type pins
that metavar.
**Codegen lowering.** `Term::Var { name = "__unreachable__" }` in
`lower_term` emits a single `unreachable\n` line, sets
`block_terminated = true`, and returns a dummy
`("0", "i8")` SSA + LLVM-type pair. The dummy type is sound
because the existing `Term::If`, `Term::Match`, and top-level
fn-body code paths all gate downstream emission on
`block_terminated`: the terminated branch's value/type is never
fed into a phi node. In an `if`, the live branch's value flows
through to the join unchanged (existing 14e behaviour). In a
`Term::Match` default block, the arm contributes nothing to
`phi_inputs` and the join collapses to whatever non-terminated
arms produced. No alternative lowering (e.g. `abort`/`trap`)
because LLVM `unreachable` lets the optimizer aggressively prune
the unreachable region.
**What shipped.**
- `crates/ailang-check/src/builtins.rs` (+12, of which ~7 are
doc): registers `__unreachable__` in `env.globals` with type
`forall a. a`; adds it to `list()` (consumed by the
`ail builtins` CLI subcommand and `value_names()`). Mirrors
how operators like `+` / `==` are installed.
- `crates/ailang-codegen/src/lib.rs` (+15, of which ~10 are
doc): special-cases `Term::Var { name = "__unreachable__" }`
in `lower_term` to emit `unreachable` + mark
`block_terminated`; adds the same `forall a. a` entry to
`builtin_ail_type` so `synth_arg_type` resolves it during
monomorphisation walks.
- `crates/ailang-core/src/desugar.rs` (+3 net): one-line
swap of the chain default from `Term::Lit { Unit }` to
`Term::Var { name: "__unreachable__".into() }` in
`desugar_match`; module-level doc updated to reflect the new
contract; one new unit test
(`chain_default_is_unreachable_builtin`) that walks the
desugared output for a two-lit-arm match and asserts the
deepest `else_` is `Term::Var { name = "__unreachable__" }`.
- `examples/lit_pat.ailx` + `examples/lit_pat.ail.json`: the
trailing `(case _ 0)` workaround on `categorize_first` is
removed. The remaining match (`Nil` arm + `Cons (pat-lit 0)
_` arm + `Cons h _` arm) is exhaustive on `IntList`; the
chain default is unreached. Header comment rewritten to
describe 16d's role. Output unchanged: `100, 200, 999, -1,
0, 7`.
- `examples/unreachable_demo.ailx` + `.ail.json`: new fixture.
`safe_div(a, b)` returns `a / b` when `b != 0` and panics via
`__unreachable__` otherwise. Driver uses non-zero divisors
only, so the panic branch is never executed. Output: `4, 5`.
Exercises the typechecker's `forall a. a` instantiation
against `Int` and codegen's `unreachable` emission inside an
`if`'s then-branch.
- `crates/ail/tests/e2e.rs` (+18): `unreachable_demo` test;
`lit_pat_demo`'s doc updated to mention the 16d simplification.
- `docs/DESIGN.md`: new "Builtins" bullet under "What is
supported", listing every builtin (operators, `not`, IO ops)
and calling out `__unreachable__ : forall a. a` with its UB
semantics.
**Hash determinism.** Of the 36 fixture defs across all
`examples/*.ail.json`, exactly one hash changed: the
`categorize_first` def of `lit_pat` (`c4faec3abc2ed388` →
`644de0c0ec15fc17`), because that's the only def whose source
text changed. All other defs — including `lit_pat`'s other
three (`IntList`, `classify`, `main`) — are bit-identical to
their pre-16d forms. The new `unreachable_demo` fixture has
two new hashes (`safe_div`, `main`) which obviously didn't
exist before. Verified by stash-diff of `ail manifest` output
on `sum`, `list`, `maybe_int`, and `lit_pat`.
**Other fixtures that benefited.** Just `lit_pat` —
`categorize_first` was the only place in the shipped corpus
where a `(case _ ...)` arm existed solely to dominate the
chain terminator. The `(case _ 0)` arms in `nested_pat` and
`std_either_list` are semantically required (they handle
`Nil` and partial-coverage cases that the preceding nested
ctor patterns don't reach), not workarounds; they stay.
`std_list::take` / `drop` use a different workaround (base-
case-via-arm-body / `if (== n 0) Nil ...`) which 16c-aux
addresses, not 16d.
**Tests: 122 → 124 (+2).**
- e2e: 45 → 46 (`unreachable_demo`).
- `ailang-core::desugar::tests`: 25 → 26
(`chain_default_is_unreachable_builtin`).
- All other crates unchanged.
**Cumulative state, post-16d.**
- Stdlib unchanged (5 modules, 29 combinators).
- `Term`/`Pattern`/`Literal` enums unchanged. The new builtin
is purely a name-level addition (`env.globals` + a codegen
lowering branch); no AST shape changed and no schema bump.
- 16a desugar pass now does four jobs: nested-ctor flattening
(16a), LetRec lift (16b.116b.7), lit-pattern → If rewrite
(16c), and `__unreachable__` chain default (16d). Pass
remains the single AST-→-AST hop between `load_module` and
typecheck.
- Compiler bugs surfaced and fixed in dogfood since 14a: 5/5
(unchanged).
**Queue update post-16d.** 16d done. Open: `closure-of-self`
(informally 16b.5-body — unchanged); `closure-poly`
(informally 16b.5b — unchanged); 16e (`==` extension to
Bool/Str/Unit — surfaced by 16c, unchanged); 17a (per-fn
arena, gated — unchanged); 16c-aux (`std_list::take`/`drop`
refactor onto lit patterns — unchanged).
+1 -1
View File
@@ -1 +1 @@
{"defs":[{"ctors":[{"fields":[],"name":"Nil"},{"fields":[{"k":"con","name":"Int"},{"k":"con","name":"IntList"}],"name":"Cons"}],"kind":"type","name":"IntList"},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":100},"t":"lit"},"pat":{"lit":{"kind":"int","value":0},"p":"lit"}},{"body":{"lit":{"kind":"int","value":200},"t":"lit"},"pat":{"lit":{"kind":"int","value":1},"p":"lit"}},{"body":{"lit":{"kind":"int","value":999},"t":"lit"},"pat":{"p":"wild"}}],"scrutinee":{"name":"n","t":"var"},"t":"match"},"doc":"Map 0->100, 1->200, anything else->999.","kind":"fn","name":"classify","params":["n"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":-1},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"Cons","fields":[{"lit":{"kind":"int","value":0},"p":"lit"},{"p":"wild"}],"p":"ctor"}},{"body":{"name":"h","t":"var"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"p":"wild"}],"p":"ctor"}},{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"p":"wild"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Empty -> -1; first element 0 -> 0; otherwise return the head.","kind":"fn","name":"categorize_first","params":["xs"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"IntList"}],"ret":{"k":"con","name":"Int"}}},{"body":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":17},"t":"lit"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[],"ctor":"Nil","t":"ctor","type":"IntList"}],"fn":{"name":"categorize_first","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":0},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"fn":{"name":"categorize_first","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":7},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"fn":{"name":"categorize_first","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"doc":"Drive both fns. Expected (per line): 100, 200, 999, -1, 0, 7.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"lit_pat","schema":"ailang/v0"}
{"defs":[{"ctors":[{"fields":[],"name":"Nil"},{"fields":[{"k":"con","name":"Int"},{"k":"con","name":"IntList"}],"name":"Cons"}],"kind":"type","name":"IntList"},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":100},"t":"lit"},"pat":{"lit":{"kind":"int","value":0},"p":"lit"}},{"body":{"lit":{"kind":"int","value":200},"t":"lit"},"pat":{"lit":{"kind":"int","value":1},"p":"lit"}},{"body":{"lit":{"kind":"int","value":999},"t":"lit"},"pat":{"p":"wild"}}],"scrutinee":{"name":"n","t":"var"},"t":"match"},"doc":"Map 0->100, 1->200, anything else->999.","kind":"fn","name":"classify","params":["n"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":-1},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"Cons","fields":[{"lit":{"kind":"int","value":0},"p":"lit"},{"p":"wild"}],"p":"ctor"}},{"body":{"name":"h","t":"var"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"p":"wild"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Empty -> -1; first element 0 -> 0; otherwise return the head.","kind":"fn","name":"categorize_first","params":["xs"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"IntList"}],"ret":{"k":"con","name":"Int"}}},{"body":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":17},"t":"lit"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[],"ctor":"Nil","t":"ctor","type":"IntList"}],"fn":{"name":"categorize_first","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":0},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"fn":{"name":"categorize_first","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":7},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"IntList"}],"ctor":"Cons","t":"ctor","type":"IntList"}],"fn":{"name":"categorize_first","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"t":"seq"},"doc":"Drive both fns. Expected (per line): 100, 200, 999, -1, 0, 7.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"lit_pat","schema":"ailang/v0"}
+8 -6
View File
@@ -5,10 +5,13 @@
; inside Pat-Ctor's first field. Both shapes are now handled by the
; 16c desugar pass; codegen sees only If/Match-on-Ctor combinations.
;
; The trailing `_` catch-all in `categorize_first` is required by the
; 16a chain machinery: the chain's terminator is a `Unit` literal,
; reachable when no arm matches. A `_` arm dominates the chain so the
; terminator stays unreachable in practice and the match type-checks.
; Iter 16d update — the chain machinery's terminator is now the
; polymorphic `__unreachable__` builtin (`forall a. a`, lowered to
; LLVM `unreachable`) instead of a `Unit` literal. That removed the
; pre-16d need for a trailing `(case _ 0)` arm in `categorize_first`
; whose only purpose was to dominate the synthetic `Unit`-typed
; terminator. The two ctor arms (`Nil` and `Cons h _`) are
; exhaustive on `IntList`; the chain default is never reached.
;
; Expected stdout (one per line): 100, 200, 999, -1, 0, 7.
@@ -36,8 +39,7 @@
(match xs
(case (pat-ctor Nil) -1)
(case (pat-ctor Cons (pat-lit 0) _) 0)
(case (pat-ctor Cons h _) h)
(case _ 0))))
(case (pat-ctor Cons h _) h))))
(fn main
(doc "Drive both fns. Expected (per line): 100, 200, 999, -1, 0, 7.")
+1
View File
@@ -0,0 +1 @@
{"defs":[{"body":{"cond":{"args":[{"name":"b","t":"var"},{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"==","t":"var"},"t":"app"},"else":{"args":[{"name":"a","t":"var"},{"name":"b","t":"var"}],"fn":{"name":"/","t":"var"},"t":"app"},"t":"if","then":{"name":"__unreachable__","t":"var"}},"doc":"a/b for b != 0; otherwise panic via __unreachable__.","kind":"fn","name":"safe_div","params":["a","b"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"},{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":8},"t":"lit"},{"lit":{"kind":"int","value":2},"t":"lit"}],"fn":{"name":"safe_div","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"args":[{"args":[{"lit":{"kind":"int","value":15},"t":"lit"},{"lit":{"kind":"int","value":3},"t":"lit"}],"fn":{"name":"safe_div","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"t":"seq"},"doc":"Drive safe_div with non-zero divisors. Expected: 4, 5.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"unreachable_demo","schema":"ailang/v0"}
+35
View File
@@ -0,0 +1,35 @@
; Iter 16d — `__unreachable__` as an explicit user-callable primitive.
; Type: forall a. a (lowered to LLVM `unreachable`).
;
; This fixture exercises `__unreachable__` in a value position that is
; statically unreachable but still type-checks: the impossible branch
; of an if. `safe_div(a, b)` returns `a / b` when `b != 0` and panics
; via `__unreachable__` otherwise. The driver only ever calls it with
; non-zero divisors, so the panic branch is never executed.
;
; The point of the fixture is the type-system + codegen surface:
; `__unreachable__` unifies against `Int` (the fn's return type) at
; the typechecker, and codegen lowers it to `unreachable` followed by
; the rest of the if's join machinery, which is sound because the
; surrounding if's `block_terminated` flag is honoured.
;
; Expected stdout (one per line): 4, 5.
(module unreachable_demo
(fn safe_div
(doc "a/b for b != 0; otherwise panic via __unreachable__.")
(type (fn-type (params (con Int) (con Int)) (ret (con Int))))
(params a b)
(body
(if (app == b 0)
__unreachable__
(app / a b))))
(fn main
(doc "Drive safe_div with non-zero divisors. Expected: 4, 5.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(seq (do io/print_int (app safe_div 8 2))
(do io/print_int (app safe_div 15 3))))))