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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user