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
+220 -2
View File
@@ -351,7 +351,11 @@ pub fn lower_workspace(ws: &Workspace) -> Result<String> {
out.push_str("declare i32 @printf(ptr, ...)\n");
out.push_str("declare i32 @puts(ptr)\n");
out.push_str("declare ptr @GC_malloc(i64)\n\n");
out.push_str("declare ptr @GC_malloc(i64)\n");
// Iter 16e: `==` on `Str` lowers to `@strcmp` followed by
// `icmp eq i32 0`. NUL-terminated strings make this a one-liner;
// libc supplies `strcmp` so no extra link flag is needed.
out.push_str("declare i32 @strcmp(ptr, ptr)\n\n");
out.push_str(&header);
out.push_str(&body);
@@ -1500,6 +1504,25 @@ impl<'a> Emitter<'a> {
}
fn lower_app(&mut self, name: &str, args: &[Term], tail: bool) -> Result<(String, String)> {
// Iter 16e: `==` is polymorphic (`forall a. (a, a) -> Bool`).
// Dispatch on the resolved AIL arg type — the LLVM `ptr` shape
// aliases multiple AIL types (Str vs ADT vs Fn), so we cannot
// dispatch on the LLVM type alone. ADT/Fn equality is rejected
// here with a clear error; `Unit` evaluates both sides for
// their side effects then returns constant `i1 1`.
if name == "==" {
if args.len() != 2 {
return Err(CodegenError::Internal(
"builtin `==` expected 2 args".into(),
));
}
let arg_ty = self.synth_arg_type(&args[0])?;
let (a, a_ll) = self.lower_term(&args[0])?;
let (b, _b_ll) = self.lower_term(&args[1])?;
let _ = tail;
return self.lower_eq(&arg_ty, &a, &b, &a_ll);
}
// Built-in arithmetic / comparison.
if let Some((instr, ret_ty)) = builtin_binop(name) {
if args.len() != 2 {
@@ -2330,6 +2353,80 @@ impl<'a> Emitter<'a> {
}
}
/// Iter 16e: lower a `==` call after the two operands have been
/// emitted. Dispatches on the resolved AIL type of the arg side
/// (both sides have the same type after typecheck). The `_a_ll`
/// hint is the LLVM type the lowering produced for `a`; we use
/// it as a sanity check against `arg_ty`'s expected LLVM shape.
///
/// Supported:
/// - `Int` → `icmp eq i64`
/// - `Bool` → `icmp eq i1`
/// - `Str` → `@strcmp` then `icmp eq i32 0`
/// - `Unit` → constant `i1 true` (both sides already evaluated
/// for any side effects; Unit has a single inhabitant).
///
/// Rejected with `CodegenError::Internal` for ADT, `Fn`, and any
/// other type — those would need either a structural-equality
/// scheme (ADT) or a fn-pointer compare (Fn) that the language
/// does not yet specify.
fn lower_eq(
&mut self,
arg_ty: &Type,
a: &str,
b: &str,
_a_ll: &str,
) -> Result<(String, String)> {
match arg_ty {
Type::Con { name, .. } => match name.as_str() {
"Int" => {
let dst = self.fresh_ssa();
self.body.push_str(&format!(
" {dst} = icmp eq i64 {a}, {b}\n"
));
Ok((dst, "i1".into()))
}
"Bool" => {
let dst = self.fresh_ssa();
self.body.push_str(&format!(
" {dst} = icmp eq i1 {a}, {b}\n"
));
Ok((dst, "i1".into()))
}
"Str" => {
let cmp = self.fresh_ssa();
self.body.push_str(&format!(
" {cmp} = call i32 @strcmp(ptr {a}, ptr {b})\n"
));
let dst = self.fresh_ssa();
self.body.push_str(&format!(
" {dst} = icmp eq i32 {cmp}, 0\n"
));
Ok((dst, "i1".into()))
}
"Unit" => {
// Both sides have already been evaluated above for
// any side effects; Unit has a single inhabitant,
// so equality is `true` by definition.
let _ = a;
let _ = b;
Ok(("true".into(), "i1".into()))
}
other => Err(CodegenError::Internal(format!(
"`==` not supported for type `{other}` \
(ADT and user-defined types lack a structural-equality scheme)"
))),
},
Type::Fn { .. } => Err(CodegenError::Internal(
"`==` not supported for function types (no canonical fn-pointer equality)".into(),
)),
other => Err(CodegenError::Internal(format!(
"`==` not supported for type `{}`",
ailang_core::pretty::type_to_string(other)
))),
}
}
fn fresh_ssa(&mut self) -> String {
self.counter += 1;
format!("%v{}", self.counter)
@@ -2640,7 +2737,23 @@ fn builtin_ail_type(name: &str) -> Option<Type> {
};
Some(match name {
"+" | "-" | "*" | "/" | "%" => int_int_int(),
"==" | "!=" | "<" | "<=" | ">" | ">=" => int_int_bool(),
"!=" | "<" | "<=" | ">" | ">=" => int_int_bool(),
// Iter 16e: `==` is polymorphic — `forall a. (a, a) -> Bool`.
// The mono pipeline asks `synth_arg_type` for the actual arg
// types at the call site; `lower_app` then dispatches to the
// right LLVM instruction (icmp eq i64 / i1, @strcmp, or
// constant i1 1) on those resolved types.
"==" => Type::Forall {
vars: vec!["a".into()],
body: Box::new(Type::Fn {
params: vec![
Type::Var { name: "a".into() },
Type::Var { name: "a".into() },
],
ret: Box::new(Type::bool_()),
effects: vec![],
}),
},
"not" => Type::Fn {
params: vec![Type::bool_()],
ret: Box::new(Type::bool_()),
@@ -3108,6 +3221,111 @@ mod tests {
);
}
/// Iter 16e: codegen rejects `==` on ADT-typed args with a clear
/// error. The typechecker accepts the call (the rigid var of
/// `forall a. (a, a) -> Bool` unifies with the ADT type), so the
/// rejection has to happen here. The diagnostic must mention the
/// `==` symbol and the ADT type name.
#[test]
fn eq_on_adt_rejected_at_codegen() {
// Tiny ADT `data K = Mk` (nullary).
let mk = Term::Ctor {
type_name: "K".into(),
ctor: "Mk".into(),
args: vec![],
};
let m = Module {
schema: SCHEMA.into(),
name: "t".into(),
imports: vec![],
defs: vec![
Def::Type(TypeDef {
name: "K".into(),
vars: vec![],
ctors: vec![Ctor {
name: "Mk".into(),
fields: vec![],
}],
doc: None,
}),
Def::Fn(FnDef {
name: "main".into(),
ty: Type::Fn {
params: vec![],
ret: Box::new(Type::unit()),
effects: vec![],
},
params: vec![],
body: Term::Let {
name: "_b".into(),
value: Box::new(Term::App {
callee: Box::new(Term::Var { name: "==".into() }),
args: vec![mk.clone(), mk],
tail: false,
}),
body: Box::new(Term::Lit { lit: Literal::Unit }),
},
doc: None,
}),
],
};
let err = emit_ir(&m).expect_err(
"`==` on ADT must be rejected at codegen; emit_ir succeeded",
);
let msg = format!("{err:?}");
assert!(
msg.contains("==") && msg.contains("not supported"),
"expected error mentioning `==` not supported; got: {msg}"
);
}
/// Iter 16e: same negative-path guard for function-typed args.
/// `==` on `Fn` is rejected with a "not supported for function
/// types" message.
#[test]
fn eq_on_fn_rejected_at_codegen() {
// `let f = main in (== f f)` — `main` is in scope as a fn-value.
let m = Module {
schema: SCHEMA.into(),
name: "t".into(),
imports: vec![],
defs: vec![Def::Fn(FnDef {
name: "main".into(),
ty: Type::Fn {
params: vec![],
ret: Box::new(Type::unit()),
effects: vec![],
},
params: vec![],
body: Term::Let {
name: "f".into(),
value: Box::new(Term::Var { name: "main".into() }),
body: Box::new(Term::Let {
name: "_b".into(),
value: Box::new(Term::App {
callee: Box::new(Term::Var { name: "==".into() }),
args: vec![
Term::Var { name: "f".into() },
Term::Var { name: "f".into() },
],
tail: false,
}),
body: Box::new(Term::Lit { lit: Literal::Unit }),
}),
},
doc: None,
})],
};
let err = emit_ir(&m).expect_err(
"`==` on Fn must be rejected at codegen; emit_ir succeeded",
);
let msg = format!("{err:?}");
assert!(
msg.contains("==") && msg.contains("function"),
"expected error mentioning `==` and function types; got: {msg}"
);
}
#[test]
fn missing_entry_main_is_error() {
let m = Module {