MVP: AILang-Sprache mit JSON-AST, Typchecker, LLVM-IR-Backend
Erste lauffähige Iteration. examples/sum.ail.json wird zu nativem Binary kompiliert und druckt 55 (Summe 1..10) als End-to-End-Test. Architektur: - ailang-core: hashbares JSON-AST + canonical-form + pretty-printer - ailang-check: monomorpher HM-Subset + Effekt-Set-Tracking - ailang-codegen: LLVM-IR-Text-Emitter (kein libllvm-link) - ail: CLI mit check/manifest/render/describe/emit-ir/build/builtins Designentscheidungen sind in docs/DESIGN.md dokumentiert; der Verlauf in docs/JOURNAL.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
//! Built-in Operationen, die der Typchecker (und Codegen) kennen.
|
||||
|
||||
use ailang_core::ast::Type;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EffectOpSig {
|
||||
pub effect: String,
|
||||
pub params: Vec<Type>,
|
||||
pub ret: Type,
|
||||
}
|
||||
|
||||
pub fn install(env: &mut crate::Env) {
|
||||
let int_int_int = Type::Fn {
|
||||
params: vec![Type::int(), Type::int()],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
};
|
||||
let int_int_bool = Type::Fn {
|
||||
params: vec![Type::int(), Type::int()],
|
||||
ret: Box::new(Type::bool_()),
|
||||
effects: vec![],
|
||||
};
|
||||
for op in ["+", "-", "*", "/", "%"] {
|
||||
env.globals.insert(op.into(), int_int_int.clone());
|
||||
}
|
||||
for op in ["==", "!=", "<", "<=", ">", ">="] {
|
||||
env.globals.insert(op.into(), int_int_bool.clone());
|
||||
}
|
||||
env.globals.insert(
|
||||
"not".into(),
|
||||
Type::Fn {
|
||||
params: vec![Type::bool_()],
|
||||
ret: Box::new(Type::bool_()),
|
||||
effects: vec![],
|
||||
},
|
||||
);
|
||||
|
||||
env.effect_ops.insert(
|
||||
"io/print_int".into(),
|
||||
EffectOpSig {
|
||||
effect: "IO".into(),
|
||||
params: vec![Type::int()],
|
||||
ret: Type::unit(),
|
||||
},
|
||||
);
|
||||
env.effect_ops.insert(
|
||||
"io/print_bool".into(),
|
||||
EffectOpSig {
|
||||
effect: "IO".into(),
|
||||
params: vec![Type::bool_()],
|
||||
ret: Type::unit(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Liefert die Liste aller registrierten Built-ins. Praktisch für CLI-Subcommand
|
||||
/// `ail builtins`, wenn der LLM erwartete Signaturen prüfen will.
|
||||
pub fn list() -> Vec<(&'static str, &'static str)> {
|
||||
vec![
|
||||
("+", "(Int, Int) -> Int"),
|
||||
("-", "(Int, Int) -> Int"),
|
||||
("*", "(Int, Int) -> Int"),
|
||||
("/", "(Int, Int) -> Int"),
|
||||
("%", "(Int, Int) -> Int"),
|
||||
("==", "(Int, Int) -> Bool"),
|
||||
("!=", "(Int, Int) -> Bool"),
|
||||
("<", "(Int, Int) -> Bool"),
|
||||
("<=", "(Int, Int) -> Bool"),
|
||||
(">", "(Int, Int) -> Bool"),
|
||||
(">=", "(Int, Int) -> Bool"),
|
||||
("not", "(Bool) -> Bool"),
|
||||
("io/print_int", "(Int) -> Unit !IO [effect op]"),
|
||||
("io/print_bool", "(Bool) -> Unit !IO [effect op]"),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
//! Typchecker für AILang (MVP).
|
||||
//!
|
||||
//! Monomorpher HM-Subset: keine Type-Variablen im Body, alle Top-Level-Defs
|
||||
//! müssen vollständig annotiert sein. Effekte werden als Set propagiert
|
||||
//! und mit der Annotation am Funktionstyp abgeglichen.
|
||||
//!
|
||||
//! Eingebaute Operationen werden über [`Builtins`] aufgelöst.
|
||||
|
||||
use ailang_core::ast::*;
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
pub mod builtins;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CheckError {
|
||||
#[error("def `{0}`: {1}")]
|
||||
Def(String, Box<CheckError>),
|
||||
|
||||
#[error("type mismatch: expected {expected}, got {got}")]
|
||||
TypeMismatch { expected: String, got: String },
|
||||
|
||||
#[error("unknown identifier: `{0}`")]
|
||||
UnknownIdent(String),
|
||||
|
||||
#[error("unknown effect operation: `{0}`")]
|
||||
UnknownEffectOp(String),
|
||||
|
||||
#[error("`{0}` is not a function (got {1})")]
|
||||
NotAFunction(String, String),
|
||||
|
||||
#[error("arity mismatch for `{name}`: expected {expected} args, got {got}")]
|
||||
ArityMismatch {
|
||||
name: String,
|
||||
expected: usize,
|
||||
got: usize,
|
||||
},
|
||||
|
||||
#[error("undeclared effect `{0}` used in body")]
|
||||
UndeclaredEffect(String),
|
||||
|
||||
#[error("function type required for fn `{0}`, got {1}")]
|
||||
FnTypeRequired(String, String),
|
||||
|
||||
#[error("param count mismatch in `{name}`: type has {ty_count}, params has {param_count}")]
|
||||
ParamCountMismatch {
|
||||
name: String,
|
||||
ty_count: usize,
|
||||
param_count: usize,
|
||||
},
|
||||
|
||||
#[error("polymorphic types not supported in MVP body of `{0}`")]
|
||||
PolymorphicNotSupported(String),
|
||||
|
||||
#[error("const `{0}` may not have effects (got !{1:?})")]
|
||||
ConstHasEffects(String, Vec<String>),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, CheckError>;
|
||||
|
||||
/// Ergebnis der Typprüfung eines Moduls: Mapping vom Symbolnamen zum
|
||||
/// (Typ, Hash) — bereit für `manifest`-Ausgabe.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CheckedModule {
|
||||
pub symbols: IndexMap<String, (Type, String)>,
|
||||
}
|
||||
|
||||
pub fn check(m: &Module) -> Result<CheckedModule> {
|
||||
let mut env = Env::new();
|
||||
builtins::install(&mut env);
|
||||
|
||||
// Pass 1: alle Top-Level-Symbole registrieren (für Vorwärtsreferenzen).
|
||||
for def in &m.defs {
|
||||
match def {
|
||||
Def::Fn(f) => {
|
||||
env.globals.insert(f.name.clone(), f.ty.clone());
|
||||
}
|
||||
Def::Const(c) => {
|
||||
env.globals.insert(c.name.clone(), c.ty.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: jede Def prüfen.
|
||||
let mut symbols = IndexMap::new();
|
||||
for def in &m.defs {
|
||||
check_def(def, &env).map_err(|e| CheckError::Def(def.name().to_string(), Box::new(e)))?;
|
||||
let h = ailang_core::hash::def_hash(def);
|
||||
let ty = match def {
|
||||
Def::Fn(f) => f.ty.clone(),
|
||||
Def::Const(c) => c.ty.clone(),
|
||||
};
|
||||
symbols.insert(def.name().to_string(), (ty, h));
|
||||
}
|
||||
|
||||
Ok(CheckedModule { symbols })
|
||||
}
|
||||
|
||||
fn check_def(def: &Def, env: &Env) -> Result<()> {
|
||||
match def {
|
||||
Def::Fn(f) => check_fn(f, env),
|
||||
Def::Const(c) => check_const(c, env),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_fn(f: &FnDef, env: &Env) -> Result<()> {
|
||||
let (param_tys, ret_ty, declared_effs) = match &f.ty {
|
||||
Type::Fn { params, ret, effects } => {
|
||||
(params.clone(), (**ret).clone(), effects.clone())
|
||||
}
|
||||
other => {
|
||||
return Err(CheckError::FnTypeRequired(
|
||||
f.name.clone(),
|
||||
ailang_core::pretty::type_to_string(other),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if f.params.len() != param_tys.len() {
|
||||
return Err(CheckError::ParamCountMismatch {
|
||||
name: f.name.clone(),
|
||||
ty_count: param_tys.len(),
|
||||
param_count: f.params.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut locals = IndexMap::new();
|
||||
for (n, t) in f.params.iter().zip(param_tys.iter()) {
|
||||
locals.insert(n.clone(), t.clone());
|
||||
}
|
||||
let mut effects = BTreeSet::new();
|
||||
let body_ty = synth(&f.body, env, &mut locals, &mut effects, &f.name)?;
|
||||
expect_eq(&ret_ty, &body_ty)?;
|
||||
|
||||
let declared: BTreeSet<String> = declared_effs.into_iter().collect();
|
||||
for e in &effects {
|
||||
if !declared.contains(e) {
|
||||
return Err(CheckError::UndeclaredEffect(e.clone()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_const(c: &ConstDef, env: &Env) -> Result<()> {
|
||||
let mut locals = IndexMap::new();
|
||||
let mut effects = BTreeSet::new();
|
||||
let v = synth(&c.value, env, &mut locals, &mut effects, &c.name)?;
|
||||
expect_eq(&c.ty, &v)?;
|
||||
if !effects.is_empty() {
|
||||
return Err(CheckError::ConstHasEffects(
|
||||
c.name.clone(),
|
||||
effects.into_iter().collect(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn synth(
|
||||
t: &Term,
|
||||
env: &Env,
|
||||
locals: &mut IndexMap<String, Type>,
|
||||
effects: &mut BTreeSet<String>,
|
||||
in_def: &str,
|
||||
) -> Result<Type> {
|
||||
match t {
|
||||
Term::Lit { lit } => Ok(match lit {
|
||||
Literal::Int { .. } => Type::int(),
|
||||
Literal::Bool { .. } => Type::bool_(),
|
||||
Literal::Unit => Type::unit(),
|
||||
}),
|
||||
Term::Var { name } => {
|
||||
if let Some(t) = locals.get(name) {
|
||||
return Ok(t.clone());
|
||||
}
|
||||
if let Some(t) = env.globals.get(name) {
|
||||
return Ok(t.clone());
|
||||
}
|
||||
Err(CheckError::UnknownIdent(name.clone()))
|
||||
}
|
||||
Term::App { callee, args } => {
|
||||
let cty = synth(callee, env, locals, effects, in_def)?;
|
||||
let (params, ret, fx) = match &cty {
|
||||
Type::Fn { params, ret, effects: fx } => {
|
||||
(params.clone(), (**ret).clone(), fx.clone())
|
||||
}
|
||||
Type::Forall { .. } => {
|
||||
return Err(CheckError::PolymorphicNotSupported(in_def.to_string()));
|
||||
}
|
||||
other => {
|
||||
return Err(CheckError::NotAFunction(
|
||||
callee_name(callee),
|
||||
ailang_core::pretty::type_to_string(other),
|
||||
));
|
||||
}
|
||||
};
|
||||
if args.len() != params.len() {
|
||||
return Err(CheckError::ArityMismatch {
|
||||
name: callee_name(callee),
|
||||
expected: params.len(),
|
||||
got: args.len(),
|
||||
});
|
||||
}
|
||||
for (a, exp) in args.iter().zip(params.iter()) {
|
||||
let actual = synth(a, env, locals, effects, in_def)?;
|
||||
expect_eq(exp, &actual)?;
|
||||
}
|
||||
for e in fx {
|
||||
effects.insert(e);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
Term::Let { name, value, body } => {
|
||||
let v = synth(value, env, locals, effects, in_def)?;
|
||||
let prev = locals.insert(name.clone(), v);
|
||||
let r = synth(body, env, locals, effects, in_def)?;
|
||||
match prev {
|
||||
Some(p) => {
|
||||
locals.insert(name.clone(), p);
|
||||
}
|
||||
None => {
|
||||
locals.shift_remove(name);
|
||||
}
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
Term::If { cond, then, else_ } => {
|
||||
let c = synth(cond, env, locals, effects, in_def)?;
|
||||
expect_eq(&Type::bool_(), &c)?;
|
||||
let t1 = synth(then, env, locals, effects, in_def)?;
|
||||
let t2 = synth(else_, env, locals, effects, in_def)?;
|
||||
expect_eq(&t1, &t2)?;
|
||||
Ok(t1)
|
||||
}
|
||||
Term::Do { op, args } => {
|
||||
let sig = env
|
||||
.effect_ops
|
||||
.get(op)
|
||||
.ok_or_else(|| CheckError::UnknownEffectOp(op.clone()))?
|
||||
.clone();
|
||||
if args.len() != sig.params.len() {
|
||||
return Err(CheckError::ArityMismatch {
|
||||
name: op.clone(),
|
||||
expected: sig.params.len(),
|
||||
got: args.len(),
|
||||
});
|
||||
}
|
||||
for (a, exp) in args.iter().zip(sig.params.iter()) {
|
||||
let actual = synth(a, env, locals, effects, in_def)?;
|
||||
expect_eq(exp, &actual)?;
|
||||
}
|
||||
effects.insert(sig.effect.clone());
|
||||
Ok(sig.ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn callee_name(t: &Term) -> String {
|
||||
match t {
|
||||
Term::Var { name } => name.clone(),
|
||||
_ => "<expr>".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_eq(expected: &Type, got: &Type) -> Result<()> {
|
||||
if expected == got {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CheckError::TypeMismatch {
|
||||
expected: ailang_core::pretty::type_to_string(expected),
|
||||
got: ailang_core::pretty::type_to_string(got),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Env {
|
||||
pub globals: IndexMap<String, Type>,
|
||||
pub effect_ops: IndexMap<String, builtins::EffectOpSig>,
|
||||
}
|
||||
|
||||
impl Env {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ailang_core::SCHEMA;
|
||||
|
||||
fn fn_def(name: &str, ty: Type, params: Vec<&str>, body: Term) -> Def {
|
||||
Def::Fn(FnDef {
|
||||
name: name.into(),
|
||||
ty,
|
||||
params: params.into_iter().map(|s| s.into()).collect(),
|
||||
body,
|
||||
doc: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_simple_arithmetic_fn() {
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "t".into(),
|
||||
imports: vec![],
|
||||
defs: vec![fn_def(
|
||||
"add",
|
||||
Type::Fn {
|
||||
params: vec![Type::int(), Type::int()],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
},
|
||||
vec!["a", "b"],
|
||||
Term::App {
|
||||
callee: Box::new(Term::Var { name: "+".into() }),
|
||||
args: vec![
|
||||
Term::Var { name: "a".into() },
|
||||
Term::Var { name: "b".into() },
|
||||
],
|
||||
},
|
||||
)],
|
||||
};
|
||||
check(&m).expect("should typecheck");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_type_mismatch() {
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "t".into(),
|
||||
imports: vec![],
|
||||
defs: vec![fn_def(
|
||||
"bad",
|
||||
Type::Fn {
|
||||
params: vec![],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
},
|
||||
vec![],
|
||||
Term::Lit {
|
||||
lit: Literal::Bool { value: true },
|
||||
},
|
||||
)],
|
||||
};
|
||||
let err = check(&m).unwrap_err();
|
||||
let msg = format!("{err}");
|
||||
assert!(msg.contains("type mismatch"), "got: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_effect_to_be_declared() {
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "t".into(),
|
||||
imports: vec![],
|
||||
defs: vec![fn_def(
|
||||
"leaks",
|
||||
Type::Fn {
|
||||
params: vec![],
|
||||
ret: Box::new(Type::unit()),
|
||||
effects: vec![], // !IO fehlt
|
||||
},
|
||||
vec![],
|
||||
Term::Do {
|
||||
op: "io/print_int".into(),
|
||||
args: vec![Term::Lit {
|
||||
lit: Literal::Int { value: 1 },
|
||||
}],
|
||||
},
|
||||
)],
|
||||
};
|
||||
let err = check(&m).unwrap_err();
|
||||
let msg = format!("{err}");
|
||||
assert!(msg.contains("undeclared effect"), "got: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lets_local_shadow_global() {
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "t".into(),
|
||||
imports: vec![],
|
||||
defs: vec![fn_def(
|
||||
"f",
|
||||
Type::Fn {
|
||||
params: vec![],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
},
|
||||
vec![],
|
||||
Term::Let {
|
||||
name: "x".into(),
|
||||
value: Box::new(Term::Lit {
|
||||
lit: Literal::Int { value: 7 },
|
||||
}),
|
||||
body: Box::new(Term::Var { name: "x".into() }),
|
||||
},
|
||||
)],
|
||||
};
|
||||
check(&m).expect("should typecheck");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_branches_must_match() {
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "t".into(),
|
||||
imports: vec![],
|
||||
defs: vec![fn_def(
|
||||
"f",
|
||||
Type::Fn {
|
||||
params: vec![],
|
||||
ret: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
},
|
||||
vec![],
|
||||
Term::If {
|
||||
cond: Box::new(Term::Lit {
|
||||
lit: Literal::Bool { value: true },
|
||||
}),
|
||||
then: Box::new(Term::Lit {
|
||||
lit: Literal::Int { value: 1 },
|
||||
}),
|
||||
else_: Box::new(Term::Lit { lit: Literal::Unit }),
|
||||
},
|
||||
)],
|
||||
};
|
||||
let err = check(&m).unwrap_err();
|
||||
assert!(format!("{err}").contains("type mismatch"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user