Iter 16e: == polymorphic over Int / Bool / Str / Unit

Lifts the Int-only restriction on `==`. Declared type becomes
forall a. Fn(a, a) -> Bool; codegen monomorphises and dispatches:

  Int  → icmp eq i64
  Bool → icmp eq i1
  Str  → call @strcmp + icmp eq i32 0
  Unit → constant true (operands still emitted for side effects)
  ADT / Fn / other → CodegenError::Internal

This unblocks 16c's build_eq for non-Int lit patterns. == joins
__unreachable__ as the second polymorphic builtin (same Forall
machinery).

- check/builtins.rs: == registered as Forall(a, Fn(a, a) -> Bool).
- codegen: lower_eq dispatch table; @strcmp declared in IR header
  alongside @printf/@GC_malloc/@puts.
- examples/eq_demo.{ailx,ail.json}: covers all four supported
  scalars including a Str-lit-pattern match.
- IR snapshots refreshed: only +declare i32 @strcmp(ptr, ptr) in
  the header; every define body bit-identical.
- e2e + check + codegen tests: 124 → 133 (+9, of which +1 is the
  e2e fixture and the rest exercise the new dispatch / typecheck
  surface).

Other comparison ops (<, <=, >, >=, !=) remain Int-only — out of
scope for this iter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 23:12:24 +02:00
parent af35612c1d
commit 937782e36d
13 changed files with 666 additions and 22 deletions
+42 -18
View File
@@ -773,7 +773,11 @@ collector wired up in Iter 14f, see Decision 9); nested constructor
sub-patterns inside `match` (lifted in Iter 16a via the desugar pass);
literal sub-patterns inside a Ctor pattern (lifted in Iter 16c — the
desugar pass rewrites every `Pattern::Lit` to a `Term::If` on `==`,
both at the top level of an arm and inside a Ctor sub-pattern).
both at the top level of an arm and inside a Ctor sub-pattern);
`==` extended from Int-only to a polymorphic
`forall a. (a, a) -> Bool` over `Int`/`Bool`/`Str`/`Unit` (lifted
in Iter 16e — codegen monomorphises and dispatches on the
resolved arg type; ADT/Fn arg types are rejected at codegen).
- No effect handlers — only the built-in IO and Diverge ops.
- No refinements / SMT escalation.
@@ -798,19 +802,39 @@ 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__`.
- **Builtins.** Arithmetic operators (`+`, `-`, `*`, `/`, `%`) of
type `(Int, Int) -> Int`; ordering operators and `!=` (`!=`,
`<`, `<=`, `>`, `>=`) of type `(Int, Int) -> Bool`; logical
`not : (Bool) -> Bool`; the IO effect ops listed above;
**`==` : forall a. (a, a) -> Bool** (Iter 16e); and
**`__unreachable__ : forall a. a`** (Iter 16d).
- **`==` is polymorphic** (Iter 16e). The typechecker accepts
`==` at any type whose two sides agree (the rigid `a` of the
`Forall` is unified by HM at the use site). Codegen
monomorphises and dispatches on the resolved AIL arg type:
`Int` → `icmp eq i64`; `Bool` → `icmp eq i1`;
`Str` → `call @strcmp(ptr, ptr)` then `icmp eq i32 0`
(`@strcmp` is declared in the LLVM IR header alongside
`@printf` / `@GC_malloc`); `Unit` → constant `i1 true`
(Unit has a single inhabitant; both sides are still
evaluated for any side effects). ADT and `Fn` arg types are
rejected at codegen with a `CodegenError::Internal`
mentioning `==` and the offending type — neither has a
canonical structural-equality scheme yet, and the language
deliberately does not silently elide the check. The other
comparison ops stay Int-only; their codegen path emits
`icmp s{lt,le,gt,ge,ne}` over `i64`.
- **`__unreachable__`** 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
@@ -820,10 +844,10 @@ What **is** supported (and used as the smoke test for the pipeline):
- Literal patterns at top level and inside Ctor sub-patterns (Iter 16c,
via desugar). `(pat-lit 0)` and `(pat-ctor Cons (pat-lit 0) _)` both
parse and lower; the rewrite is to `Term::If { cond = (== sv lit) }`,
so any literal kind whose `==` is supported by the typechecker is
authorable. Today that means `Int`; `Bool`/`Str`/`Unit` lit patterns
are accepted by desugar but reach typecheck as a type error because
`==` is `(Int, Int) -> Bool` only.
so any literal kind whose `==` is supported is authorable. After
Iter 16e (`==` polymorphic over `Int`/`Bool`/`Str`/`Unit`), that
covers every lit kind the AST ships — including `(pat-lit "hi")`
over a `Str` scrutinee, exercised by `examples/eq_demo.ail.json`.
- **Imports + qualified cross-module references** via dotted names
(Iter 5). Extends to **types and constructors** (Iter 14h): a foreign
module's ADT is referenced as `(con std_pair.Pair a b)`, its ctors as